插件化与热修复
# 19.插件化与热修复
# 目录介绍
- 一、引言:动态化的需求与挑战
- 二、ClassLoader与类加载机制
- 2.1 Android的ClassLoader体系
- 2.2 双亲委派模型
- 2.3 findClass的查找过程
- 三、插件化的核心原理
- 3.1 插件化需要解决的四大问题
- 3.2 插件APK的加载
- 四、Activity插件化方案
- 4.1 占坑方案(VirtualAPK/RePlugin思路)
- 4.2 Hook Instrumentation实现
- 五、资源加载与插件资源管理
- 5.1 AssetManager加载插件资源
- 5.2 资源ID冲突问题
- 六、热修复的技术流派
- 6.1 三大技术流派
- 七、类替换方案原理(Tinker)
- 7.1 核心原理
- 7.2 dexElements插桩实现
- 7.3 CLASS_ISPREVERIFIED问题
- 八、底层替换方案原理(AndFix)
- 8.1 ArtMethod替换
- 8.2 Native层替换代码
- 8.3 底层替换的局限
- 九、Instant Run与代码热替换
- 9.1 Instant Run的三种模式
- 9.2 Robust方案(美团)
- 十、资源热修复
- 10.1 资源修复原理
- 10.2 资源差分与合成
- 10.3 资源修复的兼容性问题
- 十一、SO库热修复
- 11.1 SO库修复方案
- 11.2 SO加载机制分析
- 11.3 SO修复的限制
- 十二、动态化方案对比与选型
- 12.1 方案对比
- 12.2 选型建议
- 十三、面试高频问题与深度分析
- 13.1 热修复为什么需要重启才能生效?
- 13.2 插件化在Android高版本的限制?
- 13.3 描述插件化加载Activity的完整过程
- 13.4 Tinker热修复的原理和优缺点
- 13.5 为什么Robust不需要重启
- 十四、插件化与热修复的安全风险
- 14.1 安全隐患分析
- 14.2 Google Play的政策限制
- 14.3 插件化的替代方案
- 十五、总结
# 一、引言:动态化的需求与挑战
在移动互联网快速迭代的背景下,传统的发版→审核→用户更新流程太慢。插件化和热修复技术让应用可以在不发版的情况下动态加载新功能或修复Bug。
疑惑:Android的安全沙箱机制不允许动态执行未安装的代码,那插件化是如何实现的?热修复又是怎么"偷梁换柱"的?
# 二、ClassLoader与类加载机制
# 2.1 Android的ClassLoader体系
Android ClassLoader继承关系:
ClassLoader(Java基类)
├── BootClassLoader(加载核心库:java.lang.*等)
├── PathClassLoader(加载已安装APK的类)
│ → 默认的应用ClassLoader
│ → dexPath = APK中的classes.dex
└── DexClassLoader(加载外部dex/apk/jar)
→ 可以指定任意路径的dex文件
→ 插件化的核心工具
BaseDexClassLoader(PathClassLoader和DexClassLoader的共同父类)
└── DexPathList pathList
└── Element[] dexElements
├── dex文件1 → DexFile对象
├── dex文件2 → DexFile对象
└── dex文件3 → DexFile对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 2.2 双亲委派模型
类加载的双亲委派流程:
loadClass(className) {
// 1. 检查是否已加载
Class<?> c = findLoadedClass(name);
if (c != null) return c;
// 2. 委托父ClassLoader加载
if (parent != null) {
c = parent.loadClass(name);
}
// 3. 父ClassLoader加载不到,自己加载
if (c == null) {
c = findClass(name);
// BaseDexClassLoader.findClass()
// → pathList.findClass(name, suppressedExceptions)
// → 遍历dexElements数组,在每个dex中查找
}
return c;
}
关键点:dexElements数组的顺序决定了类的加载优先级
→ 这是热修复的核心利用点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 2.3 findClass的查找过程
// BaseDexClassLoader.findClass()
protected Class<?> findClass(String name) {
List<Throwable> suppressedExceptions = new ArrayList<>();
// 委托给DexPathList查找
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
throw new ClassNotFoundException(name);
}
return c;
}
// DexPathList.findClass()
public Class<?> findClass(String name, List<Throwable> suppressed) {
// 遍历dexElements数组
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz; // 找到了就返回,不再继续遍历
}
}
return null;
}
// 如果在dexElements[0]中找到了类,就不会去dexElements[1]中找
// → 热修复原理:将修复后的dex插入dexElements数组最前面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 三、插件化的核心原理
# 3.1 插件化需要解决的四大问题
1. 代码加载:如何加载插件APK中的类
→ DexClassLoader加载dex
→ 底层原理:DexClassLoader构造时传入插件APK路径
→ BaseDexClassLoader.findClass() → 遍历dexElements查找类
→ 插件APK会被优化为odex/vdex加速加载
2. 资源加载:如何加载插件的资源
→ AssetManager.addAssetPath()
→ 将插件APK路径添加到AssetManager的搜索路径
→ Resources通过AssetManager访问插件中的资源文件
→ 问题:宿主和插件的资源ID可能冲突(需要修改AAPT分配规则)
3. 组件生命周期:如何让未注册的Activity有生命周期
→ 占坑Activity + Hook AMS
→ 在宿主Manifest中预注册StubActivity
→ 启动插件Activity时,Intent先指向StubActivity绕过AMS检查
→ 在ActivityThread创建Activity时,替换回真正的插件Activity
→ AMS只知道StubActivity,真正运行的是插件Activity
4. 四大组件注册:未在Manifest中注册的组件如何使用
→ 宿主预埋占坑组件
→ Activity:多个占坑Activity(不同启动模式各预埋几个)
→ Service:占坑Service + 内部分发机制
→ BroadcastReceiver:动态注册不需要Manifest声明
→ ContentProvider:占坑Provider + URI路由分发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 3.2 插件APK的加载
// 加载插件APK
public void loadPlugin(String pluginPath) {
// 1. 创建插件的ClassLoader
DexClassLoader pluginClassLoader = new DexClassLoader(
pluginPath, // 插件APK路径
getDir("dex", 0).getAbsolutePath(), // dex优化输出目录
null, // native库路径
getClassLoader() // 父ClassLoader
);
// 2. 加载插件中的类
Class<?> pluginClass = pluginClassLoader.loadClass("com.plugin.PluginActivity");
// 3. 创建实例
Object instance = pluginClass.newInstance();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 四、Activity插件化方案
# 4.1 占坑方案(VirtualAPK/RePlugin思路)
核心思想:在宿主AndroidManifest中预埋"占坑"Activity
宿主Manifest:
<activity android:name=".StubActivity1" />
<activity android:name=".StubActivity2" />
<activity android:name=".StubActivity3"
android:launchMode="singleTask" />
<!-- 预埋多个占坑Activity,覆盖各种launchMode -->
启动插件Activity的流程:
1. App调用 startActivity(pluginActivityIntent)
2. Hook层拦截Intent,替换为StubActivityIntent
3. AMS认为启动的是已注册的StubActivity → 通过检查
4. AMS回调App创建Activity时
5. Hook层再将StubActivity替换回PluginActivity
6. 反射创建PluginActivity实例
7. PluginActivity正常执行生命周期
Hook点:
方案A:Hook Instrumentation
→ 在execStartActivity中替换Intent
→ 在newActivity中创建插件Activity
方案B:Hook IActivityManager(AMS代理)
→ 在startActivity前替换Intent
→ 在Handler.mCallback中还原
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 4.2 Hook Instrumentation实现
// 替换Instrumentation
public class PluginInstrumentation extends Instrumentation {
private Instrumentation base;
@Override
public ActivityResult execStartActivity(Context who, IBinder contextThread,
IBinder token, Activity target, Intent intent, int requestCode, Bundle options) {
// 1. 保存真实的插件Activity类名
intent.putExtra("plugin_activity", intent.getComponent().getClassName());
// 2. 替换为占坑Activity
intent.setClassName(who, "com.host.StubActivity");
// 3. 调用原始方法(AMS检查的是StubActivity → 通过)
return base.execStartActivity(who, contextThread, token, target, intent,
requestCode, options);
}
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) {
// 4. 还原为插件Activity
String pluginActivity = intent.getStringExtra("plugin_activity");
if (pluginActivity != null) {
// 用插件ClassLoader加载真实的Activity
return (Activity) pluginClassLoader.loadClass(pluginActivity).newInstance();
}
return base.newActivity(cl, className, intent);
}
}
// 通过反射替换ActivityThread中的mInstrumentation
Field field = ActivityThread.class.getDeclaredField("mInstrumentation");
field.setAccessible(true);
field.set(currentActivityThread, new PluginInstrumentation(originalInstrumentation));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 五、资源加载与插件资源管理
# 5.1 AssetManager加载插件资源
// 创建插件的AssetManager和Resources
AssetManager assetManager = AssetManager.class.newInstance();
// 反射调用隐藏方法addAssetPath
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, pluginApkPath);
Resources pluginResources = new Resources(
assetManager,
hostResources.getDisplayMetrics(),
hostResources.getConfiguration());
// 插件Activity中使用插件Resources
@Override
public Resources getResources() {
return pluginResources; // 返回插件的Resources
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 5.2 资源ID冲突问题
问题:宿主和插件的资源ID可能冲突
宿主:R.layout.activity_main = 0x7F030001
插件:R.layout.activity_main = 0x7F030001 ← 冲突!
解决方案:
1. 修改插件的资源ID前缀(AAPT修改packageId)
宿主:0x7F(默认)
插件1:0x71
插件2:0x72
2. 独立Resources
每个插件使用独立的AssetManager和Resources
3. 资源混合加载
将插件资源路径也添加到宿主的AssetManager
→ 需要处理同名资源覆盖问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 六、热修复的技术流派
# 6.1 三大技术流派
┌────────────────┬──────────────┬────────────────┬──────────────┐
│ 流派 │ 代表框架 │ 原理 │ 生效时机 │
├────────────────┼──────────────┼────────────────┼──────────────┤
│ 类加载替换 │ Tinker/QFix │ 替换dexElements │ 重启后生效 │
│ 底层方法替换 │ AndFix/Robust│ 替换ArtMethod │ 即时生效 │
│ Instant Run │ Google │ 增量代码推送 │ 即时/重启 │
└────────────────┴──────────────┴────────────────┴──────────────┘
1
2
3
4
5
6
7
2
3
4
5
6
7
三大流派的深入对比:
1. 类加载替换(稳定性最好)
原理:利用ClassLoader的dexElements数组顺序加载特性
→ 将修复后的dex插入到数组头部
→ 类加载时先找到修复版本,不再加载旧版本
优点:兼容性好,支持所有修改类型
缺点:需要重启才能生效(类一旦加载无法卸载)
代表:Tinker(微信)、QFix(手Q)、Nuwa
2. 底层方法替换(即时生效)
原理:在ART运行时替换方法的入口指针
→ 直接修改ArtMethod结构体中的entryPoint
→ 调用旧方法时实际跳转到新方法
优点:即时生效,无需重启
缺点:兼容性差(不同ART版本的ArtMethod结构不同)
不支持新增/删除方法和字段
代表:AndFix(阿里)、Sophix(阿里改进版)
3. 代码分发(工程化最好)
原理:编译时在每个方法前插入分发逻辑
→ 运行时通过开关决定执行原方法还是补丁方法
优点:即时生效、兼容性好、不依赖ART内部结构
缺点:包体积增加(每个方法都插入了分发代码)
代表:Robust(美团)、Aceso
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 七、类替换方案原理(Tinker)
# 7.1 核心原理
Tinker的热修复原理:
原始APK中的dex:
dexElements = [classes.dex, classes2.dex]
→ 类A(有Bug)在classes.dex中
修复过程:
1. 比较旧APK和新APK,生成差分包(dexDiff算法)
2. 差分包下发到设备
3. 设备端合成完整的修复dex
4. 将修复dex插入dexElements数组最前面
修复后:
dexElements = [patch.dex, classes.dex, classes2.dex]
→ patch.dex中包含修复后的类A
→ 加载类A时先在patch.dex中找到 → 使用修复后的版本
→ classes.dex中的旧类A不会被加载(已经在patch.dex中找到了)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 7.2 dexElements插桩实现
// 将修复dex插入dexElements最前面
public static void installPatch(Context context, File patchDex) {
// 1. 获取应用的ClassLoader
ClassLoader classLoader = context.getClassLoader();
// 2. 反射获取pathList
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathList = pathListField.get(classLoader);
// 3. 反射获取dexElements
Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object[] oldElements = (Object[]) dexElementsField.get(pathList);
// 4. 加载patch dex
DexClassLoader patchLoader = new DexClassLoader(
patchDex.getAbsolutePath(), optimizedDir, null, classLoader);
Object patchPathList = pathListField.get(patchLoader);
Object[] patchElements = (Object[]) dexElementsField.get(patchPathList);
// 5. 合并:patch在前,原始在后
Object[] newElements = new Object[patchElements.length + oldElements.length];
System.arraycopy(patchElements, 0, newElements, 0, patchElements.length);
System.arraycopy(oldElements, 0, newElements, patchElements.length, oldElements.length);
// 6. 设置回去
dexElementsField.set(pathList, newElements);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 7.3 CLASS_ISPREVERIFIED问题
Android Dalvik上的问题:
如果类A的所有引用都在同一个dex中,Dalvik会给A打上IS_PREVERIFIED标记
→ 如果修复后的A在另一个dex中引用了原dex的类
→ 会抛出 Class ref in pre-verified class resolved to unexpected implementation
解决方案(QZone方案):
→ 在编译时通过字节码插桩,让每个类都引用一个单独dex中的类
→ 这样所有类都不会被打上IS_PREVERIFIED标记
Tinker的方案:
→ 生成完整的修复dex(而非增量patch)
→ 完全替换旧dex
→ 避免跨dex引用问题
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 八、底层替换方案原理(AndFix)
# 8.1 ArtMethod替换
AndFix的核心:直接替换ArtMethod结构体
在ART虚拟机中,每个Java方法对应一个ArtMethod结构体:
struct ArtMethod {
uint32_t declaring_class_; // 所属类
uint32_t access_flags_; // 访问标志
uint32_t dex_code_item_offset_; // dex中的代码偏移
uint32_t dex_method_index_; // dex方法索引
struct PtrSizedFields {
void* data_; // 解释执行的CodeItem或JNI入口
void* entry_point_from_quick_compiled_code_; // 编译后代码入口
} ptr_sized_fields_;
};
热修复过程:
1. 加载补丁dex,找到修复后的方法
2. 获取原方法和新方法的ArtMethod指针
3. 用新方法的ArtMethod整体替换旧方法的ArtMethod
memcpy(oldMethod, newMethod, sizeof(ArtMethod));
4. 之后调用原方法时,实际执行的是新方法的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 8.2 Native层替换代码
// AndFix Native层替换(简化)
void replaceMethod(JNIEnv* env, jobject src, jobject dest) {
// 获取ArtMethod指针
ArtMethod* smeth = reinterpret_cast<ArtMethod*>(
env->FromReflectedMethod(src));
ArtMethod* dmeth = reinterpret_cast<ArtMethod*>(
env->FromReflectedMethod(dest));
// 整体替换
// 将目标方法的所有字段复制到源方法
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->access_flags_ = dmeth->access_flags_;
smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
smeth->dex_method_index_ = dmeth->dex_method_index_;
smeth->ptr_sized_fields_.data_ = dmeth->ptr_sized_fields_.data_;
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 8.3 底层替换的局限
局限性:
1. ArtMethod结构体在不同Android版本中不同
→ 需要针对每个版本适配
→ 新版本发布后需要更新适配
2. 不能增减方法数量
→ 只能修改已有方法的实现
3. 不能修改类结构
→ 不能新增/删除字段
→ 不能新增/删除方法
4. 内联优化可能导致替换失效
→ 被内联的方法替换后,调用处仍执行旧代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 九、Instant Run与代码热替换
# 9.1 Instant Run的三种模式
Hot Swap(热交换):
→ 不重启Activity
→ 修改方法体内的代码
→ 通过修改dispatch方法路由到新实现
→ 原理:ASM在每个类中插入$change字段
→ 运行时替换$change指向的IncrementalChange对象
→ 方法调用时先检查$change,不为null则调用补丁方法
Warm Swap(温交换):
→ 重启Activity(不重启进程)
→ 修改资源文件
→ 原理:替换AssetManager,重新加载资源
→ Activity重建后使用新的资源
Cold Swap(冷交换):
→ 重启应用(重启进程)
→ 修改类结构(新增方法/字段)
→ 原理:生成新的dex分片,替换旧的
→ 类似Tinker的dexElements替换
Instant Run的局限性:
仅支持Debug模式,不支持Release
Android Studio 3.5+被Apply Changes取代
→ Apply Changes使用JVMTI(Android 8+)
→ 原理更简洁:直接通过JVMTI API重定义类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 9.2 Robust方案(美团)
// Robust的思路:编译时在每个方法前插入分发逻辑
// 原始方法:
public String getUserName() {
return name;
}
// 插桩后:
public static ChangeQuickRedirect changeQuickRedirect;
public String getUserName() {
if (changeQuickRedirect != null) {
// 有补丁 → 执行补丁中的方法
return (String) changeQuickRedirect.accessDispatch(
new Object[]{this}, "getUserName");
}
// 无补丁 → 执行原始逻辑
return name;
}
// 下发补丁时,设置changeQuickRedirect对象
// 即时生效,无需重启
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 十、资源热修复
# 10.1 资源修复原理
资源热修复的核心是替换运行时的AssetManager,使其加载修复后的资源文件:
资源修复方案(以Tinker为例):
1. 生成资源差分包
→ 比较新旧APK中的resources.arsc
→ 生成资源patch
2. 运行时合成新的resources.arsc
3. 创建新的AssetManager
AssetManager newAssetManager = new AssetManager();
newAssetManager.addAssetPath(patchedResPath);
4. 替换所有已有的Resources中的AssetManager
→ 遍历所有Activity.getResources()
→ 反射替换其中的mAssets字段
→ Resources.mAssets = newAssetManager
5. 资源即时生效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 10.2 资源差分与合成
资源差分方案对比:
Tinker方案:
├── 对比新旧APK中的resources.arsc
├── 生成BSDiff差分包(约为全量的10%~30%)
├── 客户端合成完整的resources.arsc
└── 优点:补丁包小;缺点:合成耗时
Sophix方案:
├── 直接打包修改过的资源文件
├── 运行时通过addAssetPath追加资源路径
├── 新资源优先级高于旧资源
└── 优点:简单快速;缺点:补丁包略大
资源修复的关键步骤:
1. 构造新的AssetManager
→ 反射调用AssetManager.addAssetPath()
→ 将修复后的资源路径添加进去
2. 替换所有引用
→ ResourcesManager中缓存的Resources
→ 每个Activity持有的Resources
→ ContextImpl中的mResources
→ 通过反射逐一替换
3. 注意事项:
→ 必须替换所有持有AssetManager引用的对象
→ 遗漏任何一个都可能导致资源不一致
→ Android不同版本的Resources缓存机制不同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 10.3 资源修复的兼容性问题
不同Android版本的资源修复适配:
Android 4.x ~ 6.x:
├── addAssetPath是隐藏方法,需要反射调用
├── Resources中持有AssetManager的mAssets字段
└── 替换mAssets后立即生效
Android 7.0 ~ 8.1:
├── ResourcesManager引入了ResourcesImpl
├── 需要同时替换ResourcesImpl中的AssetManager
├── 多个Resources可能共享同一个ResourcesImpl
└── 替换策略:找到所有ResourcesImpl逐一替换
Android 9.0+:
├── AssetManager重构为AssetManager2(C++层)
├── addAssetPath已废弃
├── 需要通过setApkAssets设置资源路径
├── 反射调用的兼容性风险增大
└── 隐藏API限制需要绕过
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 十一、SO库热修复
# 11.1 SO库修复方案
SO库修复方案:
方案1:替换System.loadLibrary路径
→ 将修复后的so放在优先搜索路径
→ 修改DexPathList.nativeLibraryDirectories
方案2:接口替换
→ SO通过JNI接口被调用
→ 先加载修复后的SO
→ JNI方法注册时使用新SO的符号
注意:
→ SO修复必须重启后生效
→ 无法在运行中替换已加载的SO
→ 因为SO已映射到进程地址空间,无法卸载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 11.2 SO加载机制分析
Android SO加载的完整流程:
System.loadLibrary("native-lib")
→ Runtime.loadLibrary0()
→ ClassLoader.findLibrary("native-lib")
→ BaseDexClassLoader.findLibrary()
→ DexPathList.findLibrary()
→ 遍历nativeLibraryPathElements数组
→ 在每个路径下查找libnative-lib.so
→ Runtime.doLoad()
→ nativeLoad() → dlopen()
→ 链接器解析SO的ELF头
→ 映射.text/.data/.bss段到内存
→ 执行.init和.init_array中的初始化函数
→ 注册JNI方法(如果有JNI_OnLoad)
SO修复的关键利用点:
nativeLibraryPathElements也是一个数组,和dexElements类似
→ 将修复SO的目录插入到数组前面
→ 加载时先在修复目录中找到新的SO
→ 原目录中的旧SO不会被加载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 11.3 SO修复的限制
SO热修复的限制:
1. 无法即时生效
→ SO通过dlopen加载后映射到进程地址空间
→ 没有dlclose机制可以安全卸载(依赖关系复杂)
→ 必须重启进程后重新加载
2. ABI兼容性
→ 修复SO必须与原SO的ABI一致(armeabi-v7a/arm64-v8a)
→ 不能改变SO的导出符号
→ 如果修改了JNI方法签名,需要同时修复Java层
3. 依赖关系
→ 如果SO-A依赖SO-B,修复SO-A时需要确保SO-B先加载
→ 加载顺序错误会导致链接失败
4. 系统版本限制
→ Android 7.0+的Namespace机制限制了SO的链接范围
→ 应用SO不能链接系统私有SO
→ 修复SO需要注意不引入新的系统私有SO依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 十二、动态化方案对比与选型
# 12.1 方案对比
┌──────────┬────────────┬──────────────┬───────────────┐
│ 方案 │ 即时生效 │ 类结构修改 │ 适用场景 │
├──────────┼────────────┼──────────────┼───────────────┤
│ Tinker │ 重启生效 │ 支持 │ 全量修复 │
│ Robust │ 即时生效 │ 不支持 │ 紧急Bug修复 │
│ Sophix │ 两种都支持 │ 部分支持 │ 阿里系应用 │
│ 插件化 │ 即时加载 │ 完全支持 │ 模块动态下发 │
│ RN/Flutter│ 热更新 │ 完全支持 │ 跨平台场景 │
└──────────┴────────────┴──────────────┴───────────────┘
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 12.2 选型建议
不同场景的方案选择:
场景1:紧急线上Bug修复
推荐:Robust(美团)
理由:即时生效,无需重启,修复速度最快
局限:只能修改已有方法实现
场景2:版本级别的功能修复
推荐:Tinker(微信)
理由:支持类替换、资源替换、SO替换
局限:需要冷启动生效
场景3:功能动态下发(国内市场)
推荐:插件化框架(RePlugin/VirtualAPK)
理由:完全动态加载,功能级别更新
局限:维护成本高,高版本Android限制多
场景4:功能动态下发(海外市场)
推荐:Dynamic Feature Module(App Bundle)
理由:Google官方方案,合规安全
局限:依赖Google Play分发
场景5:跨平台动态化
推荐:React Native / Flutter CodePush
理由:跨平台统一方案
局限:性能不及原生
综合建议:
大多数项目 → Tinker(全量修复)+ Robust(紧急修复)
海外项目 → Dynamic Feature Module + 配置下发
超大型项目 → 组件化 + 按需选择插件化/热修复
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 十三、面试高频问题与深度分析
# 13.1 热修复为什么需要重启才能生效?
类加载替换方案需要重启的原因:
→ Java类一旦被ClassLoader加载,就不能被卸载
→ 已加载的类保存在ClassLoader的缓存中
→ 下次findClass先检查缓存,直接返回旧类
→ 只有重启后,所有类重新加载,才会加载到修复的类
底层替换方案不需要重启:
→ 直接修改内存中ArtMethod的函数指针
→ 下次调用该方法时立即执行新代码
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 13.2 插件化在Android高版本的限制?
Android 9+限制:
→ 隐藏API限制(@hide注解的API不可反射调用)
→ 多数Hook点依赖的是隐藏API
Android 12+限制:
→ 更严格的exported属性检查
→ 限制后台启动Activity
Google Play限制:
→ 禁止下载可执行代码
→ 插件化应用可能被拒审
应对策略:
→ 使用元反射/Double反射绕过限制
→ 使用FreeReflection等库
→ 减少对隐藏API的依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 13.3 描述插件化加载Activity的完整过程
插件Activity加载的完整过程:
1. 加载插件DEX
DexClassLoader加载插件APK中的classes.dex
将插件类纳入类加载范围
2. 加载插件资源
通过反射调用AssetManager.addAssetPath
将插件APK路径添加到资源搜索路径
创建插件专用的Resources对象
3. 启动插件Activity
├── 方案A:StubActivity占坑
│ ├── 在宿主Manifest中预注册StubActivity
│ ├── 启动StubActivity
│ ├── Hook ActivityThread.mH
│ ├── 在消息处理中将StubActivity替换为插件Activity
│ └── 插件Activity正常走生命周期
│
└── 方案B:Hook Instrumentation
├── 反射替换ActivityThread中的Instrumentation
├── 在execStartActivity中替换Intent为StubActivity
├── 在newActivity中创建插件Activity实例
└── 在callActivityOnCreate中传入插件Resources
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 13.4 Tinker热修复的原理和优缺点
Tinker热修复原理:
1. 在服务端生成补丁
├── 新旧APK做dex diff
└── 生成差量补丁文件(.patch)
2. 客户端下载补丁
└── 补丁文件远小于完整DEX
3. 客户端合成
├── 旧DEX + patch → 新DEX(完整的修复后DEX)
└── 合成在后台线程进行
4. 应用重启生效
├── 将合成的新DEX插入dexElements前面
└── 类加载时优先加载修复后的类
优点:
├── 支持类替换、资源替换、SO替换
├── 补丁包小(差量包)
├── 稳定可靠(微信数亿用户验证)
└── 开源且维护活跃
缺点:
├── 需要冷启动(重启应用)
├── 合成过程可能耗时
├── DEX合成失败需要回退
└── 不能修改AndroidManifest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 13.5 为什么Robust不需要重启
Robust即时生效的原理:
编译时插桩:
每个方法前都插入判断逻辑:
// 原始代码
public void showToast(String msg) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
}
// 插桩后的代码
public static ChangeQuickRedirect changeQuickRedirect;
public void showToast(String msg) {
if (changeQuickRedirect != null) {
// 如果有补丁,执行补丁代码
if (PatchProxy.proxy(this, new Object[]{msg},
changeQuickRedirect, false).isSupported) {
return;
}
}
// 否则执行原始代码
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
}
热修复过程:
1. 下载补丁类(实现ChangeQuickRedirect接口)
2. 通过反射设置目标类的changeQuickRedirect字段
3. 下次调用方法时自动走补丁逻辑
优点:不需要重启,即时生效
缺点:每个方法都有判断开销,增加包体积约10%
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 十四、插件化与热修复的安全风险
# 14.1 安全隐患分析
插件化和热修复虽然强大,但引入了显著的安全风险:
安全风险分析:
1. 代码注入风险
└── 动态加载的DEX可能被篡改
└── 如果下载链路没有加密和校验
└── 攻击者可以替换DEX文件执行恶意代码
2. 签名绕过
└── 动态加载的代码不受APK签名保护
└── 只有原始APK在安装时进行签名验证
└── 插件DEX可能来自不可信来源
3. 权限逃逸
└── 插件运行在宿主进程中
└── 共享宿主应用的所有权限
└── 恶意插件可以利用宿主的权限
4. 审核绕过
└── 上架审核时提交合规版本
└── 上架后通过热修复替换为违规代码
└── Google Play明确禁止此行为
防护措施:
├── DEX文件签名校验(自定义签名,不依赖APK签名)
├── HTTPS传输 + 证书固定(Certificate Pinning)
├── DEX文件加密存储
├── 代码混淆增加逆向难度
└── 运行时完整性校验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 14.2 Google Play的政策限制
Google Play关于动态代码加载的政策:
明确禁止:
├── 从非Google Play来源下载可执行代码
├── 通过动态加载执行在审核时不存在的功能
└── 使用热修复绕过应用审核
允许的场景:
├── 动态加载在APK中已包含的DEX
├── WebView加载网页内容
└── 脚本引擎(如Lua、JavaScript)执行有限功能
国内市场:
├── 政策相对宽松
├── 热修复广泛使用(微信Tinker、美团Robust等)
└── 但同样需要注意安全风险
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 14.3 插件化的替代方案
现代替代方案:
1. Dynamic Feature Module(Android App Bundle)
└── Google官方的模块动态加载方案
└── 通过Play Store按需下载功能模块
└── 受签名保护,安全性好
└── 但只能在Google Play分发
2. Instant App
└── 无需安装即可使用的App模块
└── 基于Android App Bundle
3. 小程序化
└── 使用WebView/小程序容器
└── 动态下发前端代码
└── 在受控的沙箱中执行
4. 配置下发
└── 通过Remote Config下发配置
└── 不下发可执行代码
└── 通过配置开关控制功能
└── 安全性最好
选型建议:
海外应用 → Dynamic Feature Module
国内应用 → Tinker/Robust(需要做安全加固)
轻量级更新 → 配置下发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 十五、总结
插件化与热修复知识图谱:
类加载机制
├── ClassLoader体系 → PathClassLoader/DexClassLoader
├── 双亲委派 → 加载顺序保证
└── dexElements → 类查找路径
插件化
├── 代码加载 → DexClassLoader
├── 资源加载 → AssetManager.addAssetPath
├── 组件管理 → Hook Instrumentation/AMS
└── 占坑机制 → 预注册StubActivity
热修复
├── 类替换 → dexElements前插(Tinker)
├── 方法替换 → ArtMethod替换(AndFix)
├── 代码分发 → 方法前置路由(Robust)
└── 资源修复 → 替换AssetManager
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
上次更新: 2026/06/10, 11:13:41