热修复分为:代码修复、资源修复、动态链接修复
其中,代码修复又分为:类加载方案、底层替换方案、Instant Run 方案。
本篇关于代码修复的类加载方案的笔记整理。
涉及源码版本为 Android 7.1.1。
参考文章:
1、Android热更新实现原理浅析
2、《Android 进阶解密》
类加载方案是基于 Dex 分包方案的。
Dex 分包方案主要做的是在打包的时候将应用代码分成多个 Dex,将应用启动时必须用到的类和这些类的直接引用类放到主 Dex 中,其他代码放到次 Dex 中。当应用启动时先加载主 Dex,等到应用启动后再动态加载次 Dex。
而关于类的加载,就是遍历所有的 Dex 文件,从中去加载目标类。这里就涉及到了一个类 DexPathList
。
更进一步的,是具体的是,则是涉及到类加载器。
对于类的加载,是通过 ClassLoader
来进行的,基于双亲委托模式,会先通过具体的加载器的父加载器来加载类,如果父加载器没加载到,则会调用自身的 findClass()
方法来自行加载。
涉及到的加载器包括 DexClassLoader
和 PathClassLoader
。
DexClassLoader
可以加载 dex 文件以及包含 dex 的压缩文件(apk 和 jar 文件),而且可以加载指定路径中的 dex 文件,包括外部存储空间的。PathClassLoader
则是 Android 系统用来加载系统类和应用程序的类。通常用来加载已经安装的 apk 的 dex 文件(安装的 apk 的 dex 文件会存在 /data/dalvik-cache
中)。上述两个 ClassLoader 都继承自 BaseDexClassLoader
。
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
// 实例化成员变量 pathList
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
... // 如果没有加载到目标 Class 则会抛出异常
}
return c;
}
可以看到,通过 BaseDexClassLoader#findClass()
会进一步调用前面说到的 DexPathList
类型的 pathList.findClass()
。
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;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
在 DexPathList#findClass()
中,会遍历 dexElements
数组,该数组的元素的为 DexPathList.Element
类型,而其内部又封装了 DexFile
成员变量,该变量就对应着实际的 dex 文件,更进一步的说,加载 Class 实际上是通过 DexFile
来实现的。
因此,类加载方案的实现,就是将补丁 dex 对应的 Element
插入到应用对应的加载器的 pathList
的 dexElements
数组的靠前位置,从而使得后面同名的 Class 不被加载。
参考 Demo:HotFixDemo
补充两点:
(1)是关于 CLASS_ISPREVERIFIED
的问题(涉及到 Dalvik 虚拟机),具体参见:Android 冷启动热修复技术杂谈,因此在测试的时候要使用基于 ART 虚拟机的机型,即 Android 5.0 及以上版本。
(2)由于 Android 9.0 隐藏了部分 API,所以无法实现反射替换对应的 dexElements
数组,因此在测试的要使用 9.0 以下的手机。
关键部分代码:
public void doHotFix(Context context) throws IllegalAccessException, NoSuchFieldException, ClassNotFoundException {
if (context == null) {
return;
}
// 补丁存放目录为 /storage/emulated/0/Android/data/com.lxbnjupt.hotfixdemo/files/patch
// 注意,这里的 dexFile 是一个目录
File dexFile = context.getExternalFilesDir(DEX_DIR);
if (dexFile == null || !dexFile.exists()) {
Log.e(TAG,"热更新补丁目录不存在");
return;
}
// 得到 new DexClassLoader 时需要的存储路径
File odexFile = context.getDir(OPTIMIZE_DEX_DIR, Context.MODE_PRIVATE);
if (!odexFile.exists()) {
odexFile.mkdir();
}
// 获取 /storage/emulated/0/Android/data/com.lxbnjupt.hotfixdemo/files/patch
// 目录下的所有文件,用于找出里面的补丁 dex
File[] listFiles = dexFile.listFiles();
if (listFiles == null || listFiles.length == 0) {
return;
}
// 获取补丁 dex 文件路径集合
String dexPath = getPatchDexPath(listFiles);
String odexPath = odexFile.getAbsolutePath();
// 获取应用对应的 PathClassLoader
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
// 构建 DexClassLoader,用于加载补丁 dex
// DexClassLoader 构造方法的四个参数:
// 第一个:dex 文件相关路径集合,多个路径用文件分隔符分隔,默认文件分隔符为 :
// 第二个:解压的 dex 文件的存储路径,必须是一个内部存储路径
// 第三个:包含 C/C++ 库的路径集合,可以为 null
// 第四个:父加载器
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, odexPath, null, pathClassLoader);
// 这里要新 new 一个 DexClassLoader 的原因就是为了借助系统来构建出补丁 dex 对应
// 的 Element 元素的数组,从而插入到应用的 PathClassLoader 的
// pathList.dexElements 中
// 通过反射获取 PathClassLoader 的 Element 数组
Object pathElements = getDexElements(pathClassLoader);
// 获取构建的 DexClassLoader 的 Element 数组
Object dexElements = getDexElements(dexClassLoader);
// 合并 Element 数组
Object combineElementArray = combineElementArray(pathElements, dexElements);
// 通过反射,将合并后的 Element 数组赋值给 PathClassLoader 中 pathList 里面的
// dexElements 变量
setDexElements(pathClassLoader, combineElementArray);
}
注意,由于类加载之后是无法被卸载的,因此类加载方法需要重启 App 后让 ClassLoader 重新加载新的类。
而且重启应用之后,如果没有触发加载补丁类,则应用还是会加载原来的 BUG 类。