[造轮子]Android动态加载框架总结

46 篇文章 0 订阅
40 篇文章 0 订阅

用了一周多,做了一个Android动态加载的小玩具DCommand。支持下载APK,获取其中的资源、执行代码、启动Activity(这个是抄的,非常粗糙)。
最开始只是觉得动态加载逻辑代码很有用,如果MVP模式使用合理的话,对于大部分的逻辑更新、线上bug修复直接使用动态下发APK,更新P端的逻辑即可。后来越来越复杂,最后基本所有方面都可以动态使用,如果再深入开发的话,做个MVP框架也是可以的(当然最好迁移到RxJava上,这种网络文件操作很多的东西,用响应式编程还是很赞的)。


整体结构

这里写图片描述

理想情况下,结构是分层的:
- Apk管理,ApkConfigManager,负责下载Apk、验证安全并读取其中的配置。维护Apk相关数据库
- classloader管理,ClassManager,负责创建对应某个Apk的DexClassLoader
- resources/asset管理,ResourceManager,负责创建对应某个Apk的ResourceFetcher。维护resource相关数据库
- interface管理,CommandManager,负责加载某个interface在Apk中对应的实现。维护interface相关数据库
- component管理,ComponentManager,负责启动指定类名的Android Component(暂时只有Activity)。维护component相关数据库
- resource获取,ResourceFetcher,负责构建Resources对象,并获取其中的指定资源
- App,动态加载的使用者


流程

以获取interface的实现为例:
1. 调用CommandManager.getImplement
2. CommandManager调用ClassManager.loadClass
3. ClassManager调用ApkConfigManager.getApkInfoAndFileById
4. ApkConfigManager下载Config.json并解析
5. ApkConfigManager使用Apk的id找到url,下载Apk
6. ApkConfigManager校验Apk签名
7. ApkConfigManager通知有新Apk
8. CommandManager得到通知,更新数据库
9. ApkConfigManager通过回调,将Apk文件返回给ClassManager
10. ClassManager使用返回的文件构造ClassFetcher,通过回调返回给CommandManager
11. CommandManager使用数据库中的映射关系找到interface的实现,反射构建实现对象


细节

名词解释
Apk动态加载的目标,从网络上下载的Apk文件
宿主使用动态加载的程序
1. Apk配置和更新

使用id唯一表示Apk,不使用版本,使用时间戳来标记Apk新旧。时间戳是配置文件的生成时间点,如果Apk是在配置文件之后下载的,肯定是最新版本。使用Apk版本会要求多次读文件内容,效率有问题。

2. Apk校验

校验分为两部分:Apk签名完整性的校验,Apk签名和宿主签名的校验。

  • Apk签名校验:
    • Apk签名过程:解释0解释1。总结起来就是:先对所有文件进行digest放到MANIFEST.MF中;再对MANIFEST.MF的每一行加密,放到CERT.SF中;最后把公钥信息放到CERT.RSA中。
    • 校验过程:入口在PackageParser中,基本是签名的逆过程,但是写的实在是乱,没看懂。真正调用签名校验的地方是PackageParser.collectCertificates。
      真正使用不需要这么复杂。正常情况下,使用PackageManager.getPackageArchieveInfo并传入GET_SIGNATURES就可以了。当然也可以直接反射。
  • 签名交叉校验
    其实就是读入宿主和Apk的签名,对比是不是相同的公钥。
//读入证书
X509Certificate cert = (X509Certificate) certFactory
                        .generateCertificate(new ByteArrayInputStream(signature.toByteArray()));
//公钥字符串
cert.getPublicKey().toString();

公钥本身在公钥字符串中,在字符串modulus=和,publicExponent之间。

3. AndroidManifest的解析

Android中Xml是预处理过的,所以不能随随便便就读出来了。一个还算准确的图一个通过源码分析的解释。特别推荐一下后一个,他通过看ResType.h这些aapt相关的类,非常准确的还原了二进制的结构。
manifest文件二进制片段示例中对应关系如下(二进制是big-endian的):

        /*
            <manifest versionCode="1"

            0201// type
            1000// header size
            8800 0000// size
            0200 0000// line number
            ffff ffff// comment
            ffff ffff// ns
            0c00 0000// name
            1400// start
            1400// size
            0500// count
            0000// id
            0000// class
            0000// style
            0700 0000// ns
            0000 0000// name
            ffff ffff// raw value
            0800// size
            00// 00
            10// type
            0100 0000//data
        */

对于大部分基础信息,PackageManager.getPackageArchieveInfo就足够了。我这里用到的,PackageManager.getPackageArchieveInfo没有的,只有读取meta-data的功能。这个答案里的代码是有问题的,需要修改,具体可见Manifest类。

4. DexClassLoader

构造函数中的optimizedDirectory就是dex文件解压后的位置,第一次还是比较慢的。

5. 泛型
  • return泛型时,Java会自动类型匹配。但是用回调代替return后,自动类型匹配不管用。可以用一个比较鬼的办法:
public interface Listener{
    <T> T onXXX(T obj);
}
调用时:
Foo ret = listener.onXXX(obj);

此时obj会被匹配成Foo类的对象。

6. 动态Activity

启动动态Activity问题在于系统不识别动态Activity。基本就是用fragment或者proxyActivity来绕过。关于ProxyActivity
使用proxyActivity分为两部分:
- 宿主提供ProxyActivity,系统实际识别的就是这个Activity。ProxyActivity将所有系统的回调事件路由给DynamicActivity,基本就是所有onXXX函数
- Apk提供DynamicActivity,所有真正的逻辑都在这里。DynamicActivity将所有调用系统的函数都委托给ProxyActivity来做,比如startActivity、getXXX等。
里面有一个坑,在super.onCreate之前,getIntent是返回不了有效数据的。

后来发现,其实还是有很多牛逼的解决方法更优雅的解决这个问题的。总结一下探索新方法的思路:
1. 仔细看一遍Activity的启动流程
2. 寻找里面非native、非IPC的与Activity相关的部分(存在于同一个虚拟机实例中,可以通过反射替换成自己的实现):

  • Activity.mInstrumentation
  • PackageInfo
  • ActivityInfo
7. Resources
  • 资源是靠id标记的,id的最高8位是包名,正常生成的Apk,id都是7f打头的
  • 资源id的type字段并不是固定的,是在aapt生成时遇到什么新资源就加一生成的
  • 资源的维度简直就不能理解,老罗的解读
  • 资源读取时,就是顺序的找,找到了就返回了。所以当宿主和Apk的资源同时存在时(不修改aapt一定会有重复id),一定会出bug。所以不能反射修改Activity的mResources,这样会出错。只能显式的分开用这两种资源
  • 调用某Apk中的资源代码:
            assetManager = AssetManager.class.newInstance();
            ReflectUtils.invokeMethod(assetManager, "addAssetPath", new Class[]{String.class}, new Object[]{apkFile.getAbsolutePath()});
            mResources = new Resources(assetManager, metrics, configuration);

这个mResources就是可以用的,包括id和真正资源,系统相关代码在ResourceManager#getTopLevelResources里

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值