Fork me on GitHub

Android热更新总结

Android热更新总结

类加载方案

类加载方案,我更偏向于叫dex插队方案,实现原理基于ClassLoader,原理看Android中的ClassLoader。使用这类的基本是腾讯系。

大致流程如下

  1. 应用刚开始启动的时候(App.onCreated),连接服务器,检测是否有更新补丁patch.dex,如果有就下载下来。
  2. 创建新的ClassLoader主动加载补丁patch.dex中的类,反射获取dexElements。
  3. 通过反射将上面获取的dexElements插入到App应用当前的类加载的dexElements之前。
  4. 这样当应用要调用出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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
63 ClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdx,
64 bool fromUnverifiedConstant)
65{
66 DvmDex* pDvmDex = referrer->pDvmDex;
67 ClassObject* resClass;
68 const char* className;

// 这是查找缓存 是否已经加载过该class,下面讲手Q方案会用到,这里暂时忽略
74 resClass = dvmDexGetResolvedClass(pDvmDex, classIdx);
75 if (resClass != NULL)
76 return resClass;

//在这里通过classIdx在dex文件中查找原先的bug类
90 className = dexStringByTypeIdx(pDvmDex->pDexFile, classIdx);
91 if (className[0] != '\0' && className[1] == '\0') {

93 resClass = dvmFindPrimitiveClass(className[0]);
94 } else {

//在这里通过ClassLoader来查找bug类,根据修复原理,这个时候找到的bug类是插队进去的已经修复的类
95 resClass = dvmFindClassNoInit(className, referrer->classLoader);
96 }
97
98 if (resClass != NULL) {
//检测是否需要进行dex校验
118 if (!fromUnverifiedConstant &&
119 IS_CLASS_FLAG_SET(referrer, CLASS_ISPREVERIFIED))
120 {
121 ClassObject* resClassCheck = resClass;
122 if (dvmIsArrayClass(resClassCheck))
123 resClassCheck = resClassCheck->elementClass;
//dex校验 referrer->pDvmDex是class.dex resClassCheck->pDvmDex是patch.dex 所以报错!
125 if (referrer->pDvmDex != resClassCheck->pDvmDex &&
126 resClassCheck->classLoader != NULL)
127 {
128 ALOGW("Class resolved by unexpected DEX:"
129 " %s(%p):%p ref [%s] %s(%p):%p",
130 referrer->descriptor, referrer->classLoader,
131 referrer->pDvmDex,
132 resClass->descriptor, resClassCheck->descriptor,
133 resClassCheck->classLoader, resClassCheck->pDvmDex);
134 ALOGW("(%s had used a different %s during pre-verification)",
135 referrer->descriptor, resClass->descriptor);
136 dvmThrowIllegalAccessError(
137 "Class ref in pre-verified class resolved to unexpected "
138 "implementation");
139 return NULL;
140 }
141 }
//如果加载class没问题 就把他放到缓存里去
154 dvmDexSetResolvedClass(pDvmDex, classIdx, resClass);
155 } else {

160 }
161
162 return resClass;
163}

待解决:为什么multiDex没有出现问题?应该是从条件上直接避免了判断?具体还要查看dvm加载流程

判断是否要进行dex校验的代码为:

1
2
118        if (!fromUnverifiedConstant &&
119 IS_CLASS_FLAG_SET(referrer, CLASS_ISPREVERIFIED))

2个条件 一个是 fromUnverifiedConstant 一个是 CLASS_ISPREVERIFIED 标记。

1
2
3
57 * "fromUnverifiedConstant" should only be set if this call is the direct
58 * result of executing a "const-class" or "instance-of" instruction, which
59 * use class constants not resolved by the bytecode verifier.

fromUnverifiedConstant的注释说明如上,稍后会说明,下面主要来说一下CLASS_ISPREVERIFIED

http://androidxref.com/4.4.4_r1/xref/dalvik/vm/analysis/DexPrepare.cpp#1067

而为类打上CLASS_ISPREVERIFIED标记的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1067    if (doVerify) {
1068 if (dvmVerifyClass(clazz)) {
1069 /*
1070 * Set the "is preverified" flag in the DexClassDef. We
1071 * do it here, rather than in the ClassObject structure,
1072 * because the DexClassDef is part of the odex file.
1073 */
1074 assert((clazz->accessFlags & JAVA_FLAGS_MASK) ==
1075 pClassDef->accessFlags);
1076 ((DexClassDef*)pClassDef)->accessFlags |= CLASS_ISPREVERIFIED;
1077 verified = true;
1078 } else {
1079 // TODO: log when in verbose mode
1080 ALOGV("DexOpt: '%s' failed verification", classDescriptor);
1081 }
1082 }

http://androidxref.com/4.4.4_r1/xref/dalvik/vm/analysis/DexVerify.cpp#40

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
40bool dvmVerifyClass(ClassObject* clazz)
41{
42 int i;
43
44 if (dvmIsClassVerified(clazz)) {
45 ALOGD("Ignoring duplicate verify attempt on %s", clazz->descriptor);
46 return true;
47 }
48
49 for (i = 0; i < clazz->directMethodCount; i++) {
50 if (!verifyMethod(&clazz->directMethods[i])) {
51 LOG_VFY("Verifier rejected class %s", clazz->descriptor);
52 return false;
53 }
54 }
55 for (i = 0; i < clazz->virtualMethodCount; i++) {
56 if (!verifyMethod(&clazz->virtualMethods[i])) {
57 LOG_VFY("Verifier rejected class %s", clazz->descriptor);
58 return false;
59 }
60 }
61
62 return true;
63}

通过遍历clazz->directMethodsclazz->virtualMethods来验证。

directMethods包含了以下方法:

  • static方法
  • private方法
  • 构造函数

解决方法

想要避免CLASS_ISPREVERIFIED就要从上述的三个条件中着手:

  • 让class不被打上CLASS_ISPREVERIFIED标志,就不会进行dex校验
  • 让fromUnverifiedConstant 为true,就不会进行dex校验
  • 让dex校验判断异常的语句referrer->pDvmDex != resClassCheck->pDvmDex 不成立

下面将三个解决方案分别对应上述三点。

QQ空间

往所有类的构造函数里面插入了一段代码:

1
2
3
if (ClassVerifier.PREVENT_VERIFY) {
System.out.println(AntilazyLoad.class);
}

并将AntilazyLoad类打包成一个单独的hack.dex,这样就让classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了。

但是这样做的缺点就是失去了dvm的优化效果了:verify+optimize

手机QQ

通过fromUnverifiedConstant来突破

方案一

1
2
118        if (!fromUnverifiedConstant &&
119 IS_CLASS_FLAG_SET(referrer, CLASS_ISPREVERIFIED))
1
2
3
57 * "fromUnverifiedConstant" should only be set if this call is the direct
58 * result of executing a "const-class" or "instance-of" instruction, which
59 * use class constants not resolved by the bytecode verifier.

注释中的翻译大概是:只有通过”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
2
3
4
5
6
7
8
9
10
11
4636ClassObject* dvmFindLoadedClass(const char* descriptor)
4637{
4638 int result;
4639
4640 dvmHashTableLock(gDvm.loadedClasses);
4641 result = dvmHashForeach(gDvm.loadedClasses, findClassCallback,
4642 (void*) descriptor);
4643 dvmHashTableUnlock(gDvm.loadedClasses);
4644
4645 return (ClassObject*) result;
4646}

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层进行方法的结构体信息对换,从而实现完美的方法新旧替换,从而实现热修复功能

todo

AndFix

参考

QFix探索之路—手Q热补丁轻量级方案