Android热更新总结
类加载方案
类加载方案,我更偏向于叫dex插队方案,实现原理基于ClassLoader,原理看Android中的ClassLoader。使用这类的基本是腾讯系。
大致流程如下
- 应用刚开始启动的时候(App.onCreated),连接服务器,检测是否有更新补丁patch.dex,如果有就下载下来。
- 创建新的ClassLoader主动加载补丁patch.dex中的类,反射获取dexElements。
- 通过反射将上面获取的dexElements插入到App应用当前的类加载的dexElements之前。
- 这样当应用要调用出bug的模块的时候,会优先加载patch.dex中的类,从而实现bug修复。
下图中Qzone.class就是要修复的类。
Dalvik虚拟机的CLASS_ISPREVERIFIED
在dvm虚拟机中,如果某个类Clz1
被打上了CLASS_ISPREVERIFIED
标志,就会进行dex校验,如果检验失败则抛出”unexpected DEX”异常。如果对于Clz1
来说,被其引用到的类,都应该与Clz1
在同一个dex文件中,就会打上CLASS_ISPREVERIFIED
标签。
http://androidxref.com/4.4.4_r1/xref/dalvik/vm/oo/Resolve.cpp#119
1 | 63 ClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdx, |
待解决:为什么multiDex没有出现问题?应该是从条件上直接避免了判断?具体还要查看dvm加载流程
判断是否要进行dex校验的代码为:
1 | 118 if (!fromUnverifiedConstant && |
2个条件 一个是 fromUnverifiedConstant
一个是 CLASS_ISPREVERIFIED
标记。
1 | 57 * "fromUnverifiedConstant" should only be set if this call is the direct |
fromUnverifiedConstant的注释说明如上,稍后会说明,下面主要来说一下CLASS_ISPREVERIFIED
http://androidxref.com/4.4.4_r1/xref/dalvik/vm/analysis/DexPrepare.cpp#1067
而为类打上CLASS_ISPREVERIFIED
标记的代码如下:
1 | 1067 if (doVerify) { |
http://androidxref.com/4.4.4_r1/xref/dalvik/vm/analysis/DexVerify.cpp#40
1 | 40bool dvmVerifyClass(ClassObject* clazz) |
通过遍历clazz->directMethods
和clazz->virtualMethods
来验证。
directMethods包含了以下方法:
- static方法
- private方法
- 构造函数
解决方法
想要避免CLASS_ISPREVERIFIED
就要从上述的三个条件中着手:
- 让class不被打上
CLASS_ISPREVERIFIED
标志,就不会进行dex校验 - 让fromUnverifiedConstant 为true,就不会进行dex校验
- 让dex校验判断异常的语句
referrer->pDvmDex != resClassCheck->pDvmDex
不成立
下面将三个解决方案分别对应上述三点。
QQ空间
往所有类的构造函数里面插入了一段代码:
1 | if (ClassVerifier.PREVENT_VERIFY) { |
并将AntilazyLoad类打包成一个单独的hack.dex,这样就让classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED
的标志了。
但是这样做的缺点就是失去了dvm的优化效果了:verify+optimize
手机QQ
通过fromUnverifiedConstant来突破
方案一
1 | 118 if (!fromUnverifiedConstant && |
1 | 57 * "fromUnverifiedConstant" should only be set if this call is the direct |
注释中的翻译大概是:只有通过”const-class” or “instance-of”指令直接调用的时候,fromUnverifiedConstant才为true。
补丁安装后,预先以 const-class/instance-of 方式主动引用补丁类,这次引用会触发加载补丁类并将引用放入 dex 的已解析类缓存里,后续 app 实际业务逻辑引用到补丁类时,直接从已解析缓存里就能取到,这样很简单地就绕开了“unexpected DEX”异常。
缺点:想要预先引用补丁类,意味着一开始就要知道出现bug的类的名称是哪个,这显然不现实。
方案二
在Native层主动调用dvmResolveClass方法,让传入的fromUnverifiedConstant为true
1 | javaClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdx,bool fromUnverifiedConstant) |
所需要的有:
- dvmResolveClass方法地址
- 参数 ClassObject* referrer 地址
- 被引用类(补丁类) 的id classIdx
- fromUnverifiedConstant 直接手动置1或true即可
dvmResolveClass的获取可以通过dlopen获取/system/lib/libdvm.so句柄,dlsym获取函数地址。
referrer地址可以通过dvmFindLoadedClass
来获取
http://androidxref.com/4.4.4_r1/xref/dalvik/vm/oo/Class.cpp#4636
1 | 4636ClassObject* dvmFindLoadedClass(const char* descriptor) |
dvmFindLoadedClass可以根据类的描述符来获取已加载的类,所以在补丁注入成功后,在每个 dex 里找一个固定的已经加载成功的引用类即可。对于主Dex,直接找Application即可,对于分Dex,手Q的分 dex 方案有这样的逻辑:每当一个分 dex 完成注入,主Dex都会尝试加载该分dex里的一个固定空类来验证分dex是否注入成功了。那么这个固定空类就可以作为patch的引用。
至于补丁类的classIdx 通过dexdump -h
即可获取:
这个过程可以通过一个小程序自动进行:
输入: 原有 apk 的所有 dex、补丁包所有的类名
输出: 补丁包每个类所在 dex 的编号以及 classIdx 的值
微信Tinker
微信Tinker框架的原理是通过生成dex差量包的方式,与QQ空间超级补丁技术基本相同,区别在于不再将patch.dex增加到elements数组中,而是差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并,然后整体替换掉旧的dex文件,以达到修复的目的。
底层替换方案
原理:直接在native层进行方法的结构体信息对换,从而实现完美的方法新旧替换,从而实现热修复功能