[造轮子]Android动态加载框架总结
2016-02-25 20:27
387 查看
用了一周多,做了一个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,动态加载的使用者
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签名过程:解释0,解释1。总结起来就是:先对所有文件进行digest放到MANIFEST.MF中;再对MANIFEST.MF的每一行加密,放到CERT.SF中;最后把公钥信息放到CERT.RSA中。
校验过程:入口在PackageParser中,基本是签名的逆过程,但是写的实在是乱,没看懂。真正调用签名校验的地方是PackageParser.collectCertificates。
真正使用不需要这么复杂。正常情况下,使用PackageManager.getPackageArchieveInfo并传入GET_SIGNATURES就可以了。当然也可以直接反射。
签名交叉校验
其实就是读入宿主和Apk的签名,对比是不是相同的公钥。
公钥本身在公钥字符串中,在字符串modulus=和,publicExponent之间。
manifest文件二进制片段示例中对应关系如下(二进制是big-endian的):
对于大部分基础信息,PackageManager.getPackageArchieveInfo就足够了。我这里用到的,PackageManager.getPackageArchieveInfo没有的,只有读取meta-data的功能。这个答案里的代码是有问题的,需要修改,具体可见Manifest类。
此时obj会被匹配成Foo类的对象。
使用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
资源id的type字段并不是固定的,是在aapt生成时遇到什么新资源就加一生成的
资源的维度简直就不能理解,老罗的解读
资源读取时,就是顺序的找,找到了就返回了。所以当宿主和Apk的资源同时存在时(不修改aapt一定会有重复id),一定会出bug。所以不能反射修改Activity的mResources,这样会出错。只能显式的分开用这两种资源
调用某Apk中的资源代码:
这个mResources就是可以用的,包括id和真正资源
最开始只是觉得动态加载逻辑代码很有用,如果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和真正资源
相关文章推荐
- Android Studio 第一次提交代码到Git上
- Android Studio 安装教程
- 【转】Android自定义Adapter的ListView的思路及代码
- Android4开发入门经典 之 第五部分:Service
- 避免Android中Context引起的内存泄露
- 从多方面理解 Android 体系结构
- Android Studio
- android开发oom,图片缓存
- Android摘抄总结
- Android Lint工具 <22>
- Android4开发入门经典 之 第四部分:用户界面
- Android 4.4 沉浸式状态栏的实现
- 2.10 Android 代码的混淆
- android回调是怎么实现的? --以自定义Dialog为例
- Android【基础篇】
- Android KeyCode
- android studio 配置ndk方案
- android 图片的三级缓存
- Android逆向分析基础-Dalvik虚拟机
- Android进阶——安卓调用ESC/POS打印机打印