目录
  1. 1. 一、热修复的本质问题
  2. 2. 二、Android 类加载机制
    1. 2.1. 2.1 DexPathList 与双亲委派
    2. 2.2. 2.2 ClassLoader 的双亲委派模型
  3. 3. 三、ClassLoader 方案:QQ 空间超级补丁
    1. 3.1. 3.1 实现步骤
    2. 3.2. 3.2 QQ 空间方案的缺陷
  4. 4. 四、Tinker 方案:全量 DEX 替换
    1. 4.1. 4.1 Tinker 的核心思路
    2. 4.2. 4.2 BSDiff 算法与 BSPatch
    3. 4.3. 4.3 Tinker 的类加载机制
    4. 4.4. 4.4 Tinker 的限制
  5. 5. 五、Sophix 方案:Native ART Method 替换
    1. 5.1. 5.1 ART Method 结构
    2. 5.2. 5.2 Method Replacement 的核心原理
    3. 5.3. 5.3 Sophix vs Tinker vs QQ 空间
  6. 6. 六、Android App Bundle 与 Google 的方案
  7. 7. 七、面试常问题目
解读开源框架系列-热修复设计

一、热修复的本质问题

热修复(Hot Fix / Hot Patch)的核心目标是:在用户不重新安装 APK 的情况下修复线上 Bug。这在移动应用发布中有巨大的商业价值——避免因为一个崩溃导致用户流失,也避免频繁发版带来的审核等待(特别是 iOS 端,Android 端虽然可以直接发版但用户更新率低)。

Android 热修复需要解决的根本问题是:如何让新代码在旧代码之前被执行? 在 Java/Android 中,”代码”的表现形式是 DEX 文件中的 Class,而 Class 的加载由 ClassLoader 完成。因此热修复的本质是操控 ClassLoader 的类加载顺序,让修复包中的类优先于原始 APK 中的类被加载。

二、Android 类加载机制

在深入热修复方案之前,必须理解 Android 的类加载体系。

2.1 DexPathList 与双亲委派

Android 的 ClassLoader 是 BaseDexClassLoader(API 26+)或 DexClassLoader/PathClassLoader。核心逻辑委托给了 DexPathList

// AOSP: libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
public class DexPathList {
private Element[] dexElements; // 每个 Element 包含一个 DexFile

public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
return null;
}

static class Element {
private final File path;
private final DexFile dexFile;

public Class<?> findClass(String name, ClassLoader definingContext,
List<Throwable> suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed) : null;
}
}
}

关键的发现:DexPathList 遍历 dexElements 数组,返回第一个匹配的 Class。如果我们将修复的 DEX 文件插入到 dexElements 数组的最前面,那么 findClass 就会先找到修复后的类。

2.2 ClassLoader 的双亲委派模型

// AOSP: libcore/libart/src/main/java/java/lang/ClassLoader.java
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 先检查是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false); // 委派给父 ClassLoader
} else {
c = findBootstrapClassOrNull(name); // BootClassLoader
}
} catch (ClassNotFoundException e) { /* ignore */ }
if (c == null) {
c = findClass(name); // 自己查找
}
}
return c;
}

Android 中的 ClassLoader 层级:

BootClassLoader(加载 Framework 类,如 Activity、String)
↑ parent
PathClassLoader(加载已安装 APK 中的类) ← 这是热修复要 hack 的目标

三、ClassLoader 方案:QQ 空间超级补丁

这是最早的开源方案之一。核心原理非常简单:反射修改 PathClassLoader 的 parent 字段,将修复 DEX 的 DexClassLoader 插入到类加载链中

3.1 实现步骤

// 1. 获取 PathClassLoader 的 DexPathList
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathList = pathListField.get(pathClassLoader);

// 2. 创建修复 DEX 的 DexClassLoader
DexClassLoader fixClassLoader = new DexClassLoader(
fixDexPath, // 修复 DEX 的路径
optimizedDir, // dex2oat 输出目录
null, // native lib 目录
pathClassLoader // 注意:parent 设为 PathClassLoader
);

// 3. 获取修复 ClassLoader 的 dexElements
Field fixPathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
fixPathListField.setAccessible(true);
Object fixPathList = fixPathListField.get(fixClassLoader);
Field fixElementsField = DexPathList.class.getDeclaredField("dexElements");
fixElementsField.setAccessible(true);
Object[] fixElements = (Object[]) fixElementsField.get(fixPathList);

// 4. 获取原始 dexElements
Field originElementsField = DexPathList.class.getDeclaredField("dexElements");
originElementsField.setAccessible(true);
Object[] originElements = (Object[]) originElementsField.get(pathList);

// 5. 合并:修复的 elements 在前面,原始的 elements 在后面
Object[] mergedElements = (Object[]) Array.newInstance(
fixElements.getClass().getComponentType(),
fixElements.length + originElements.length
);
System.arraycopy(fixElements, 0, mergedElements, 0, fixElements.length);
System.arraycopy(originElements, 0, mergedElements, fixElements.length, originElements.length);

// 6. 替换 dexElements
originElementsField.set(pathList, mergedElements);

3.2 QQ 空间方案的缺陷

  1. CLASS_ISPREVERIFIED 问题:Dalvik 运行时(Android 4.4 以前),如果一个类在其 DEX 之外引用了其他类,且被引用者在同一个 DEX 中被打上 CLASS_ISPREVERIFIED 标记,则运行时不会再次校验。但如果热修复修改了这个类,就会出现方法签名验证失败的问题。解决方案是在原始 APK 编译时插入一个”防校验”类,破坏 CLASS_ISPREVERIFIED 标记。ART 运行时已没有此问题。

  2. Android Nougat+ 混合编译:Android 7.0 引入了混合编译模式(JIT + AOT),DexPathList 的实现有变化,需要对不同的 API 级别做兼容处理。

四、Tinker 方案:全量 DEX 替换

腾讯开源的 Tinker(https://github.com/Tencent/tinker)是目前最成熟的热修复方案之一,已被微信团队大规模验证。

4.1 Tinker 的核心思路

Tinker 不插入单个修复 DEX,而是生成完整的修复后的 DEX 文件,替换掉原始 DEX。通过 BSDiff 算法生成差分包(patch),客户端下载差分包后与原始 DEX 合成新的 DEX。

4.2 BSDiff 算法与 BSPatch

BSDiff 是一个二进制差分算法,源自 Google 的 Chromium 项目。核心思想是:

新文件 = 旧文件 + 差分信息(新增/删除/修改的字节块)

差分信息包括三部分:

  1. diff string:新旧文件中不同的连续字节。
  2. extra string:新文件中独有的连续字节。
  3. control tuples:三元组 (diff_pos, extra_pos, copy_len),控制合成过程。

BSPatch 合成伪代码:

// external/bsdiff/bspatch.c
int bspatch(const uint8_t* old, int64_t oldsize, uint8_t** new,
int64_t* newsize, const uint8_t* patch) {
// 1. 读取 patch header
// 2. 遍历 control triples
// 3. 对每个 triple:从 diff string 读取差异数据,与 old 中的对应字节
// 执行 add_bytes(逐字节加法),结果写入 new
// 4. 从 extra string 读取额外数据,直接追加到 new
}

Tinker 的补丁生成流程:

原始 APK (old.apk) ──→ bsdiff ──→ patch.patch
修复后 APK (new.apk) ──→ bsdiff ──→ patch.patch
↓ 下发到客户端
客户端: bspatch(old_apk, patch) → new_apk

4.3 Tinker 的类加载机制

Tinker 使用自定义的 TinkerClassLoader,它替代了系统的 PathClassLoader。其 dexElements 中:

  • 最前面:修复后的 DEX(由 bspatch 合成)
  • 后面:原始 DEX

这样确保新类优先被加载。Tinker 同时支持 Application 类、Library、Resource 的热修复。

4.4 Tinker 的限制

  1. 必须重启应用:新的 DEX 文件需要重建 ClassLoader 才能生效。
  2. 不支持新增四大组件:Activity、Service、BroadcastReceiver、ContentProvider 的注册信息在 AndroidManifest.xml 中,无法通过 DEX 替换更改。
  3. 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
class ArtMethod {
// ...
GcRoot<mirror::Class> declaring_class_; // 所属类
std::atomic<std::uint32_t> access_flags_; // 访问标志(public/private/static等)
uint32_t dex_method_index_; // 在 DEX 中的方法索引
uint16_t method_index_; // 虚方法表索引
// 热修复最关键的两个字段:
void* entry_point_from_quick_compiled_code_; // 方法的入口点(编译后的机器码地址)
uint32_t hotness_count_; // 热方法计数器
// ...
};

5.2 Method Replacement 的核心原理

Sophix 的 native 层直接将新方法的 entry_point_from_quick_compiled_code_ 替换为旧方法的对应字段,或者在解释模式下替换方法的 DEX 指令指针:

// Sophix 核心逻辑(简化)
void replace_art_method(ArtMethod* src, ArtMethod* dest) {
// 1. 保存 dest 的声明类信息
// 2. 逐字段复制 src 到 dest:
// - entry_point_from_quick_compiled_code_
// - dex_method_index_
// - declaring_class_ 保持指向原类
// 3. 清除 JIT 缓存中的旧方法地址
}

5.3 Sophix vs Tinker vs QQ 空间

方案 原理 是否需重启 Android 版本兼容 成熟度
QQ 空间 ClassLoader + dexElements 合并 4.0+ Demo 级
Tinker BSDiff + 全量 DEX 替换 是(默认) 4.0+ 微信验证,非常成熟
Sophix Native ArtMethod 替换 可免重启(即时生效) 4.4+ 阿里商业验证

六、Android App Bundle 与 Google 的方案

Google 通过 Android App Bundle(AAB)和 Google Play 的 In-App Updates 提供了一套官方替代方案:

  1. Android App Bundle:Google Play 根据设备配置生成 Split APKs,用户只下载需要的代码和资源。
  2. In-App Updates:提供 Immediate 和 Flexible 两种模式,在应用内完成更新。
  3. 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 以上不存在此问题。


参考源码路径:

  • 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
打赏
  • 微信
  • 支付宝

评论