Android 热修复技术概述
1. Tinker
Tinker 是腾讯推出的一款热修复框架,主要通过以下几个方面来实现修复:
- 类差异:采用腾讯自研的 DexDiff 算法,针对 dex 格式开发,优化合成效率和差异包大小(类似 bsdiff,可以忽略文件格式,针对二进制数据)。
- so 替换:使用 bsdiff 自动计算 BaseApk 与修改后的 apk 差异,生成 patch.dex;运行后,通过 classloader 加载 patch.dex 与 apk 本身的 classes.dex 合成。
2. meituan/Robust
Robust 是美团推出的热修复框架,利用 install-run 原理,在代码编译期间插入代码(ChangeQuickRedirect)。如果这个不为 null,就会先执行插入的代码,通过插桩修改 class 文件,然后通过代理的方式运行修复后的代码。
3. Qzone
Qzone 热修复基于 multidex,把有 bug 的类单独打包进 dex,在运行时加载 dex 补丁。
4. alibaba/AndFix
AndFix 是阿里巴巴推出的热修复框架,通过 native 动态修改 ARTMethod 指向方法,替换 java 层方法。通过 native 层 hook java 层的代码,可以即时生效,但需要使用 NDK。
热修复机制
热修复的核心是通过修改 classloader 加载机制,把修复后的 dex 文件优先加载。具体步骤如下:
- 获取当前应用 PathClassLoader。
- 反射获取 DexPathList 属性的 pathList。
- 反射修改 pathList 的 dexElements:
- 把补丁包 patch.dex 转化为 Element[],通过 makePathElements 进行转换(path)。
- 获取 pathList 的 dexElements 属性。
- 把 patch.dex 加入到原本 dexElements 的第一位,再赋值给 dexElements。
ClassLoader 加载方式
ClassLoader 内部有数组【Element】,按序加载,加载到所需要的 class 就不会往后执行,热修复就是把补丁 dex 文件加载到数组的前面,保证优先加载。
DexClassLoader 和 PathClassLoader 的区别
- DexClassLoader 多一个 odex 文件生成地址(optimizedDirectory)。
- PathClassLoader 地址固定生成到 /data/data/包名 下。
Android 8.0 开始,DexClassLoader 的 optimizedDirectory 参数被弃用,都会使用固定地址。
- Android framework 的 dex 文件由 BootClassLoader 加载。
- Android 应用层类加载器 由 PathClassLoader 加载。
- 额外的 dex 文件 由 DexClassLoader 加载。
双亲委托机制(类似装饰器模式)
优点:
- 安全:防止核心 API 被修改(核心库还是由 BootClassLoader 加载)。
- 避免重复加载:当父类加载过,就不用重复加载。
classloader 加载机制,当调用 loadClass 方法会先查找内存中是否有加载缓存,没有的话,使用 parent.loadclass 加载,当都没有加载到的时候,自己本身才会加载。
加载过程:
- ClassLoader.loadClass
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 查看是否加载过这个class
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//双亲委托 先通过parentclassloader加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
//没有的话,自身加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
- BaseDexClassLoader.findClass
this.pathList = new DexPathList(this, librarySearchPath);
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// ....
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//实际调用的是pathList.findClass
Class c = pathList.findClass(name, suppressedExceptions);
if (c != null) {
return c;
}
//...
return c;
}
- DexPathList.findClass:
- DexPathList 内部有 Element[] dexElements,findClass 就是遍历这个数组,Element.DexFile(DexFile 是由 native 层加载 dex 文件生成的类,包含 dex 文件相关信息)就能获取到对应的 dex 文件,再由 DexFile.loadClassBinaryName 查找对应类。
//DexPathList构造
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
//...
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// 通过makeDexElements加载dex文件为Elements[]shuju
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
//...
}
//存储dexfile
private Element[] dexElements;
public Class<?> findClass(String name, List<Throwable> suppressed) {
//便利Element取出DexFile
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
- Element.findClass
public Class<?> findClass(String name, ClassLoader definingContext,List<Throwable>suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed): null;
}
热修复就是把自己的 dex 加入到 DexFile.Element 数组中,并保证加载在 olddex 前面。