前言
(废话) 最近发现了一个问题, 一些平时博客写的很多的程序员, 反倒在日常的工作中, 却是业务写的很一般, 只会摆理论的人, 甚至还跑出来教别人如何找工作, 如何做架构, 其实自己都没搞明白. 但是受众的分层导致了输出者的分层, 教出清北学生的老师并不一定来自比这更好的学校, 因此, 对于博客的输出, 一个是作为对自己学习的一个记录, 非常仔细的梳理可以非常方便的让我们在需要的时候拿起来, 再就是即使这个知识现在不用, 无法深入下去, 可能会遗忘的比较快, 其细节可能会忘记, 但是核心思想我们还是有印象的, 再就是站在读者的角度上来看, 由于读者的差异性, 我们的博客在保证无误的前提下, 一定是可以帮助到很多同学的, 本着这些原则, 一周一篇的输出, 希望可以坚持下去.
(正题) 最近在做热修复的相关调研, 接着博客的专题, 可以好好的发一波文章了, 今天要分析的是 ClassLoader 方案最简单的一个 Nvwa, 本篇文章将会从 class 查找过程到 Nvwa 的实现, 以及在实现的时候解决了什么问题, 这几个方面展开, 逐步讲解.
基础使用
初始化
Nuwa.init(this);
装载补丁包
Nuwa.loadPatch(this,patchFile)
源码分析
类的查找
对于类的加载, 在通过 DexClassLoader 进行加载的时候, 通过 DexPathList 进行加载, 其中维护了一个 Element 的数组, 在查找的类的时候, 会遍历数组查找类, 如果找到则返回. 对于数组遍历查找的代码如下所示.
- public Class findClass(String name) {
- for (Element element : dexElements) {
- DexFile dex = element.dexFile;
- if (dex != null) {
- Class clazz = dex.loadClassBinaryName(name, definingContext);
- if (clazz != null) {
- return clazz;
- }
- }
- }
- return null;
- }
Nvwa 的初始化
- public static void init(Context context) {
- File dexDir = new File(context.getFilesDir(), DEX_DIR);
- dexDir.mkdir();
- String dexPath = null;
- try {
- // 拷贝 Asset 目录下的 Hack.apk 到指定路径
- dexPath = AssetUtils.copyAsset(context, HACK_DEX, dexDir);
- } catch (IOException e) {
- e.printStackTrace();
- }
- // 从拷贝后的指定路径加载 apk
- loadPatch(context, dexPath);
- }
在 nvwaw 的 init 方法中进行的操作是将 asset 中的一个 hack.apk 拷贝出来, 然后将其作为补丁进行装载.
补丁的加载
- public static void loadPatch(Context context, String dexPath) {
- if (context == null) {
- return;
- }
- if (!new File(dexPath).exists()) {
- return;
- }
- File dexOptDir = new File(context.getFilesDir(), DEX_OPT_DIR);
- dexOptDir.mkdir();
- try {
- DexUtils.injectDexAtFirst(dexPath, dexOptDir.getAbsolutePath());
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
由上面代码, 可以看出核心的实现在对 DexUtils 的 injectDexAtFirst 调用上.
- public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
- DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());
- Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
- Object newDexElements = getDexElements(getPathList(dexClassLoader));
- Object allDexElements = combineArray(newDexElements, baseDexElements);
- Object pathList = getPathList(getPathClassLoader());
- ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements);
- }
将两个 Dex 进行合并, 将补丁 dex 塞在数组的前面, 然后通过反射的方式设置进去, 通过这种方式, 根据上面的类加载逻辑, 可以知道, 对于类的加载是从数组的最开始的位置进行查找加载的, 当前面的 dex 查找到相应的类之后, 就会停止后面的查找, 这样, 我们通过补丁的替换的类就会生效.
- private static Object combineArray(Object firstArray, Object secondArray) {
- Class<?> localClass = firstArray.getClass().getComponentType();
- int firstArrayLength = Array.getLength(firstArray);
- int allLength = firstArrayLength + Array.getLength(secondArray);
- Object result = Array.newInstance(localClass, allLength);
- for (int k = 0; k < allLength; ++k) {
- if (k < firstArrayLength) {
- Array.set(result, k, Array.get(firstArray, k));
- } else {
- Array.set(result, k, Array.get(secondArray, k - firstArrayLength));
- }
- }
- return result;
- }
存在问题
通过上述的方式, 我们将差异补丁单独打一个包, 然后进行下发, 从而使得我们的类得到修复. 但是在这样实现的时候, 存在一个问题, 就是 在 Dalvik 虚拟机对于 dex 的一个优化. Dalvik 虚拟机在启动的时候, 会有许多的启动参数, 其中有一项就是 verify, 当 verify 被打开的时候, doVerify 变量为 true, 则进行类的校验 (dvmVerifyClass 方法调用). 若校验成功, 则这个类会被打上标记: CLASS_ISPREVERIFIED.
这么做, 是防止外部 DEX 注入的一个安全方案, 即保证运行期的 Class 与其直接引用类之间所在的 DEX 关系要与安装时候一致, 也是为了防止类被篡改校验类的合法性. Dalvik 虚拟机在安装期间, 为 Class 打上 CLASS_ISPREVERIFIED 是为了提高性能, 下次使用时, 则会省去校验操作, 提高访问效率. dvm 在运行期载入 Class 时候, 会对其内存中对应的直接引用类进行校验, 如果该类存在与直接引用类所在的 dex 不是同一个, 则直接报 "pre-verification" 错误, 该类无法加载.
由于这一个限制, 导致我们的补丁包无法在被调用到的时候, 就会抛出异常, 因此我们需要让我们的补丁包, 如何通过这次校验, 不被打上 CLASS_ISPREVERIFIED, 这样, 我们的补丁包在被加载的时候, 就不会抛出异常了.
nvwa 采取的方式就是插桩的方式, 在每一个类里去引用到另一个独立 dex 中的类, 也会是在初始化的时候加载的 hack.apk 中的 Hack.class, 通过这种方式, 可以让我们的类不会被打上这个标签. 这样就可以继续装载其它 Dex 中的类.
插桩存在一个什么问题呢? 由于没有打上验证标签, 导致每个类的装载的时候都进行验证.
微信在对插装和不插桩做的测试中. 在连续加载 700 个 50 行的类, 还有统计应用启动耗时得到的数据, 700 个类: 不插桩: 84ms, 插桩: 685ms. 启动耗时: 4934ms,7240ms.
结语
每周一更, 由于最近业务需求较多, 更新速度明显慢了很多了, 因此本篇分析的也是一很简单的框架. 接下来, 将会逐步深入, 分析一些更为复杂的热修复方案框架.
参考资料
Dalvik 中 PreVerify 问题 https://fenglincanyi.github.io/2016/11/24/Dalvik中PreVerify问题/
来源: https://juejin.im/post/5ad88a56f265da0b7155ceab