Android Hotfix—类替换计划
Hotfix旨在经过布置较小的补丁来快速修正bug,无需从头构建和发布整个使用程序,避免了从头发布整个使用程序触及的时刻和资源耗费。
本文将以ClassLoader为切入点,来讨论Hotfix类替换计划。
1. ClassLoader简介
ClassLoader是担任加载类的组件,它经过查找和加载类的字节码文件,然后将其转化为Class目标。
我们的使用程序是由多个ClassLoader相互配合加载的,它们之间存在的层次联系称为双亲派遣
:当一个类加载器收到加载类的恳求时,它首先会将该恳求派遣给其父类加载器,只要在父类加载器无法加载该类时,才会测验自己加载。
Android中的ClassLoader层次结构如下:
以下是几个比较重要的ClassLoader
-
BootClassLoader:BootClassLoader是坐落类加载器层次结构最顶层,担任加载Android体系核心类库,如java.lang包中的类。
-
PathClassLoader:Android使用程序的默许类加载器,担任加载当时使用的类和资源。
-
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计划:
- 刺进parent ClassLoader
- Hook 当时ClassLoader#findClass办法
- 刺进child ClassLoader:优先findClass,而后调用parent.loadClass
2.1 刺进parent ClassLoader
此计划需求反射刺进parent ClassLoader,依据ClassLoader双亲派遣机制,Hotfix ClassLoader
类加载优先级高于PathClassLoader
,然后到达hotfix的意图,但有必要处理以下问题:
-
Fixed Class中引证原apk中的类时ClassNotFoundException:双亲派遣加载某个类的时候,会从caller ClassLoader开始查找,即Hotfix ClassLoader,在Bootstrap ClassLoader和Hotfix ClassLoader均找不到该类,所以就报错了。为此需求Hotfix ClassLoader打破双亲派遣,自身忽略非补丁类,强制运用它的child ClassLoader加载。
-
IllegalAccessException: Patch、原APK中
package-private
的类,即便包名相同也无法被对方引证,由于Android运行时检测IsInSamePackage
,只要当两个类的ClassLoader和包名均相一起才允许拜访。为了处理此问题,需求扩展补丁类的规模
- changed classes作为最初的补丁调集
- 将补丁类引证原apk中的package-private的类添加到补丁中,构成新的补丁调集
- 重复进行第2步,直到补丁调集不变
- 把补丁类可见性悉数设置成public:便于原apk拜访补丁类
代表结构:暂未找到,从头写了一个github.com/xiaowei-lei… , 欢迎交流。
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。
dexElements
虽然对应了apk中的dex文件,但依据apk构建dexElements
的过程、类都不是public的,这意味着要想构建patch.dex
的Class实例,需求屡次运用反射,但是每个android版别的完成不尽相同,又有不同的厂商定制,维护成本不小。
代表结构:Qzone、Nuwa
2.3 刺进child ClassLoader
由于PathClassLoader是体系类,它遵从了双亲派遣,因而运行时原apk类不能反向依靠补丁类,这意味着还需求扩展补丁类的规模:
- changed classes作为最初的补丁调集
- 将补丁类引证原apk中的package-private的类添加到补丁中
- 将原APK中直接/直接依靠补丁类的类添加到补丁中
- 重复执行步骤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编译器或许会对热修正造成影响,由于它或许会缓存原始类的机器码,而不会从头编译修正后的类。
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 ClassLoader
、Hook PathClassLoader#findClass
这两种计划有影响,关于在AOT/JIT编译优化名单里边的类,findLoadedClass(className)直接就返回了,因而它们失去了优先加载修正代码的机遇,但还有解救的办法,由于ClassLoader实例之间ClassTable是阻隔的,能够从头构建一个PathClassLoader实例替换掉本来的实例,但这样就简直退化成Dalvik了。
4. 总结
本文从ClassLoader视点简单介绍了几种Hotfix类替换计划,以及ART编译优化对其的影响。
类替换Hotfix原理都是比较简单的,但完成过程中却需求处理如兼容性、proguard/R8优化、自动化补丁等,导致复杂度变高,概况参阅干流Hotfix结构源码。