热修复技术一般是对线上bug的紧急处理,不需要二次安装应用,在用户无感知的情况下就可以修复已知的bug。而插件化技术是把需要实现的模块和功能独立提取出来,减少宿主的规模,需要相应的功能时再去加载相应模块。热修复要完成或满足以下三个方面的需求,才能解决行业痛点。
- 代码热修复技术
- 资源热修复技术
- so库热修复技术
1. 代码修复技术
1.1 类加载机制
当我们调用DexClassLoader调用loadDex()的时候,如果不存在odex则会执行dexopt()方法,会先对DexFile每个文件的class文件进行校验和优化,代码如下:
static void verifyAndOptimizeClass (DexFile* pDexFile, ClassObject* clazz, const DexClassDef* pClassDex, bool doVerity, bool doOpt){
const char* classDescriptor;
bool verified = false;
classDescriptor = dexStringByTypeIdx(pDexFile, pClassDef->classIdex);
if(doVerify){
if(dvmVertifyClass(clazz)){ //执行类的Verify
//类被打上 CLASS_ISPREVERIFIED标志
((DexClassDef*)pClassDef)->accessFlags |= CLASS_ISPREVERIFIED;
verified = true;
}
}
if(doOpt){
bool needVerify = (gDvm.dexOptMode == OPTIMIZE_MODE_VERIFIED || gDvm.dexOptMode == OPTIMIZE_MODE_FULL);
if(!verified && needVerify){
//......
} else {
dvmOptimizeClass(clazz, false); //执行类的optimize
//类被打上 CLASS_ISOPTIMIZED标志
((DexClassDef*)pClassDef)->accessFlags |= CLASS_ISOPTIMIZED;
}
}
}
- dvmVerifyClass: 类校验,类校验的目的简单来说是为了防止类被篡改校验类的合法性。此时会对类的每个方法进行校验,如果类的所有方法中直接引用到的类和当前类都在同一个dex中的话,dvmVerifyClass就返回true。
- dvmOptimizeClass:类优化,简单的来说这个过程会把部分指令优化成虚拟机内部指令,然后会把这些指令存入类的vtable表中,需要的话,直接取出来用,会提升执行效率。
如果单纯的将加载好的dex文件放入baseloader的dexelements数组的最前面,则会出现报错,原因是:
ClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdex, bool fromUnverifiedConstant){
......
//如果类被打上了CLASS_ISPREVERIFIED标志
if(!fromUnverifiedConstant && IS_CLASS_FLAG_SET(referrer, CLASS_ISPREVERIFIED)){
if(referrer->pDvmDex != resClassCheck->pDvmDex && resClassCheck->classLoader != null){
dvmThrowIllegalAccessError("Class ref in pre-verified class resolved to unexpected implementation"); //抛异常
return NULL;
}
}
......
}
这就是类加载机制的问题所在,很多大厂都是为了解决这个问题而提出自家的方案。而dalvik虚拟机和art虚拟机的类加载机制又有所不同,dalvik只会从dex elements里面加载一个主dex,其余的在使用到了在加载。而art虚拟机本身就有对dex包有合成的功能,会一起把dex放到压缩文件中,然后依次加载。下面来看看不同厂家对合成dex的解决方案。
1.2 合成方案
首先是QQ空间的方案,他们团队在解决以上问题,用了一个独立的dex包,让其他不需要被标记Vertify标记都引入hack.class来规避以上问题,这样导致的问题是会影响到dalvik虚拟机的加载性能,每次运行这个类的时候,才会校验和优化,art虚拟机在替换的时候,需要把它引用的类以及父类也放在patch.dex包里面,导致合成包很大。
腾讯的tinker方案,主要就是将他们的补丁dex合成到有问题的dex中,然后整体合成一个dex,替换原先的dex elements数组中去,这种方案就不会导致art环境下 合成包很大问题,但是在合成新的dex文件会消耗很多heap内存,可能会出现oom的情况。
阿里的sophix方案,是移除dex文件中对内部类的定义,而类的方法实体以及其他存在dex文件中的信息不移除,这么做的考虑是为了寻找方法的时候,指针发生偏移。这样会就防止这个类被打上vertify的标记,在art虚拟机环境下也有很好的表现。
Qfix方案,他们会在校验dex包提前加载好所需的dex包,然后规避这个缺陷的产生,但是会导致dexopt方法执行不是按正常的逻辑执行的,会导致对class文件优化的时候会写死字段和方法的地址,在多态的情况下会导致调用的是另外一个方法的情况,这种情况就是方法的偏移量发生改变。
1.3 Application的处理
在Android应用启动的时候,Application是最先被加载的,在多dex情况下,主要有两种方法解决问题:
- 将Application用到所有的非系统类都和Application位于同一个dex包,这样就可以保证pre-verfied标志被打上,避免进入dvmOptResolveClass,在补丁加载完成之后,我们在清除pre-verified标志,使得接下来使用其他类不会报错。
- Application可以采用反射的方式来访问这个单独类,这样就可以把Application和其他类隔离开了。
第一种方案,我们可以在执行dexopt的结束后把Application类的vertify标志清除掉,然后加载完补丁dex之后,将Application的vertify标志恢复,在attachBaseContext时候,替换dex elements,这样Application初始化的时候就不会报错了。因为如果在Application初始化的时候,发现类vertify标志没有被打上,就会重新执行该类所有引用到的class dvmOptResolveClass方法,等运行到补丁class的时候,又会报错。第二种方案,也是Tinker解决问题的方式,一开始就dexopt TinkerApplication,运行之后然后再通过反射调用Application。
1.4 art环境下合成dex包存在的问题
如果dex包足够大的话,art虚拟机loadDex的时候会将patch.dex和原dex包进行合成一个完整的dex包,这个过程非常的耗时,如果在应用启动的时候,odex文件没有生成的话,会在主线程中去合成,所以不能在应用启动的时候合成。在应用启动的时候,看有没有odex,如果有的话,通过反射注入,如果没有则使用子线程去loadDex,重启后再生效。合成失败要将odex文件给删除掉,同时通过md5对odex包进行校验,看是否被篡改,如果不匹配,重新生成一遍odex。
2. 资源热修复技术
2.1 Instant Run 方式实现资源修复
- 通过构造一个新的AssertManager,并通过反射调用addAssertPath,把这个完整的新资源包加入到AssertManager中,这样就得到一个含有所有新资源的AssertManager。
- 找到所有之前引用到原有AssertManager的地方,通过反射,把引用处替换为AssertManager。
这种方式必须要将整个AssertManager全部替换掉原来系统生成的AssertManager,因为不管在Android L之前版本还是之后版本,直接通过addAssertPath是无法生效的。
2.2 sophix方案
由于AssertManager实际处理资源的逻辑都在native层,Java层只是一个引用的地方,完全可以调用native层AssertManager的析构函数,然后重新初始化,将所需要添加到 resource path 添加进去,这样native层就会处理这些资源文件,并不需要向Instant Run那样做很多反射的工作。
sophix构造了package id 为0x66开始的补丁包,这样会导致之前的资源id会发生偏移,有以下三种情况:
- 新增资源导致id偏移,将资源id改回原来的那个resource id。
- 内容发生改变的资源,需要改代码将资源id改为新的那个资源id,0x66开头的那个。
- 删除的资源,不要使用它即可。
3. so库热更新技术
3.1 so库冷部署实现方案
冷部署有两种方案可以实现,一种是通过接口替换的方式,通过加载指定目录下的so去实现;另外一种是类似于类修复反射注入方式,只要把我们的补丁so库的路径插入到PathClassLoader.nativeLibraryDirectories数组的最前面就能够达到加载补丁so库的目的。
3.2 so库热部署实现方案
JNI的注册方式有两种,一种是动态注册另外一种是静态注册,在动态注册的情况下通过加载so的方式,art虚拟机可以完成实时修复,但是dalvik虚拟机则还是返回之前so库的句柄,这种只能通过修改so的名字来规避。
so库的热部署方案对静态注册的方案有一定的局限性,因为虽然可以通过调用以下接口:
static void patchNativeMethod(JNIEnv* env, jclass clz){
env->UnregisterNatives(clz);
}
不管是静态注册还是动态注册的native方法之前是否执行过加载补丁so的时候都会重新去做映射。但是我们无法知道到底哪个native方法做了修改,而且就算做了修改,我们调用上面的方法,重新load补丁so库也有可能修复,也有可能不会被修复。因为,如果补丁so库在gDvm.nativeLibs的位置在原so库的下面,则不会被修复。而且还有个问题,就是受到dex的影响,如果so对应的方法在dex包中没有的话,会抛 NoSuchMethod的异常。