动态数组的奥秘:Android 热修复 Hotfix 中 combineArray 与 appendArray 实现详解
【免费下载链接】HotfixThe Hotfix tool can dynamically fix online bugs for Android without republishing an app.项目地址: https://gitcode.com/gh_mirrors/hot/Hotfix
Hotfix 是一款 Android 热修复工具,无需重新发布应用,就能动态修复线上 Bug。本文带你深入拆解它源码中combineArray与appendArray两个动态数组方法的实现原理,看懂热修复是如何"插入"补丁代码的。
一、什么是 Android 热修复?
应用上线后出现 Bug,等一次应用商店审核重新发版可能需要几天。而 Android 热修复的思路是:
- 服务器下发一个补丁 dex 文件
- 应用运行时通过
DexClassLoader加载它 - 把补丁的 dex 信息"合并"进应用原有的类加载器中
这样应用加载类时就能先找到"打过补丁"的版本,实现不重新发布应用即可修复线上问题。项目入口 HotfixApp.kt 中,Application.onCreate()阶段就调用了补丁加载:
HotFix.patch(applicationContext, "", "")核心逻辑全部集中在 Hotfix.kt 这一个文件里。
二、先理清整体流程:两条注入路径
HotFix.patch()会根据系统版本走不同分支:
| 环境 | 方法 | 操作对象 |
|---|---|---|
| API 14 及以上 | injectAboveEqualApiLevel14 | pathList.dexElements数组 |
| 早期系统 | injectBelowApiLevel14 | mPaths/mFiles/mZips/mDexs私有字段 |
两条路径的最终目的完全一致:把补丁 dex 的元素合并进类加载器内部的数组,并放在最前面。
而数组合并这件事,就靠combineArray和appendArray两个小方法完成。
三、combineArray:把两个数组合成一个
先看 API 14+ 分支中的关键调用(源码约 108 行):
val a = combineArray( getDexElements(getPathList(pathClassLoader)), // 应用原有 dex 元素 getDexElements(getPathList(补丁DexClassLoader)) // 补丁 dex 元素 ) setField(a2, a2.javaClass, "dexElements", a)再看combineArray的实现(第 154~167 行):
private fun combineArray(obj: Any, obj2: Any): Any { val componentType = obj2.javaClass.componentType val length = Array.getLength(obj2) val newInstance = Array.newInstance(componentType, Array.getLength(obj) + length) for (i in 0 until length2) { if (i < length) { Array.set(newInstance, i, Array.get(obj2, i)) // 前半段:obj2 } else { Array.set(newInstance, i, Array.get(obj, i - length)) // 后半段:obj } } return newInstance }🔍 几个值得注意的细节:
- 不依赖具体类型:它通过
java.lang.reflect.Array操作任意数组(String[]、File[]、DexPathList.Element[]都可以),这正是热修复代码能适配不同系统版本的秘诀。 - 顺序是反直觉的:第二个参数
obj2(补丁元素)被放在数组前面,原有元素obj排在后面。 - 新建数组而非修改原数组:因为原始 dexElements 数组通常是
final或内部持有引用,直接改写会有风险,新建一个数组再整体替换回字段更安全。
四、appendArray:在数组开头"插队"
在旧系统分支(injectBelowApiLevel14)中,PathClassLoader的mPaths字段是一个String[],存放 dex 路径。要把补丁路径加进去,代码调用了appendArray(第 169~178 行):
private fun appendArray(src: Any, dest: Any): Any { val componentType = src.javaClass.componentType val length = Array.getLength(src) val newInstance = Array.newInstance(componentType, length + 1) Array.set(newInstance, 0, dest) // 新元素放第 0 位 for (i in 1 until length + 1) { Array.set(newInstance, i, Array.get(src, i - 1)) } return newInstance }名字虽叫 "append"(追加),实际行为却是头插:新元素dest放在第 0 位,原有元素整体后移一位。这和combineArray的设计取向完全一致——
⭐补丁必须排在数组最前面,类加载器查类时才能先命中补丁版本。
五、为什么要"头插"?加载顺序 = 优先级
Android 的类加载器在dexElements数组中是从头往后逐个查找类的。假设某类在"补丁 dex"和"原包 dex"中都存在:
- 补丁在前 → 加载补丁版本 → Bug 被修复 ✅
- 补丁在后 → 加载原包版本 → 修复无效 ❌
所以combineArray和appendArray看似只是简单的数组操作,实际上它们承担了热修复方案中最关键的一步:控制类的加载优先级。
六、两个反射小工具:getField / setField
数组操作能作用于私有字段,靠的是两个反射辅助方法(第 141~152 行):
getField:getDeclaredField拿到私有字段后先setAccessible(true),再读出值setField:同理,把新数组整体写回私有字段
这两个方法被combineArray、appendArray反复组合使用,构成了整个注入流程的"底座"。
七、总结
| 方法 | 作用 | 使用场景 |
|---|---|---|
combineArray | 两个数组合并,第二个参数排前 | 合并dexElements、mFiles、mZips、mDexs |
appendArray | 单元素头插 | 向mPaths字符串数组加入补丁路径 |
一句话记住它们的奥秘:反射操作任意数组 + 补丁元素永远排在最前。理解了这一点,你就读懂了 Hotfix 热修复工具的核心原理——无需重新发版,只靠"往类加载器的数组头部插入一个补丁",就能让线上应用自我修复。🚀
相关文件参考:
- 核心实现:Hotfix.kt
- 补丁加载入口:HotfixApp.kt
【免费下载链接】HotfixThe Hotfix tool can dynamically fix online bugs for Android without republishing an app.项目地址: https://gitcode.com/gh_mirrors/hot/Hotfix
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考