本文共 4449 字,大约阅读时间需要 14 分钟。
热修复分为:代码修复、资源修复、动态链接修复
其中,代码修复又分为:类加载方案、底层替换方案、Instant Run 方案。
本篇关于代码修复的类加载方案的笔记整理。
涉及源码版本为 Android 7.1.1。
参考文章:
1、 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);}@Overrideprotected Class findClass(String name) throws ClassNotFoundException { ListsuppressedExceptions = new ArrayList (); Class c = pathList.findClass(name, suppressedExceptions); if (c == null) { ... // 如果没有加载到目标 Class 则会抛出异常 } return c;}
可以看到,通过 BaseDexClassLoader#findClass()
会进一步调用前面说到的 DexPathList
类型的 pathList.findClass()
。
public Class findClass(String name, Listsuppressed) { 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:
补充两点:
(1)是关于 CLASS_ISPREVERIFIED
的问题(涉及到 Dalvik 虚拟机),具体参见:,因此在测试的时候要使用基于 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 类。
转载地址:http://crerj.baihongyu.com/