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) {
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) {
Method[] methods = clazz.getDeclaredMethods();
MethodReplace methodReplace;
String clz;
String meth;
for (Method method : methods) {
methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null )
continue ;
clz = methodReplace.clazz();
meth = methodReplace.method();
if (!isEmpty(clz) && !isEmpty(meth)) {
replaceMethod(classLoader, clz, meth, method);
}
}
}
通过反射的方式找到新方法和旧方法,最后使用native方法进行方法的替换。
AndFix实现了方法的替换,从根本上解决了问题,但也频繁的使用了反射,不免对效率和性能也有着一定的影响。
微信热补丁方案 微信的方法比较简洁,和增量更新比较类似,通过新旧两个dex来生成差分包patch.dex,还自研了粒度是class级别的DexDiff算法,通过一个后台进程来进行打补丁操作。
参考