您的位置:首页 > 移动开发 > Android开发

[造轮子]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,动态加载的使用者

流程

以获取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和真正资源
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: