一、热修复的本质问题
热修复(Hot Fix / Hot Patch)的核心目标是:在用户不重新安装 APK 的情况下修复线上 Bug。这在移动应用发布中有巨大的商业价值——避免因为一个崩溃导致用户流失,也避免频繁发版带来的审核等待(特别是 iOS 端,Android 端虽然可以直接发版但用户更新率低)。
Android 热修复的发展历程:
2014-2015: 萌芽期 |
Android 热修复需要解决的根本问题是:如何让新代码在旧代码之前被执行? 在 Java/Android 中,”代码”的表现形式是 DEX 文件中的 Class,而 Class 的加载由 ClassLoader 完成。因此热修复的本质是操控 ClassLoader 的类加载顺序,让修复包中的类优先于原始 APK 中的类被加载。
1.1 热修复的四大技术路线
热修复方案分类: |
二、Android 类加载机制
在深入热修复方案之前,必须理解 Android 的类加载体系。
2.1 DexPathList 与双亲委派
Android 的 ClassLoader 是 BaseDexClassLoader(API 26+)或 DexClassLoader/PathClassLoader。核心逻辑委托给了 DexPathList:
// AOSP: libcore/dalvik/src/main/java/dalvik/system/DexPathList.java |
关键的发现:DexPathList 遍历 dexElements 数组,返回第一个匹配的 Class。如果我们将修复的 DEX 文件插入到 dexElements 数组的最前面,那么 findClass 就会先找到修复后的类。
2.2 ClassLoader 的双亲委派模型
// AOSP: libcore/libart/src/main/java/java/lang/ClassLoader.java |
Android 中的 ClassLoader 层级:
BootClassLoader(加载 Framework 类,如 Activity、String) |
2.3 编译和加载流程
APK 构建 应用启动 |
了解这个流程后,热修复的核心就很清晰了:在 dexElements 数组中找到合适的位置插入修复的 DEX。
三、ClassLoader 方案:QQ 空间超级补丁
这是最早的开源方案之一。核心原理非常简单:反射修改 PathClassLoader 的 parent 字段,将修复 DEX 的 DexClassLoader 插入到类加载链中。
3.1 实现步骤
// 1. 获取 PathClassLoader 的 DexPathList |
3.2 QQ 空间方案的缺陷
CLASS_ISPREVERIFIED 问题:Dalvik 运行时(Android 4.4 以前),如果一个类在其 DEX 之外引用了其他类,且被引用者在同一个 DEX 中被打上
CLASS_ISPREVERIFIED标记,则运行时不会再次校验。但如果热修复修改了这个类,就会出现方法签名验证失败的问题。解决方案是在原始 APK 编译时插入一个”防校验”类,破坏CLASS_ISPREVERIFIED标记。ART 运行时已没有此问题。Android Nougat+ 混合编译:Android 7.0 引入了混合编译模式(JIT + AOT),
DexPathList的实现有变化,需要对不同的 API 级别做兼容处理。
3.3 CLASS_ISPREVERIFIED 问题详解
这是 Dalvik 特有的问题,深入理解有助于理解 Android 运行时演进:
// 编译期 DEX 优化时,Dalvik 会做类校验 |
四、Tinker 方案:全量 DEX 替换
腾讯开源的 Tinker(https://github.com/Tencent/tinker)是目前最成熟的热修复方案之一,已被微信团队大规模验证。
4.1 Tinker 的核心思路
Tinker 不插入单个修复 DEX,而是生成完整的修复后的 DEX 文件,替换掉原始 DEX。通过 BSDiff 算法生成差分包(patch),客户端下载差分包后与原始 DEX 合成新的 DEX。
4.2 Tinker 架构全景
开发阶段 客户端 |
4.3 BSDiff 算法与 BSPatch
BSDiff 是一个二进制差分算法,源自 Google 的 Chromium 项目。核心思想是:
新文件 = 旧文件 + 差分信息(新增/删除/修改的字节块) |
差分信息包括三部分:
- diff string:新旧文件中不同的连续字节。
- extra string:新文件中独有的连续字节。
- control tuples:三元组 (diff_pos, extra_pos, copy_len),控制合成过程。
BSPatch 合成算法伪代码:
// external/bsdiff/bspatch.c |
Tinker 的补丁生成流程:
原始 APK (old.apk) ──→ bsdiff ──→ patch.patch |
4.4 Tinker 的类加载机制
Tinker 使用自定义的 TinkerClassLoader,它替代了系统的 PathClassLoader。其 dexElements 中:
- 最前面:修复后的 DEX(由 bspatch 合成)
- 后面:原始 DEX
这样确保新类优先被加载。Tinker 同时支持 Application 类、Library、Resource 的热修复。
Tinker 的 Application 热修复流程:
1. Application 启动 |
4.5 Tinker 的资源修复
Tinker 通过替换 AssetManager 和 Resources 来实现资源热修复:
// TinkerResourcesManager |
4.6 Tinker 的限制
- 必须重启应用:新的 DEX 文件需要重建 ClassLoader 才能生效。
- 不支持新增四大组件:Activity、Service、BroadcastReceiver、ContentProvider 的注册信息在 AndroidManifest.xml 中,无法通过 DEX 替换更改。
- OAT 兼容性:不同 ROM 对 dex2oat 的处理不同,某些机型上 oat 文件缓存可能导致类加载异常。
五、Sophix 方案:Native ART Method 替换
阿里开源的 Sophix(https://github.com/alibaba/Sophix)采用了更底层的方案——直接操作 ART 运行时的方法结构体。
5.1 ART Method 结构
在 ART 运行时中,每个 Java 方法在 Native 层对应一个 ArtMethod 结构体:
// AOSP: art/runtime/art_method.h |
5.2 Method Replacement 的核心原理
Sophix 的 native 层直接将新方法的 entry_point_from_quick_compiled_code_ 替换为旧方法的对应字段,或者在解释模式下替换方法的 DEX 指令指针:
// Sophix 核心逻辑(简化) |
5.3 Sophix vs AndFix
AndFix 是 Sophix 的前身,只支持 ARM 架构(32 位),不支持 x86、ARM64。Sophix 解决了这些问题:
| 维度 | AndFix | Sophix |
|---|---|---|
| 架构支持 | ARM only (32bit) | ARM, ARM64, x86, x86_64 |
| ART 兼容 | 5.0-7.0 (有限) | 5.0+ (全面) |
| 字段拷贝 | 部分字段 | 全字段(更安全) |
| JIT 缓存 | 不处理 | 主动清除 |
| 方法替换粒度 | 单个方法 | 单个方法 + 类级 |
| 即时性 | 是 | 是 |
5.4 Sophix 的三层修复架构
Sophix 方案被设计为一个融合了三种修复粒度的方案:
┌─────────────────────────────────────┐ |
六、Robust 方案:Instant Run 插桩
6.1 原理
美团开源的 Robust(https://github.com/Meituan-Dianping/Robust)基于 Instant Run 的插桩技术。它在编译期给每个类注入一个 ChangeQuickRedirect 接口的引用,运行时每个方法先检查是否有修复逻辑:
// 原始代码 |
6.2 优劣势
优点:
- 即时生效:不需要重启应用
- 兼容性好:不依赖反射或 Native Hook
- 可修复新增字段/方法:因为修复的类在独立 DEX 中
缺点:
- 性能损耗:每个方法调用前都有额外的 if 判断和接口调用
- 包体积增加:插桩注入的代码增加了 DEX 体积
- 不支持 lambda:lambda 表达式的方法无法被插桩
- 混淆兼容性:需要特殊的 proguard 配置
七、Sophix vs Tinker vs QQ 空间 vs Robust
| 方案 | 原理 | 是否需重启 | Android 版本兼容 | 新增方法 / 字段 | 性能影响 | 成熟度 |
|---|---|---|---|---|---|---|
| QQ 空间 | ClassLoader + dexElements 合并 | 是 | 4.0+ | 否 | 无 (仅 ClassLoader) | Demo 级 |
| Tinker | BSDiff + 全量 DEX 替换 | 是(默认) | 4.0+ | 是(全量替换) | 无 | 微信验证,非常成熟 |
| AndFix | Native ArtMethod 替换 | 否 | 4.4-7.0 (ARM) | 否 | 无 | 中等 |
| Sophix | Native Hook + Method Replace + Resource | 可免重启(即时效) | 4.4+ | 部分(即时方案限制) | 无 | 阿里商业验证 |
| Robust | Instant Run 插桩 | 否 | 4.0+ | 是 | 有(每个方法都有 if 判断) | 美团验证 |
八、Android 各版本对热修复的影响
8.1 Android 7.0 (API 24) 的变化
Android 7.0 将 JIT 编译器与 AOT 编译器混合使用:
- 初始安装时不再全量 AOT 编译(dex2oat 只做 verify)。
- 应用中频繁调用的”热点”方法由 JIT 编译。
- 设备空闲时,由
dex2oat守护进程在后台做 AOT 编译。
这对热修复的影响:
- 存在 oat 文件缓存,即使替换了 DEX,系统可能仍使用旧的 oat。
- Tinker 方案需要清除 oat 文件缓存,确保新的 DEX 被重新编译。
8.2 Android 9.0 (API 28) 的隐藏 API 限制
Android 9.0 引入了对 @hide API 和非 SDK 接口的访问限制:
- 所有反射调用
@hideAPI 都会触发警告或抛出NoSuchMethodException。 - 这对依赖反射修改 dexElements 的热修复方案(QQ 空间方案、Tinker)有重大影响。
// Android 9+ 中,这些反射访问可能被阻止 |
8.3 Android 10 (API 29) 的存储范围限制
Android 10 引入了 Scoped Storage,限制了应用对共享存储的访问。热修复补丁文件的存储路径可能受影响——应用只能从自己的私有目录或特定媒体集合中读取文件。
九、Android App Bundle 与 Google 的方案
Google 通过 Android App Bundle(AAB)和 Google Play 的 In-App Updates 提供了一套官方替代方案:
- Android App Bundle:Google Play 根据设备配置生成 Split APKs,用户只下载需要的代码和资源。
- In-App Updates:提供 Immediate 和 Flexible 两种模式,在应用内完成更新。
- Dynamic Delivery:通过 Play Feature Delivery 按需下载功能模块。
Google 并不鼓励热修复,因为热修复绕过了 Google Play 的安全审查机制,且可能引入不兼容问题。但这对于中国大陆市场(无法使用 Google Play)的应用来说,热修复仍然是最重要的基础设施之一。
十、面试常问题目
Q1: Android 类加载的双亲委派机制是什么?热修复如何利用它?
双亲委派机制:当 ClassLoader 加载一个类时,先委托给 parent ClassLoader 加载,如果 parent 没找到才自己加载。热修复通过反射将修复的 DEX 插入到 dexElements 数组最前面,因为 findClass 遍历 dexElements 时返回第一个匹配的类,修复后的类就会先于原始类被加载,从而”替换”了旧类。
Q2: Tinker 为什么需要重启才能生效?
Tinker 替换的是完整的 DEX 文件,而 DEX 文件在应用启动时被 PathClassLoader 加载到 dexElements 中。修改 dexElements 需要重新创建或修改 ClassLoader,这要求重启应用以重新初始化 Application 和 Activity。Sophix 之所以可以不重启,是因为它直接修改了 ART 运行时的 ArtMethod 结构体指针,绕过了 ClassLoader。
Q3: 热修复可以新增 Activity 吗?
ClassLoader 方案和 Tinker 方案都不能。Activity 必须在 AndroidManifest.xml 中声明,而 Manifest 在 APK 打包时就被固定了。不过可以通过”代理 Activity”模式间接实现:在 Manifest 中预注册一个代理 Activity,运行时由代理 Activity 通过反射创建目标 Activity 并转发所有生命周期方法。虚拟引擎方案(VirtualApp、RePlugin)则从根本上解决了这个问题。
Q4: CLASS_ISPREVERIFIED 问题是什么?
这是 Dalvik 虚拟机(Android 4.4 之前)特有的问题。Dalvik 在 DEX 优化时,如果一个类引用的所有外部类都在同一个 DEX 中,会给这个类打上 CLASS_ISPREVERIFIED 标记,表示”已验证”。当热修复修改了这个类的引用关系(比如调用了一个在不同 DEX 中的新方法),运行时校验会失败,抛出 IllegalAccessError。ART 运行时不再有此标记,因此 ART 以上不存在此问题。
Q5: Sophix 为什么能即时生效?它的限制是什么?
Sophix 的即时生效依赖于直接修改 ART 运行时中的 ArtMethod 结构体。当 Java 方法被调用时,ART 通过 ArtMethod 中的 entry_point_from_quick_compiled_code_(编译后的机器码入口)或 entry_point_from_interpreter_(解释器入口)来执行代码。Sophix 将这些入口指针替换为修复后的方法的入口指针,使得后续调用直接跳转到修复代码。
限制:
- 不能新增方法或字段:ArtMethod 的替换是单个方法的替换,不能改变类的结构(类的虚方法表大小在编译时就固定了)。
- 不能修改类的继承关系:ArtMethod 的
declaring_class_必须指向原类。 - 架构依赖:ArtMethod 结构体的字段布局在不同 CPU 架构(ARM、ARM64、x86)上不同,需要针对性适配。
- Android 版本依赖:ArtMethod 的字段顺序和大小在不同 Android 版本之间可能变化,需要做兼容处理。
- JIT 缓存问题:Android 7.0+ 的 JIT 可能已经编译并缓存了旧方法,Sophix 需要主动清除 JIT 代码缓存。
核心参考源码路径:
- DexPathList:
libcore/dalvik/src/main/java/dalvik/system/DexPathList.java - ClassLoader:
libcore/libart/src/main/java/java/lang/ClassLoader.java - BaseDexClassLoader:
libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java - ArtMethod:
art/runtime/art_method.h - BSDiff:
external/bsdiff/bspatch.c - Tinker:
https://github.com/Tencent/tinker - Sophix:
https://github.com/alibaba/Sophix - Robust:
https://github.com/Meituan-Dianping/Robust - AndFix:
https://github.com/alibaba/AndFix - ActivityThread:
frameworks/base/core/java/android/app/ActivityThread.java - Instrumentation:
frameworks/base/core/java/android/app/Instrumentation.java


