跳到主要内容

设计热更新方案

问题

如何设计一个 Android 热更新(热修复)方案?

答案

主流方案对比

方案原理粒度时效性代表框架
类加载替换修改 DexPathList,优先加载 patch.dex类级别重启生效Tinker、QZone
底层替换替换 ArtMethod 指针方法级别即时生效AndFix、Sophix
资源修复替换 AssetManager 的资源路径资源文件重启生效Tinker
.so 修复修改 System.loadLibrary 加载路径so 文件重启生效Tinker

类加载方案(Tinker 原理)

核心原理:Android 的 PathClassLoaderdexElements 数组顺序查找类。将修复后的 dex 插到数组前面,就能"覆盖"旧类:

// 简化版 DexPathList 注入
fun installPatchDex(context: Context, patchDexFile: File) {
val classLoader = context.classLoader
// 反射获取 pathList 字段
val pathListField = classLoader.javaClass.superclass!!
.getDeclaredField("pathList").apply { isAccessible = true }
val pathList = pathListField.get(classLoader)

// 反射获取 dexElements 数组
val dexElementsField = pathList.javaClass
.getDeclaredField("dexElements").apply { isAccessible = true }
val oldElements = dexElementsField.get(pathList) as Array<*>

// 把 patch dex 构造为 Element 并插到数组最前面
val patchElement = makeDexElement(patchDexFile)
val newElements = arrayOf(patchElement) + oldElements
dexElementsField.set(pathList, newElements)
}

安全校验

安全须知

热更新包必须做完整的安全校验,防止被篡改植入恶意代码:

  1. 签名校验 — 服务端用私钥签名 patch,客户端用公钥验签
  2. 完整性校验 — MD5/SHA256 校验文件完整性
  3. 版本校验 — patch 版本必须匹配当前 App 版本
  4. 回滚机制 — 加载失败自动删除 patch 并上报

设计要点

要点方案
差量包BSdiff 生成差分包,客户端合成
灰度按设备 ID hash 分桶,逐步放量
回滚patch 加载异常时自动清除
多补丁支持叠加,按版本号顺序应用
监控统计补丁下载率、应用成功率、崩溃率
Google Play 限制

Google Play 政策禁止通过非 Play 渠道更新可执行代码(dex、.so)。此方案主要用于国内应用市场分发的 App。


常见面试问题

Q1: Tinker 和 Sophix 的区别?

答案

  • Tinker(类加载方案):通过 dex 差分 + 合成,将修复后的完整 dex 插入 ClassLoader。优点是稳定可靠,缺点是需要重启才生效
  • Sophix(混合方案):方法级别的修复用底层 ArtMethod 替换可即时生效;类结构变更则回退到类加载方案需重启。兼顾了时效性和兼容性

Q2: 为什么类加载方案需要重启才能生效?

答案

因为一个类一旦被 ClassLoader 加载到内存,就会被缓存。即使修改了 dexElements 的查找顺序,已经加载过的类不会重新加载。只有重启进程后,所有类重新经过 ClassLoader 查找时,才会优先命中插入在前面的 patch dex 中的修复类。

相关链接