热修复之类加载方案 笔记整理

热修复分为:代码修复、资源修复、动态链接修复

其中,代码修复又分为:类加载方案、底层替换方案、Instant Run 方案。

本篇关于代码修复的类加载方案的笔记整理。

涉及源码版本为 Android 7.1.1。

参考文章:
1、Android热更新实现原理浅析
2、《Android 进阶解密》


1、理论基础

类加载方案是基于 Dex 分包方案的。

Dex 分包方案主要做的是在打包的时候将应用代码分成多个 Dex,将应用启动时必须用到的类和这些类的直接引用类放到主 Dex 中,其他代码放到次 Dex 中。当应用启动时先加载主 Dex,等到应用启动后再动态加载次 Dex。

而关于类的加载,就是遍历所有的 Dex 文件,从中去加载目标类。这里就涉及到了一个类 DexPathList

更进一步的,是具体的是,则是涉及到类加载器。

对于类的加载,是通过 ClassLoader 来进行的,基于双亲委托模式,会先通过具体的加载器的父加载器来加载类,如果父加载器没加载到,则会调用自身的 findClass() 方法来自行加载。

涉及到的加载器包括 DexClassLoaderPathClassLoader

  1. DexClassLoader 可以加载 dex 文件以及包含 dex 的压缩文件(apk 和 jar 文件),而且可以加载指定路径中的 dex 文件,包括外部存储空间的。
  2. 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 插入到应用对应的加载器的 pathListdexElements 数组的靠前位置,从而使得后面同名的 Class 不被加载。

2、具体实现

参考 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 类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值