Android Hotfix—类替换方案

Android Hotfix—类替换方案

Hotfix旨在通过部署较小的补丁来快速修复bug,无需重新构建和发布整个应用程序,避免了重新发布整个应用程序涉及的时间和资源消耗。

本文将以ClassLoader为切入点,来探讨Hotfix类替换方案。

1. ClassLoader简介

ClassLoader是负责加载类的组件,它通过查找和加载类的字节码文件,然后将其转化为Class对象。

我们的应用程序是由多个ClassLoader相互配合加载的,它们之间存在的层次关系称为双亲委派:当一个类加载器收到加载类的请求时,它首先会将该请求委派给其父类加载器,只有在父类加载器无法加载该类时,才会尝试自己加载。

Android中的ClassLoader层次结构如下:

image.png

以下是几个比较重要的ClassLoader

  1. BootClassLoader:BootClassLoader是位于类加载器层次结构最顶层,负责加载Android系统核心类库,如java.lang包中的类。

  2. PathClassLoader:Android应用程序的默认类加载器,负责加载当前应用的类和资源。

  3. DexClassLoader:Android提供的类加载器,可以加载外部DEX/APK文件,为Android应用程序提供了更大的灵活性和可扩展性,常用于热修复/插件化。

2. 利用ClassLoader实现Hotfix

为了修复bug,需要干预ClassLoader类加载过程,使其运行时优先加载修复代码,从而实现Hotfix。

先看下ClassLoader#loadClass的代码片段

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);
        if (clazz == null) {
            try {
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                // Don't want to see this.
            }
            if (clazz == null) {
                clazz = findClass(className);
            }
        }
        return clazz;
    }

可以干预类加载的几个方法,从而形成不同的hotfx方案:

  1. 插入parent ClassLoader
  2. Hook 当前ClassLoader#findClass方法
  3. 插入child ClassLoader:优先findClass,而后调用parent.loadClass

2.1 插入parent ClassLoader

image.png

此方案需要反射插入parent ClassLoader,基于ClassLoader双亲委派机制,Hotfix ClassLoader类加载优先级高于PathClassLoader,从而达到hotfix的目的,但必须处理以下问题:

  1. Fixed Class中引用原apk中的类时ClassNotFoundException:双亲委派加载某个类的时候,会从caller ClassLoader开始查找,即Hotfix ClassLoader,在Bootstrap ClassLoader和Hotfix ClassLoader均找不到该类,所以就报错了。为此需要Hotfix ClassLoader打破双亲委派,自身忽略非补丁类,强制使用它的child ClassLoader加载。

  2. IllegalAccessException: Patch、原APK中package-private的类,即使包名相同也无法被对方引用,因为Android运行时检测IsInSamePackage,只有当两个类的ClassLoader和包名均相同时才允许访问。

    为了处理此问题,需要扩大补丁类的范围

    1. changed classes作为最初的补丁集合
    2. 将补丁类引用原apk中的package-private的类添加到补丁中,形成新的补丁集合
    3. 反复进行第1、2步,直到补丁集合不变
    4. 把补丁类可见性全部设置成public:便于原apk访问补丁类

2.2 Hook PathClassLoader#findClass

一个apk中可能包含多个dex文件,主dex命名为 "classes.dex",而其他dex则按照以下格式命名:

  • classes2.dex
  • classes3.dex
  • ...
  • classesN.dex

当存在多个DEX文件时,PathClassLoader会根据dex文件的加载顺序来决定类的优先级,如果多个dex中有相同的类,会优先加载左侧dex中的类。

利用这个特性,需要将patch.dex插入到dexElements最前面即可实现hotfix。 image-20230712200734366.png

dexElements虽然对应了apk中的dex文件,但根据apk构建dexElements的过程、类都不是public的,这意味着要想构建patch.dex的Class实例,需要多次使用反射,然而每个android版本的实现不尽相同,又有不同的厂商定制,维护成本不小。

代表框架:Qzone、Nuwa

2.3 插入child ClassLoader

image.png

因为PathClassLoader是系统类,它遵从了双亲委派,因此运行时原apk类不能反向依赖补丁类,这意味着还需要扩大补丁类的范围:

  1. changed classes作为最初的补丁集合A
  2. 将补丁类引用原apk中的package-private的类添加到补丁中
  3. 将原APK中直接/间接依赖补丁类的类添加到补丁中
  4. 反复执行步骤2、3直到补丁集合不变。

可见,补丁的范围可能远远大于changed classes;另外需要找到所有持有PathClassLoader的地方,反射替换为Hotfix ClassLoader

代表框架:Tinker

3. ART编译优化对热修复的影响

3.1 预先 (AOT) 编译

Android Runtime (ART) 使用AOT编译技术在应用安装时将字节码转换为机器码。

这可能导致在运行时无法动态替换已编译的类。

3.2 即时 (JIT) 编译器

Android Runtime (ART) 包含一个具备代码分析功能的即时 (JIT) 编译器,该编译器可以在 Android 应用运行时持续提高其性能。JIT 编译器对 Android 运行组件当前的预先 (AOT) 编译器进行了补充,可以提升运行时性能,节省存储空间,加快应用和系统更新速度。相较于 AOT 编译器,JIT 编译器的优势也更为明显,因为在应用自动更新期间或在无线下载 (OTA) 更新期间重新编译应用时,它不会拖慢系统速度。

jit-arch.png

JIT编译器可能会对热修复造成影响,因为它可能会缓存原始类的机器码,而不会重新编译修复后的类。

3.3 对热修复的影响

AOT/JIT将一部分代码优化成机器码,在apk启动时会把优化后的类添加到ClassTable中。

在类加载时,使用时ClassLinker::LookupClass会先从ClassTable中去查找,找不到时才会走到DefineClass中。

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className); // Here
        if (clazz == null) {
            try {
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                // Don't want to see this.
            }
            if (clazz == null) {
                clazz = findClass(className);
            }
        }
        return clazz;
    }

显然对于上述插入parent ClassLoaderHook PathClassLoader#findClass这两种方案有影响,对于在AOT/JIT编译优化名单里面的类,findLoadedClass(className)直接就返回了,因此它们失去了优先加载修复代码的时机,但还有拯救的办法,由于ClassLoader实例之间ClassTable是隔离的,可以重新构建一个PathClassLoader实例替换掉原来的实例,但这样就几乎退化成Dalvik了。

4. 总结

本文从ClassLoader角度简单介绍了几种Hotfix类替换方案,以及ART编译优化对其的影响。

类替换Hotfix原理都是比较简单的,但实现过程中却需要处理如兼容性、proguard/R8优化、自动化补丁等,导致复杂度变高,详情参考主流Hotfix框架源码。

全部评论

相关推荐

05-09 14:45
门头沟学院 Java
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务