前面我们提到的热替换原理,根本是基于 native层方法的替换,所以当类的结构发生变化时,热部署模式就会受到限制。
但是冷部署能突破这种约束,可以更好的达到修复目的,再加上冷部署在稳定性上有独特的优势,因此可以作为热部署的有力补充而存在。
1. 冷启动类加载原理
1.1 冷启动实现方案概述
冷启动重启生效,现在一般有两种实现方案:
QQ空间 | Tinker | |
---|---|---|
原理 | 为了解决Dalvik下 unexpected dex problem异常而采用的插桩的方式,单独放一个帮助类在独立的dex中让其他类调用,阻止了类被打上CLASS_ISPREVERIFIED 标识从而规避问题的出现。最后加载补丁到dexFile对象作为参数构建一个Element对象插入到dexElments数组的最前面 |
提供dex差量包,整体替换dex的方案。差量的方式给出patch.dex ,然后将 patch.dex与应用的classes.dex 合并成一个完整的dex,完整dex加载得到的dexFile对象作为参数构建一个Element对象然后整体替换掉旧的Elements数组 |
优点 | 没有合成整包,产物比较小,比较灵活 | 自研dex差异算法,补丁包很小,dex merge成完整dex,Dalvik不影响类加载性能,Art下也不存在必须包含子类/引用类的情况 |
缺点 | Dalvik下影响类加载性能,Art下类地址写死,导致必须包含父类/引用类,最后补丁包很大 | dex合并内存消耗在vm heap上,容易OOM,最后导致dex合并失败 |
这里对tinker方案的dex merge缺陷进行简单说明。
dex merge操作是在Java层面进行的,所有对象的分配都是在java heap上完成的,如果此时进程申请的 java heap对象超过了 vm heap
规定的大小,那么进程发生OOM,系统memory killer可能会杀掉该进程,导致dex合成失败。
另外一方面,我们知道JNI层面 C++ new/malloc
申请的内存,分配中在 native heap
,native heap的增长并不受vm heap大小的限制,只受限于RAM,如果RAM不足,那么进程也会被杀死导致闪退。
所以如果只是从dex merge方面思考,在JNI层进行dex merge可以有效避免OOM,提高dex 合并的成功率,只是JNI层实现起来比较复杂而已。
Sophix另辟蹊径,寻求一种既能无侵入打包,又能将热部署模式作为补充解决方案,下面分别的 DVM和ART虚拟机的冷启动方案分别进行介绍。
1. 2 插桩实现的前因后果及造成的性能影响
如果仅仅是把补丁类打入补丁包中而不做任何处理的话,那么在运行时类加载的时候会产生异常并且退出,接下来看下抛出这个异常的前因后果。
加载一个dex文件到本地内存的时候,如果不存在 odex文件,那么首先会执行dexopt
,dexopt的入口在 davilk/opt/OptMain.cpp的main方法中,最后调用到 verifyAndOptimizeClass()
执行真正的 verify/optimize操作
在第一次安装Apk的时候,会对原先dex执行dexopt,此时假如APK只存在一个dex,dvmVerifyClass()
就会返回true。然后Apk中所有的类都会被打上 CLASS_ISPREVERIFIED
的标识,接下来执行 dvmOptimizeClass()
,类接着被打上 CLASS_ISOPTIMIZED
标识。
dvmVerifyClass()
类校验,简单来说,类校验的目的是为了防止校验类的合法性被篡改。此时会对类的每个方法进行校验,这里我们只需要知道如果类的所有方法中直接引用到的类(第一层级关系,不会进行递归搜索)和当前的类都在一个dex中的话,该方法就会返回true。dvmOptimizeClass()
类优化,简单来说就这个过程会把部分指令优化成虚拟机的内部指令,比如方法调用指令invoke-*
变成了invoke-*-quick
,quick指令会从类的vtable表中直接获取,vtable简单来说就是类的所有方法的一张大表,因此提高了方法的执行速率。
现在假定A类是补丁类,所以补丁A类在单独的dex中。类B中的某个方法引用到补丁类A,所以执行该方法会尝试解析类A。
通过上面代码得知,类B由于被打上了 CLASS_ISPREVERIFIED
标志,接下来referrer是类B,resClassCheck
是补丁类A,他们属于不同的dex,就抛出了 dvmThrowIllegalAccessError
的异常了。
所以为了解决这个问题,而引申出插桩的方案,下面通过流程来介绍下这个方案:
- 创建一个单独的无关帮助类,并将这个类打包时放到一个单独的dex文件中
- 原来的dex文件(也就是我们自己原有的程序),所有的类的构造函数都要引入第一步中的类
当然不是我们自己手动在所有类的构造函数里面加,而是通过.class
字节码修改技术,在所有的.class
类的构造函数中加入这个类的引用。 - 在安装Apk的时期,会走到上述的
verifyAndOptimizeClass()
方法校验类方法dvmVerifyClass()
,在这个方法中由于发现了 原有的代码(自己代码)引用到了一个非本代码所在dex的其他方法(即帮助类方法),所以dvmVerifyClass()
返回了false,这就导致虚拟机不会给所有的 类打上CLASS_ISPREVERIFIED
标识。 - 基于步骤3,在冷启动加载时,源程序引用了补丁类中的方法时,会去解析补丁类,走
dvmResolveClass()
方法,里面会做检验,由于源程序所有类都没有被打上CLASS_ISPERVERIFIED
标识,所以该方法不会抛出错误。
而上面步骤1所用到的无关帮助类,以及步骤2所涉及到的字节码修改技术,就是插桩方案的核心。
但是插桩会给类加载效率带来比较严重的影响,熟悉DVM开发的人知道,一个类的加载通常有三个阶段: dvmResolveClass()
、dvmLinkClass()
、dvmlnitClass()
。dvmInitClass()
阶段在类解析完成并尝试初始化类的执行,这个方法主要完成父类的初始化、当前类的初始化、静态变量的初始化赋值等操作:
bool dvmInitClass(ClassObject* clazz) {
if(clazz->status < CLASS_VERIFIED) {
// 1
clazz->status = CLASS_VERIFYING;
if (!dvmVerifyClass(clazz)) {
// 2
....
}
clazz->status = CLASS_VERIFIED;
}
if (!IS_CLASS_FLAG_SET(clazz, CLASS_ISPOTIMIZED) && gDvm.optimizing) {
//3
dvmOptimizeClass(clazz, essentialOnly); // 4
SET_CLASS_FLAG(clazz, CLASS_ISOPTIMIZED);
}
...
}
注释1: 如果类没有打上 IS_PERVERIFIED
标识,那么由于枚举类型,它是小于 CLASS_VERIFIED的,即这个if里面的内容会执行的。
注释2:执行了 dvmVerifyClass()
方法校验类。
注释3:如果类没有打上 IS_OPTIMIZED
的标识,那么 注释3的if语句里面的内容会执行
注释4:执行 dvmOptimizeClass
做类优化。
综上可知,如果类没有在加载时的verifyAndOptimizeClass()
的方法打上 CLASS_ISPERVERIFIED/CLASS_ISOPTIMIZED
这两个标识,则会在 类的初始化时去执行类的校验和优化。
由于类检验的任务可以认为是很重的,因为会对类的所有方法中的所有指令都做校验,单个类加载时耗时并不多,但是同一个时间点加载大量的类情况下,这种耗时就会被放大。所以这也是插桩给类的加载效率带来比较大影响的后果。(因为这些情况都是放在第一次安装Apk的时候做的)
我们知道若采用插桩,会导致所有类都非perverify,从而导致类校验和类优化会在类加载时触发。
平均每个类检验与优化的耗时并不长, 而且这个耗时每个类只有一次(类只会加载一次)。但是由于在应用刚启动这种场景下一般会同时加载大量的类,因此在这种情况下影响还是蛮大的,启动的时候就容易白屏,这一点用户是无法容忍的。
1.3 避免插桩的QFix方案
手机QQ热补丁轻量级 QFix方案提供了一种不同的思路:
上图是 dvmResolveClass()
的内容,从1.2节中我们知道,如果不插桩,会走到上图的第二个方框的if语句内,该判断检验两个东西:
- 类的
CLASS_ISPERVERIFIED
是否为true(插桩方案就是使这个判断为false) fromUnverifiedConstant
是否为false
而QFix的思路则是将着手点放在fromUnverifiedConstant
上,如果它为true,那么就不用再使用 插桩的方案了。
那该怎样改变这个字段的值呢?
我们首先要保证 resClass不为null,即 dvmDexGetResolvedClass()
的返回结果不为null,如果保证这个方法的返回值不为null呢?
我们只需要调用过一次 dvmDexSetResolvedClass(pDvmDex, classIdx, resClass);
就可以了,下面举个例子来简单说明:
public class B {
public static void test() {
A.a();
}
}
我们此时需要打包的类A,所以类A被打入到一个独立的补丁 dex文件中。
那么执行到类B的test方法时,A.a();
这行代码就会尝试去解析类A,此时进行 dvmResolveClass(const ClassObject* referrer, u4 classIdx, bool fromUnverifiedConstant)
.
- referrer:实际上是类B
- classIdx:类A在原dex文件结构类区中的索引ID
- fromUnverifiedConstant:是否执行 const-class/instance-of 指令
此时调用的是类A的静态a方法, invoke-static
指令不属于 const-class/instance-of
这两个指令。如果不做任何处理,dvmDexGetResolvedClass()
的返回值时null。因为类A是从补丁dex中解析加载的,类B是在原dex中,所以 B->pDvmDex != A->pDvmDex
,接下来就会看到 dvmThrowIllegalAccessError
从而导致运行异常。
为了避免异常,必须要在开始的时候,就把补丁类A添加到原有dex(pDvmDex)的pResClasses数组中。这样就确保了在执行类B的test方法时,dvmDexGetResolvedClass()
返回值不为null了。这样就会在图片中注释3的下面一行,直接返回这个resClass,而不会去执行后续类A和类B的dex一致性校验了。
具体实现,首先通过补丁工具反编译dex为smali文件拿到以下文件。
preResolveClz
需要打包的类A的描述符,非必须,为了调试方便加上该参数而已referClz
需要打包的类A所在的dex文件的任何一个类描述符,注意,这里不限定必须是引用补丁类A的某个类,实际上只要是同一个dex
中的任意一个类都可以。所以我们直接拿原dex
中的第一个类即可。classIdx
需要打包的类A在原有dex文件中的类索引ID。
然后通过dlopen 拿到 libdvm.so
库的句柄,通过 dlsym拿到该so库的 dvmResolveClass/dvmFindLoadedClass
函数指针。首先需要预加载引用类xxx/xxx/class,这样dvmFindLoadedClass(xxx/xxx/class)
返回值才不为null,然后 dvmFindLoadedClass()
的执行结果得到的 ClassObejct作为第一个参数执行 dvmResolvedClass(class,id ,true)
即可。
下面来看下JNI层代码实现。实际上可以看到 preResolveClz参数是非必须的:
jboolean resolveClodPatchClasses(JNIEnv *env, jclass clz, jstring preResolveClz, jstring refererClz, jlong classIdx, dexstuff_t *dexstuff) {
LOGD("start resolveClodPatchClasses");
ClassObject *refererObj = dexstuff->dvmFindLoadedClass_fnPtr(
Jstring2CStr(env, refererClz)); //通过refererClz 调用dvmFindLoadedClass加载补丁类
LOGD("referrer ClassObject: %s\n", refererObj->decriptor);
if (strlen(refererObj->descriptor) == 0) {
return JNI_FALSE;
}
ClassObject *resolveClass = dexstuff->dvmResolveClass-fnPtr(refererObj, classIdx, true); //调用dvmResolveClass方法
LOGD("classIDx ClassObject: %s\n", resolveClass->descriptor);
if (strlen(resolveClass->descriptor) == 0) {
return JNI_FLASE;
}
return JNI_TRUE;
}
这个思路不同于去Hook某个系统方法,而是从native层直接调用,同时更不需要插桩。具体实现需要注意以下3点:
- dvmResolveClass的第三个参数 fromUnverifiedConstant必须为true。
- 在Apk多dex的情况下,
dvmResolveClass()
的第一个参数referrer类必须跟需要打包的类在同一个dex中,但是它们两个类不需要存在任何引用关系,任何一个在同一个dex中的类作为referrer都可以。 - referrer类必须提前加载。
然而,QFix的方案有它独特的缺陷,由于是在dexopt后绕过的,dexopt会改变原有的很多逻辑,许多odex层面的优化会固定字段和方法的访问偏移,这就会导致比较严重的bug,在2.2节会详细讲解这一影响。最后采用的是自研的全量dex方案,具体在下一章讲解。
1.4 Art下冷启动实现
前面说过补丁在热部署模式下是一个完整的类,补丁的粒度是类。现在的需求是补丁既能走热部署模式也能走冷启动模式,为了减小补丁包的大小,并没有为热部署和冷启动分别准备一套补丁,而是在同一个热部署模式下补丁能够降级直接走冷启动,所以不需要做dex merge。
但是通过前面的阅读,我们知道了为了解决Art下类地址写死的问题,Tinker通过 dex merge成一个全新完整的新dex整体替换掉旧的dexElements数组。事实上,Art虚拟机下面默许已经支持多dex压缩文件的加载。
下面分别来看一下 DVM和ART对 DexFile.loadDex()
尝试把一个dex文件解析加载到native中,内存都发生了什么。实际上都是调用了 DexFile.openDexFileNative()
这个native方法。看下 native层对应的 C/C++代码具体实现。
(1)在DVM中的实现:
// dalvik/vm/native/dalvik_system_DexFile.cpp
static void Dalvik_dalvik_system_DexFile_openDexFileNative(const u4* args,
JValue* pResult)
{
....
if (hasDexExtension(sourceName)
&& dvmRawDexFileOpen(sourceName, outputName, &pRawDexFile, false) == 0) {
//加载一个原始dex文件
ALOGV("Opening DEX file '%s' (DEX)", sourceName);
pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));
pDexOrJar->isDex = true;
pDexOrJar->pRawDexFile = pRawDexFile;
pDexOrJar->pDexMemory = NULL;
} else if (dvmJarFileOpen(sourceName, outputName, &pJarFile, false) == 0) {
//加载一个压缩文件
ALOGV("Opening DEX file '%s' (Jar)", sourceName);
pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));
pDexOrJar->isDex = false;
pDexOrJar->pJarFile = pJarFile;
pDexOrJar->pDexMemory = NULL;
} else {
ALOGV("Unable to open DEX file '%s'", sourceName);
dvmThrowIOException("unable to open DEX file");
}
...
}
int dvmJarFileOpen(...){
...
entry = dexZipFindEntry(&archive, kDexInJarName); /* kDexInJarName=="classes.dex",说明只加载一个dex */
...
}
在 dvmJarFileOpen()
方法中,Dalvik尝试加载一个压缩文件的时候只会去把 classes.dex
加载到内存中。如果此时压缩文件中有多个dex文件,那么除了classes.dex
之外的其他dex
文件将会被直接忽略掉。
在Art虚拟机下:方法调用链 DexFile_openDexFileNative -> OpenDex.FilesFromOat -> LoadDexFiles
具体代码就不展示了,我们只需要知道,在Art下默认已经支持加载压缩文件中包含多个dex,首先肯定加载primary dex也就是 classes.dex
,后续会加载其他的dex。所以补丁类只需要放到classes.dex
中即可,后续出现在其他dex中的“补丁类”是不会被重复加载的。
所以Sophix得到在Art最终的冷启动方案:我们只要把补丁dex命名为classes.dex
。原Apk中的dex依次命名为 classes(2,3,4...).dex
就可以了,然后一起打包为一个压缩文件,在通过 DexFile.loadDex()
得到DexFile
对象,最后用该DexFile
对象整体替换旧的dexElements
数组就可以了。
Sophix方案和Tinker方案的不同点如下所示:
需要注意:
- 补丁dex必须命名为
classes.dex
- 用
loadDex()
得到的DexFile完整替换掉 dexElements数组而不是插入。
1.5 不得不说的其他点
我们知道DexFile.loadDex()
尝试把一个dex文件解析并加载到native内存, 在加载到native内存之前, 如果dex不存在对应的odex, 那么Dalvik下会执行dexopt
, Art下会执行dexoat
, 最后得到的都是一个优化后的odex。 实际上最后虚拟机执行的是这个odex而不是dex。
现在有这么一个问题,如果dex足够大那么dexopt/dexoat实际上是很耗时的,根据上面我们提到的方案, Dalvik下实际上影响比较小, 因为loadDex仅仅是补丁包。 但是Art下影响是非常大的, 因为loadDex是补丁dex和apk中原dex合并成的一个完整补丁压缩包, 所以dexoat非常耗时。
所以如果优化后的odex文件没生成或者没生成一个完整的odex文件, 那么loadDex便不能在应用启动的时候进行的, 因为会阻塞loadDex线程, 一般是主线程。 所以为了解决这个问题, 我们把loadDex当做一个事务来看, 如果中途被打断, 那么就删除odex文件, 重启的时候如果发现存在odex文件, loadDex完之后, 反射注入/替换dexElements数组, 实现patch。 如果不存在odex文件, 那么重启另一个子线程loadDex, 重启之后再生效。
另外一方面为了patch补丁的安全性, 虽然对补丁包进行签名校验, 这个时候能够防止整个补丁包被篡改, 但是实际上因为虚拟机执行的是odex而不是dex, 还需要对odex文件进行md5完整性校验, 如果匹配, 则直接加载。 不匹配,则重新生成一遍odex文件, 防止odex文件被篡改。
1.6 完整的方案考虑
代码修复冷启动方案由于它的高兼容性, 几乎可以修复任何代码修复的场景, 但是注入前被加载的类(比如:Application类)肯定是不能被修复的。 所以我们把它作为一个兜底的方案, 在没法走热部署或者热部署失败的情况, 最后都会走代码冷启动重启生效, 所以我们的补丁是同一套的。 具体实施方案对Dalvik下和Art下分别做了处理:
- Dalvik下通过巧妙的方式避免插桩, 没有带来任何类加载效率的影响。
- Art下本质上虚拟机已经支持多dex的加载, 我们要做的仅仅是把补丁dex作为主dex(classes.dex)加载而已。
2 多态对冷启动类加载的影响
前面我们知道冷启动方案几乎可以修复任何场景的代码缺陷,但Dalvik下的QFix方案存在很大的限制,下面将深入介绍在目前方案下为什么会有这些限制,同时给出具体的解决方案。
2.1 重新认识多态
实现多态的技术一般叫做动态绑定,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。多态一般指的是非静态私有方法的多态,field和静态方法不具有多态性。示例如下:
public class B extends A {
Strign name = "B name";
@Override
void a_t1() {
System.out.println("B a_t1");
}
void b_t1(){
}
public static void main(String[] args) {
A obj = new B();
System.out.println(obj.name);
obj.a_t1();
}
}
class A {
String name = "A name";
void a_t1() {
System.out.println("A a_1...");
}
void a_t2();
}
输出结果: A name / B a_t1
可以看到name这个field没有多态性,print这个方法具有多态性,这里先分析一下方法多态性的实现。首先 new B()
的执行会尝试加载类B,方法调用链 dvmResolveClass->dvmLinkClass->createVtable
,此时会为类B创建一个vtable,其实在虚拟机中加载每个类都会为这个类生成一张vtable
表,vtable
表就是当前类的所有virtual
方法的一个数组,当前类和所有继承父类的public/protected/default
方法就是virtual方法,因为public/protected/default修饰的方法是可以被继承的。private/static方法不属于这个范畴,因为不能被继承。
这里就不放 createVtable()
的代码了,有兴趣的可以自行上网查阅,这里来大概说一下它做了什么,子类vtable的大小等于子类virtual方法数+父类vtable的大小:
- 整体复制父类的vtable到子类的vtable
- 遍历子类的virtual方法集合,如果方法原型一致,说明是重写父类方法,那么在相同索引位置处,子类重写方法覆盖掉vtable中父类的方法
- 若方法原型不一致,那么把该方法添加到vtable末尾。
所以在上述示例中,假如父类A的vtable是 vtable[0]=A.a_t1, vtable[1]=A.a_t2
,那么B类的vtable就是 vtable[0]=B.a_t1, vtable[1]=A.a_t2, vtable[2]=B.b_t1
。接下来 obj.a_t1()
发生了什么。invoke-virtual指令的解释如下:
...
if(methodCallRange) {
thisPtr = (Object*) GET_REGISTER(vdst);
} else {
thisPtr = (Object*) GET_REGISTER(vdst & 0x0f); //当前对象
}
baseMethod = dvmDexGetReslvedMethod(methodClassDex, ref); //是否已经解析过该方法
if(baseMethod == NULL) {
baseMethod = dvmResolveMethod(curMethod->clazz, ref, METHOD_VIRTUAL);
//没有解析过该方法调用 dvmResolveMethod,baseMethod得到的当然是A.a_t1方法
...
}
methodToCall = thisPtr->clazz->vtable[baseMethod->methodIndex]; /* A.a_t1方法在类A的vtable中的索引去类B的vtable中查找
...
首先 obj 引用类型是基类A,所以上述代码中 baseMethod
拿到的是 A.a_t1()
,baseMethod->methodIndex
是该方法在类A的vtable中的索引0,obj的实际类型是类B,所以thisPtr->clazz就是类B,那么 B.vtable[0]
就是 B.a_t1()
方法,所以 obj.a_t1()
实际上最后调用的是 B.a_t1()
方法。这样就实现了方法的多态。
至于field/static方法为什么不具有多态性,这里不进行详细的代码分析,有需要的可以看 iget/invoke-static
的指令解释,简单来讲,是从当前变量的引用类型而不是实际类型中查找,如果找不到,再去父类中递归查找。
所以field和static方法不具备多态性。
2.2 冷启动方案限制
下面来看一下如果新增了一个 public/protected/default方法,会出现什么情况。
public class Demo {
public static void test_addMethod(){
A obj = new A();
obj.a_t2();
}
}
class A {
int a =0;
//新增a_t1方法
void a_t1() {
Log.d("Sophix","A a_t1");
}
void a_t2() {
Log.d("Sophix","A a_t2");
}
}
修复后的APK中新增了 a_t1()
方法,DEMO类不做任何的修复,测试发现应用补丁后Demo.test_addMethod()
得到的结果竟然是 Sophix: A a_t1,这表明 obj.a_t2()
执行的是 a_t1()
方法,下面深入分析一下本质原因。
在 2.1节提到过,在dex文件第一次加载的时候,会执行dexopt,dexopt有 verify和optimize两个过程,那分别就是类校验和类优化。
这里主要介绍一下 optimize
阶段:
//Android4.4 dalvik/vm/analysis/Optimize.cpp
void dvmOptimizeClass(ClassObject* clazz, bool essentialOnly)
{
int i;
for (i = 0; i < clazz->directMethodCount; i++) {
optimizeMethod(&clazz->directMethods[i], essentialOnly); // 1
}
for (i = 0; i < clazz->virtualMethodCount; i++) {
optimizeMethod(&clazz->virtualMethods[i], essentialOnly); // 2
}
}
static void optimizeMethod(Method* method, bool essentialOnly)
{
...
/*
* non-essential substitutions:
* invoke-{virtual,direct,static}[/range] --> execute-inline
* invoke-{virtual,super}[/range] --> invoke-*-quick
*/
if (!matched && !essentialOnly) {
switch (opc) {
case OP_INVOKE_VIRTUAL:
if (!rewriteExecuteInline(method, insns, METHOD_VIRTUAL)) {
rewriteVirtualInvoke(method, insns, //4
OP_INVOKE_VIRTUAL_QUICK);
}
break;
....
}
注释1:对direct方法进行类优化(即不能继承的方法)
注释2:对virtual方法进行类优化(即可以继承的方法)
注释3:如果是虚方法,重写 invoke-virtual为虚拟机内部指令 invoke-virtual-quick,这个指令后面跟的立即数(insns)就是该方法在类vtable中的索引值。
invoke-virtual-quick
效率比 invoke-virtual
更高,因为它直接从实际类型的vtable中获取调用方法指针,而省略了 dvmResolveMethod从变量的引用类型获取方法在vtable索引ID的步骤,所以更高效。
所以很容易知道在上面代码中示例中,方法调用错乱发生的本质原因了。打包前类A的 vtable值时 vtable[0]=a_t2
。打包后类新增了a_t1方法, 那么类A的vtable值为 vtable[0]=a_t1, vtable[1]=a_t2
,但是 obj.a_t2()
这行代码在odex中的指令实际上是 invoke-virtual-quick A.vtable[0]
,所以导包前调用的是 a_t2()方法,打包后调用的是 a_t1方法,导致了方法的调用错乱。
(其实就是加载期类优化所导致的)
2.3 终极解决方案
可见,由于多态的影响,QFix的方案最终会遭到问题,我们最后的希望就是寄托于类似Tinker方案的完整dex解决方案。
利用Google已经开源的DexMerge方案,把补丁dex和原dex合并成一个完整的dex似乎是可行的,但仅仅这样还是不够的,多dex下如果DexMerge抛出了65535方法数超了异常,DexMerge会导致内存风暴,在内存不足的情况下容易更新失败。完整的dex合成要求在移动端进行,且实现较为复杂。
因此,Sophix自研了一套完整的dex方案,具体是如何实现的,请看下一章节。