HotFix指可以以打补丁的方式动态修复紧急bug,而不用重新发布新版本的技术。继插件化之后,HotFix在2015年爆发,淘宝有Dexposed(需要使用xposed框架)、支付宝有AndFix(方法替换)、QQ空间有热补丁方案(从classloader加载dex的考虑)以及微信也有DexDiff(差分热补丁),Android Studio 2.0的Instant Run其实也是热补丁方案的体现。它让应用无需重新安装就可以完成更新、修复bug。


QQ空间采用的方案

QQ空间给出的方案是基于dex分包,就是将多个dex塞到app的classloader中,但对于热补丁来说,两个dex中必然会存在有重复的类,classloader会选择加载哪一个类是一个问题。

1
2
3
4
5
6
7
8
9
public Class findClass(String name,List<Throwable> suppressed){
for(Element element:dexElements){
DexFile dex = element.dexFile;
if(dex !!= null){
Class clazz = dex.loadClassBinaryName(name,definingContext,suppressed);
if(clazz != null) return clazz;
}
}
}

classloader会依次遍历所有的dex,直到找到第一个有对应class的dex,然后返回class,遍历完成都没找到则会返回null。

上面的findClass()方法(在DexClassLaoder中实现),其中有Class clazz = dex.loadClassBinaryName(name, definingContext),定位到其实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final class DexFile {
public Class loadClassBinaryName(String name, ClassLoader loader){
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
}
```
可见获取Class是调用native方法来实现的。
所以办法就有了。是不是可以**1.把dex插入到dexElements的最前面**,这是其一。另外,被引用的class不在同一类里面也会出现问题,原因就是classloader底层还有一个校验引用者和被引用者的dex是否相同的过程,而引用者打上了CLASS_ISPREVERIFIED标志就会进行校验,所以需要**2.阻止引用者被打上CLASS_ISPREVERIFIED标志**。
最终方案是往所有类的构造函数中插入了:
```java
if (ClassVerifier.PREVENT_VERIFY) {
System.out.println(AntilazyLoad.class);
}

AntilazyLoad类会被打包成单独的hack.dex,这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,在应用启动的时候加载进来,AntilazyLoad类所在的dex包必须被先加载进来,不然AntilazyLoad类会被标记为不存在。

女娲开源项目

目前已有相应的开源项目Nuwa

1. 在工程目录下build.gradle添加classpath 'cn.jiajixin.nuwa:gradle:1.2.2':
1
2
3
4
5
6
7
8
9
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.3'
classpath 'cn.jiajixin.nuwa:gradle:1.2.2'
}
}
2. 添加apply plugin: "cn.jiajixin.nuwa"到app下面的build.gradle文件,并添加依赖:
1
2
3
dependencies {
compile 'cn.jiajixin.nuwa:nuwa:1.0.0'
}
3. 在Application的onCreate()方法中调用Nuwa.init(this),然后就可以在需要的时候加载补丁了
1
Nuwa.loadPatch(this,patchFile);

执行编译,最终会在app/build/outputs/nuwa/debug/目录下生成patch.jar文件。

但是项目已经9个月没有更新了,据说也有一些坑。

RocooFix

查看项目,作者在使用Nuwa发现一些坑之后,决定在Nuwa的基础上做修改,修复了一些bug,支持不同的gradle版本,且支持dvm和art虚拟机。同样也是生成patch.jar,具体操作在作者wiki上有详细说明。


AndFix

AndFix原理就是:在Native层使用指针替换的方式替换bug方法,以达到修复bug的目的。

查看patchManager.loadPatch()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void loadPatch() {
mLoaders.put("*", mContext.getClassLoader());
Set<String> patchNames;
List<String> classes;
for (Patch patch : mPatchs) {
patchNames = patch.getPatchNames();
for (String patchName : patchNames) {
//获取补丁内Class的集合
classes = patch.getClasses(patchName);
//重点方法:修复的方法
mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(), classes);
}
}
}

接着查看mAndFixManager.fix(patch.getFile())方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void fixClass(Class<?> clazz, ClassLoader classLoader) {
// 反射找到clazz中的所有方法
Method[] methods = clazz.getDeclaredMethods();
//注解
MethodReplace methodReplace;
String clz;
String meth;
for (Method method : methods) {
//遍历所有方法,找到有MethodReplace注解的方法,即需要替换的方法
methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null)
continue;
clz = methodReplace.clazz();
meth = methodReplace.method();
//找到需要替换的方法后调用replaceMethod替换方法
if (!isEmpty(clz) && !isEmpty(meth)) {
replaceMethod(classLoader, clz, meth, method);
}
}
}

通过反射的方式找到新方法和旧方法,最后使用native方法进行方法的替换。

AndFix实现了方法的替换,从根本上解决了问题,但也频繁的使用了反射,不免对效率和性能也有着一定的影响。


微信热补丁方案

微信的方法比较简洁,和增量更新比较类似,通过新旧两个dex来生成差分包patch.dex,还自研了粒度是class级别的DexDiff算法,通过一个后台进程来进行打补丁操作。


参考