目录
  1. 1. 一、热修复的本质问题
    1. 1.1. 1.1 热修复的四大技术路线
  2. 2. 二、Android 类加载机制
    1. 2.1. 2.1 DexPathList 与双亲委派
    2. 2.2. 2.2 ClassLoader 的双亲委派模型
    3. 2.3. 2.3 编译和加载流程
  3. 3. 三、ClassLoader 方案:QQ 空间超级补丁
    1. 3.1. 3.1 实现步骤
    2. 3.2. 3.2 QQ 空间方案的缺陷
    3. 3.3. 3.3 CLASS_ISPREVERIFIED 问题详解
  4. 4. 四、Tinker 方案:全量 DEX 替换
    1. 4.1. 4.1 Tinker 的核心思路
    2. 4.2. 4.2 Tinker 架构全景
    3. 4.3. 4.3 BSDiff 算法与 BSPatch
    4. 4.4. 4.4 Tinker 的类加载机制
    5. 4.5. 4.5 Tinker 的资源修复
    6. 4.6. 4.6 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 AndFix
    4. 5.4. 5.4 Sophix 的三层修复架构
  6. 6. 六、Robust 方案:Instant Run 插桩
    1. 6.1. 6.1 原理
    2. 6.2. 6.2 优劣势
  7. 7. 七、Sophix vs Tinker vs QQ 空间 vs Robust
  8. 8. 八、Android 各版本对热修复的影响
    1. 8.1. 8.1 Android 7.0 (API 24) 的变化
    2. 8.2. 8.2 Android 9.0 (API 28) 的隐藏 API 限制
    3. 8.3. 8.3 Android 10 (API 29) 的存储范围限制
  9. 9. 九、Android App Bundle 与 Google 的方案
  10. 10. 十、面试常问题目
解读开源框架系列-热修复设计

一、热修复的本质问题

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

Android 热修复的发展历程:

2014-2015: 萌芽期
- QQ空间超级补丁:基于 ClassLoader + dexElements 合并
- Nuwa (女娲):基于 QQ 空间方案的开源封装
- RocooFix:早期的全量 DEX 替换

2015-2017: 爆发期
- Tinker (腾讯/微信):全量 DEX 合成 + BSDiff 差分
- AndFix (阿里):native ArtMethod 替换(ARM only)
- Amigo:多 ClassLoader 隔离方案
- Robust (美团):Instant Run 方案

2017-2020: 成熟期
- Sophix (阿里):AndFix + Tinker 融合 → 三层修复
- Tinker 2.0:支持 Application 热修复
- 各大厂商自研方案

2020-至今: 官方替代
- Google In-App Updates API
- AAB Dynamic Delivery

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

1.1 热修复的四大技术路线

热修复方案分类:

1. ClassLoader 方案(Java 层)
├── QQ 空间方案:注入 dexElements
├── Tinker 方案:全量 DEX 替换 + BSDiff 差分
└── Amigo 方案:多 ClassLoader 隔离

2. Native 方案(C/C++ 层)
├── AndFix:直接替换 ArtMethod 结构体字段
└── Sophix (AndFix 升级版):更全面的 ArtMethod 替换

3. Instant Run 方案
├── Robust:基于 Instant Run 的插桩代码
└── Aceso:字节码织入

4. 混合方案
└── Sophix 三层修复:
├── Native Hook (即时生效,修复方法级 Bug)
├── Method Replace (即时生效,修复类级 Bug)
└── Resource Patch (Tinker 方案,修复资源文件)

二、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 的目标

2.3 编译和加载流程

APK 构建                              应用启动
--------- -------
源码 (.java/.kt) PathClassLoader
│ │
▼ ▼
CLASS 文件 loadClass()
│ │
▼ ▼
dx/d8 → classes.dex findClass()
│ │
▼ ▼
打包到 APK DexPathList.findClass()


dexElements[i].findClass()


DexFile.loadClassBinaryName()


native defineClass


JIT/AOT 编译 (ART)

了解这个流程后,热修复的核心就很清晰了:在 dexElements 数组中找到合适的位置插入修复的 DEX

三、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 级别做兼容处理。

3.3 CLASS_ISPREVERIFIED 问题详解

这是 Dalvik 特有的问题,深入理解有助于理解 Android 运行时演进:

// 编译期 DEX 优化时,Dalvik 会做类校验
// libcore/dalvik/src/main/java/dalvik/system/DexFile.java (旧版)
//
// 如果一个类 A 引用类 B,且 B 与 A 在同一个 DEX 文件中,
// 则 A 被打上 CLASS_ISPREVERIFIED 标记。
// 加载时如果发现该标记,Dalvik 会跳过 A 的方法校验。
// 但如果热修复后,A 要引用修改后的 B(在不同 DEX 中),
// 因为 A 已有 ISPREVERIFIED 标记,Dalvik 不再校验,
// 发现方法所在的类与预期不同(不同 DEX 中的类),
// 抛出 IllegalAccessError

// 防校验方案:在原始 DEX 中插入一个引用外部类的方法
public class AntilazyLoad {
// 这个方法引用了所有 DEX 中的类,破坏了 ISPREVERIFIED
public static void prevent() {
// 引用一个外部 DEX 中的类(实际不存在,只是编译期声明)
}
}

四、Tinker 方案:全量 DEX 替换

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

4.1 Tinker 的核心思路

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

4.2 Tinker 架构全景

开发阶段                              客户端
-------- ------
基准 APK (old.apk) ──→ 缓存在设备上
│ │
│ (修复后) │
▼ │
修复 APK (new.apk) │
│ │
│ bsdiff(old, new) │
▼ │
patch.patch │
│ │
│ 下发到 CDN │
│ │
└──────────────────────────────►│

bspatch(old.apk, patch.patch) → new_apk


TinkerClassLoader 加载 new_apk

4.3 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(magic, control_size, diff_size, new_size)
int64_t control_size, diff_size, new_size;
memcpy(&control_size, patch + MAGIC_SIZE, sizeof(control_size));
memcpy(&diff_size, patch + MAGIC_SIZE + 8, sizeof(diff_size));
memcpy(&new_size, patch + MAGIC_SIZE + 16, sizeof(new_size));

// 2. 分配新文件缓冲区
uint8_t* new_data = malloc(new_size);

// 3. 遍历 control triples
int64_t new_pos = 0;
int64_t old_pos = 0;
int64_t diff_pos = 0;
int64_t extra_pos = 0;

int64_t control_offset = MAGIC_SIZE + 24;
int64_t diff_offset = control_offset + control_size;
int64_t extra_offset = diff_offset + diff_size;

while (new_pos < new_size) {
int64_t diff_len, extra_len, copy_len;
// 读取 control triple
decode_offset(patch, control_offset, &diff_len);
decode_offset(patch, control_offset, &extra_len);
decode_offset(patch, control_offset, &copy_len);

// 应用 diff:diff_data[i] + old[old_pos + i] => new_data
for (int i = 0; i < diff_len; i++) {
new_data[new_pos++] = old_data[old_pos++] + patch[diff_pos++];
}

// 应用 extra:直接复制 extra data
for (int i = 0; i < extra_len; i++) {
new_data[new_pos++] = patch[extra_pos++];
}

// 应用 copy:直接复制 old data
for (int i = 0; i < copy_len; i++) {
new_data[new_pos++] = old_data[old_pos++];
}
}

*new = new_data;
*newsize = new_size;
return 0;
}

Tinker 的补丁生成流程:

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

4.4 Tinker 的类加载机制

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

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

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

Tinker 的 Application 热修复流程:

1. Application 启动
2. TinkerApplicationLifeCycle 在 Application.attachBaseContext 中运行
3. 检查是否有待应用的补丁
4. 如果有补丁 → 合成新 DEX、新 SO、新资源
5. 创建 TinkerClassLoader 替换系统的 PathClassLoader
6. 反射修改 LoadedApk.mClassLoader
7. 加载修复后的类

4.5 Tinker 的资源修复

Tinker 通过替换 AssetManager 和 Resources 来实现资源热修复:

// TinkerResourcesManager
public void patchResources(Context context) {
// 1. 加载补丁中的 resources.arsc
// 2. 创建新的 AssetManager → addAssetPath(patch.apk) → addAssetPath(origin.apk)
// 3. 创建新的 Resources
// 4. 反射替换 ContextImpl 和 LoadedApk 中的 Resources 引用
}

4.6 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_; // 热方法计数器

// 解释模式入口
void* entry_point_from_interpreter_; // 解释器入口
// ...
};

5.2 Method Replacement 的核心原理

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

// Sophix 核心逻辑(简化)
void replace_art_method(ArtMethod* src, ArtMethod* dest) {
// 1. 保存 dest 的声明类信息(替换后不能改变 declaring_class)
GcRoot<mirror::Class> declaring_class = dest->declaring_class_;

// 2. 逐字段复制 src 到 dest:
dest->access_flags_ = src->access_flags_;
dest->dex_method_index_ = src->dex_method_index_;
dest->method_index_ = src->method_index_;
dest->entry_point_from_quick_compiled_code_
= src->entry_point_from_quick_compiled_code_;
dest->entry_point_from_interpreter_
= src->entry_point_from_interpreter_;

// 3. 恢复 declaring_class(关键!)
dest->declaring_class_ = declaring_class;

// 4. 清除 JIT 缓存中的旧方法地址
// (Android 7.0+ JIT 可能已编译并缓存了旧方法)
flushJitCache(dest);
}

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 方案被设计为一个融合了三种修复粒度的方案:

┌─────────────────────────────────────┐
│ Sophix 三层修复 │
│ │
│ Layer 1: Native Hook │
│ ├── 即时生效(无需重启) │
│ ├── 修复方法级 bug │
│ ├── 原理:替换 ArtMethod 入口点 │
│ └── 限制:不支持新增方法/字段 │
│ │
│ Layer 2: Method Replace │
│ ├── 即时生效(无需重启) │
│ ├── 修复类级 bug │
│ ├── 原理:替换整个 ArtMethod │
│ └── 限制:不支持修改类结构 │
│ │
│ Layer 3: Resource Patch │
│ ├── 需要重启 │
│ ├── 修复资源文件(图片、布局、字符串)│
│ ├── 原理:Tinker 方案 + AssetManager │
│ └── 支持新增/替换任意资源 │
└─────────────────────────────────────┘

六、Robust 方案:Instant Run 插桩

6.1 原理

美团开源的 Robust(https://github.com/Meituan-Dianping/Robust)基于 Instant Run 的插桩技术。它在编译期给每个类注入一个 ChangeQuickRedirect 接口的引用,运行时每个方法先检查是否有修复逻辑:

// 原始代码
public class MyClass {
public int doSomething(int a, int b) {
return a + b;
}
}

// Robust 插桩后
public class MyClass {
public static ChangeQuickRedirect changeQuickRedirect; // 编译期注入

public int doSomething(int a, int b) {
if (changeQuickRedirect != null) {
// 有修复代码,使用修复逻辑
return ((Integer) changeQuickRedirect.accessDispatch(
"doSomething", new Object[]{a, b}, null, null)).intValue();
}
// 原始逻辑
return a + b;
}
}

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 编译。

这对热修复的影响:

  1. 存在 oat 文件缓存,即使替换了 DEX,系统可能仍使用旧的 oat。
  2. Tinker 方案需要清除 oat 文件缓存,确保新的 DEX 被重新编译。

8.2 Android 9.0 (API 28) 的隐藏 API 限制

Android 9.0 引入了对 @hide API 和非 SDK 接口的访问限制:

  • 所有反射调用 @hide API 都会触发警告或抛出 NoSuchMethodException
  • 这对依赖反射修改 dexElements 的热修复方案(QQ 空间方案、Tinker)有重大影响。
// Android 9+ 中,这些反射访问可能被阻止
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
// 抛出 NoSuchFieldException

// 解决方案:
// 1. 在 AndroidManifest.xml 中添加豁免
// <uses-library android:name="org.apache.http.legacy" />
// 2. 使用 Google 提供的豁免 API(需要 Google Play 审核)
// 3. 使用 JNI 层绕过(如通过 native 代码访问隐藏字段)

8.3 Android 10 (API 29) 的存储范围限制

Android 10 引入了 Scoped Storage,限制了应用对共享存储的访问。热修复补丁文件的存储路径可能受影响——应用只能从自己的私有目录或特定媒体集合中读取文件。

九、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 以上不存在此问题。

Q5: Sophix 为什么能即时生效?它的限制是什么?

Sophix 的即时生效依赖于直接修改 ART 运行时中的 ArtMethod 结构体。当 Java 方法被调用时,ART 通过 ArtMethod 中的 entry_point_from_quick_compiled_code_(编译后的机器码入口)或 entry_point_from_interpreter_(解释器入口)来执行代码。Sophix 将这些入口指针替换为修复后的方法的入口指针,使得后续调用直接跳转到修复代码。

限制:

  1. 不能新增方法或字段:ArtMethod 的替换是单个方法的替换,不能改变类的结构(类的虚方法表大小在编译时就固定了)。
  2. 不能修改类的继承关系:ArtMethod 的 declaring_class_ 必须指向原类。
  3. 架构依赖:ArtMethod 结构体的字段布局在不同 CPU 架构(ARM、ARM64、x86)上不同,需要针对性适配。
  4. Android 版本依赖:ArtMethod 的字段顺序和大小在不同 Android 版本之间可能变化,需要做兼容处理。
  5. 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
打赏
  • 微信
  • 支付宝

评论