Android 热修复技术原理与主流方案对比
Android 热修复技术允许在不重新发版的情况下修复线上 Bug。解析了热修复的定义、必要性及主流方案选择策略。涵盖阿里系(Sophix)、腾讯系(Tinker)及美团(Robust)等方案的原理对比。详细介绍了 NativeHook、JavaHook、MultiDex、Dex 替换、资源修复及 SO 库修复的技术实现细节。同时讨论了版本管理分支策略与分发监控的重要性,帮助开发者掌握热修复本质并应用于实际项目。

Android 热修复技术允许在不重新发版的情况下修复线上 Bug。解析了热修复的定义、必要性及主流方案选择策略。涵盖阿里系(Sophix)、腾讯系(Tinker)及美团(Robust)等方案的原理对比。详细介绍了 NativeHook、JavaHook、MultiDex、Dex 替换、资源修复及 SO 库修复的技术实现细节。同时讨论了版本管理分支策略与分发监控的重要性,帮助开发者掌握热修复本质并应用于实际项目。

热修复技术是当下 Android 开发中比较高级和热门的知识点,是中级开发人员通向高级开发中必须掌握的技能。目前 Android 业内,热修复技术百花齐放,各大厂都推出了自己的热修复方案,使用的技术方案也各有所异,当然各个方案也都存在各自的局限性。希望通过本文的梳理阐述,了解这些热修复方案的对比及实现原理,掌握热修复技术的本质,同时也能应用实践到实际项目中去。
简单来讲,为了修复线上问题而提出的修补方案,程序修补过程无需重新发版!

在正常软件开发流程中,线下开发->上线->发现 bug->紧急修复上线。不过对于这种方式代价太大,而且永远避免不了面临如下几个问题:
而相对比之下,热修复的开发流程就显得更加灵活,无需重新发版,实时高效热修复,无需下载新的应用,代价小,最重要的是及时的修复了 bug。而且随着热修复技术的发展,现在不仅可以修复代码,同时还可以修复资源文件及 SO 库。

文章开篇就说了现在各大厂都推出了自己的热修复方案,那么我们到底该如何去选择一套适合自己的热修复技术去学习呢?接下来我将从现在主流热修复的方案对比来给予你答案。
| 名称 | 说明 |
|---|---|
| AndFix/HotFix | 开源,实时生效 |
| Sophix | 阿里百川,未开源,免费、实时生效 |
| 其他 | 未开源,商业收费,实时生效/冷启动修复 |
HotFix 是 AndFix 的优化版本,Sophix 是 HotFix 的优化版本。目前阿里系主推是 Sophix。
| 名称 | 说明 |
|---|---|
| QZone | QQ 空间,未开源,冷启动修复 |
| Robust | 手 Q 团队,开源,冷启动修复 |
| Tinker | 微信团队,开源,冷启动修复。提供分发管理, |
| 名称 | 说明 |
|---|---|
| Robust | 美团,开源,实时修复 |
| 大众点评 | 开源,冷启动修复 |
| 饿了么 | 开源,冷启动修复 |

怎么选?这个只能说一切看需求。如果公司综合实力强,完全考虑自研都没问题,但需要综合考虑成本及维护。下面给出几点建议:
项目需求
学习及使用成本
选择大厂
推荐选择

NativeHook 的原理是直接在 native 层进行方法的结构体信息对换,从而实现完美的方法新旧替换,从而实现热修复功能。 下面以 AndFix 的一段 jni 代码来进行说明,如下:
void replace_6_0(JNIEnv* env, jobject src, jobject dest) {
// 通过 Method 对象得到底层 Java 函数对应 ArtMethod 的真实地址
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src);
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->class_loader_ =
reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->class_loader_; //for plugin classloader
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->clinit_thread_id_ =
reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->clinit_thread_id_;
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->status_ = reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->status_-1;
//for reflection invoke
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->super_class_ = 0;
//把旧函数的所有成员变量都替换为新函数的
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
smeth->access_flags_ = dmeth->access_flags_ | 0x0001;
smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
smeth->dex_method_index_ = dmeth->dex_method_index_;
smeth->method_index_ = dmeth->method_index_;
smeth->ptr_sized_fields_.entry_point_from_interpreter_ =
dmeth->ptr_sized_fields_.entry_point_from_interpreter_;
smeth->ptr_sized_fields_.entry_point_from_jni_ =
dmeth->ptr_sized_fields_.entry_point_from_jni_;
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
LOGD("replace_6_0: %d , %d",
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);
}
void setFieldFlag_6_0 {
art::mirror::ArtField* artField =
(art::mirror::ArtField*) env->(field);
artField->access_flags_ = artField->access_flags_ & (~) | ;
(, artField->access_flags_);
}
每一个 Java 方法在 art 中都对应一个 ArtMethod,ArtMethod 记录了这个 Java 方法的所有信息,包括访问权限及代码执行地址等。通过 env->FromReflectedMethod 得到方法对应的 ArtMethod 的真正开始地址,然后强转为 ArtMethod 指针,从而对其所有成员进行修改。 这样以后调用这个方法时就会直接走到新方法的实现中,达到热修复的效果。
以美团的 Robust 为例,Robust 的原理可以简单描述为:

下面通过 Robust 的源码来进行分析。 首先看一下打基础包是插入的代码逻辑,如下:
public static ChangeQuickRedirect u;
protected void onCreate(Bundle bundle) {
//为每个方法自动插入修复逻辑代码,如果 ChangeQuickRedirect 为空则不执行
if (u != null) {
if (PatchProxy.isSupport(new Object[]{bundle}, this, u, false, 78)) {
PatchProxy.accessDispatchVoid(new Object[]{bundle}, this, u, false, 78);
return;
}
}
super.onCreate(bundle);
...
}
Robust 的核心修复源码如下:
public class PatchExecutor extends Thread {
@Override
public void run() {
...
applyPatchList(patches);
...
}
/**
* 应用补丁列表
*/
protected void applyPatchList(List<Patch> patches) {
...
for (Patch p : patches) {
...
currentPatchResult = patch(context, p);
...
}
}
/**
* 核心修复源码
*/
protected boolean patch(Context context, Patch patch) {
...
//新建 ClassLoader
DexClassLoader classLoader = new DexClassLoader(patch.getTempPath(), context.getCacheDir().getAbsolutePath(),
null, PatchExecutor.class.getClassLoader());
patch.delete(patch.getTempPath());
...
try {
patchsInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());
patchesInfo = (PatchesInfo) patchsInfoClass.newInstance();
} catch (Throwable t) {
...
}
...
//通过遍历其中的类信息进而反射修改其中 ChangeQuickRedirect 对象的值
for (PatchedClassInfo patchedClassInfo : patchedClasses) {
...
try {
oldClass = classLoader.loadClass(patchedClassName.trim());
Field[] fields = oldClass.getDeclaredFields();
for (Field field : fields) {
if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), oldClass.getCanonicalName())) {
changeQuickRedirectField = field;
;
}
}
...
{
patchClass = classLoader.loadClass(patchClassName);
patchClass.newInstance();
changeQuickRedirectField.setAccessible();
changeQuickRedirectField.set(, patchObject);
} (Throwable t) {
...
}
} (Throwable t) {
...
}
}
;
}
}
Android 内部使用的是 BaseDexClassLoader、PathClassLoader、DexClassLoader 三个类加载器实现从 DEX 文件中读取类数据,其中 PathClassLoader 和 DexClassLoader 都是继承自 BaseDexClassLoader 实现。dex 文件转换成 dexFile 对象,存入 Element[]数组,findclass 顺序遍历 Element 数组获取 DexFile,然后执行 DexFile 的 findclass。源码如下:
// 加载名字为 name 的 class 对象
public Class findClass(String name, List<Throwable> suppressed) {
// 遍历从 dexPath 查询到的 dex 和资源 Element
for (Element element : dexElements) {
DexFile dex = element.dexFile;
// 如果当前的 Element 是 dex 文件元素
if (dex != null) {
// 使用 DexFile.loadClassBinaryName 加载类
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
所以此方案的原理是 Hook 了 ClassLoader.pathList.dexElements[],将补丁的 dex 插入到数组的最前端。因为 ClassLoader 的 findClass 是通过遍历 dexElements[]中的 dex 来寻找类的。所以会优先查找到修复的类。从而达到修复的效果。

下面使用 Nuwa 的关键实现源码进行说明如下:
public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
//新建一个 ClassLoader 加载补丁 Dex
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());
//反射获取旧 DexElements 数组
Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
//反射获取补丁 DexElements 数组
Object newDexElements = getDexElements(getPathList(dexClassLoader));
//合并,将新数组的 Element 插入到最前面
Object allDexElements = combineArray(newDexElements, baseDexElements);
Object pathList = getPathList(getPathClassLoader());
//更新旧 ClassLoader 中的 Element 数组
ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements);
}
private static PathClassLoader getPathClassLoader() {
PathClassLoader pathClassLoader = (PathClassLoader) DexUtils.class.getClassLoader();
return pathClassLoader;
}
private static Object getDexElements(Object paramObject)
throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
ReflectionUtils.getField(paramObject, paramObject.getClass(), );
}
Object
IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
ReflectionUtils.getField(baseDexClassLoader, Class.forName(), );
}
Object {
Class<?> localClass = firstArray.getClass().getComponentType();
Array.getLength(firstArray);
firstArrayLength + Array.getLength(secondArray);
Array.newInstance(localClass, allLength);
( ; k < allLength; ++k) {
(k < firstArrayLength) {
Array.set(result, k, Array.get(firstArray, k));
} {
Array.set(result, k, Array.get(secondArray, k - firstArrayLength));
}
}
result;
}
为了避免 dex 插桩带来的性能损耗,dex 替换采取另外的方式。原理是提供 dex 差量包,整体替换 dex 的方案。差量的方式给出 patch.dex,然后将 patch.dex 与应用的 classes.dex 合并成一个完整的 dex,完整 dex 加载得到 dexFile 对象作为参数构建一个 Element 对象然后整体替换掉旧的 dex-Elements 数组。

这也是微信 Tinker 采用的方案,并且 Tinker 自研了 DexDiff/DexMerge 算法。Tinker 还支持资源和 So 包的更新,So 补丁包使用 BsDiff 来生成,资源补丁包直接使用文件 md5 对比来生成,针对资源比较大的(默认大于 100KB 属于大文件)会使用 BsDiff 来对文件生成差量补丁。
下面我们关键看看 Tinker 的实现源码,当然具体的实现算法很复杂,我们只看关键的实现,最后的修复在 UpgradePatch 中的 tryPatch 方法,如下:
@Override
public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
//省略一堆校验
... ....
//下面是关键的 diff 算法及合并实现,实现相对复杂,感兴趣可以再仔细阅读源码
//we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process
if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
return false;
}
if (!BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch library failed");
return false;
}
if (!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch resource failed");
return false;
}
// check dex opt file at last, some phone such as VIVO/OPPO like to change dex2oat to interpreted
if (!DexDiffPatchInternal.waitAndCheckDexOptFile(patchFile, manager)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, check dex opt file failed");
return false;
}
if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, newInfo, patchInfoLockFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, rewrite patch info failed");
manager.getPatchReporter().onPatchInfoCorrupted(patchFile, newInfo.oldVersion, newInfo.newVersion);
return false;
}
TinkerLog.w(TAG, );
;
}
public static void monkeyPatchExistingResources(Context context,
String externalResourceFile, Collection activities) {
if (externalResourceFile == null) {
return;
}
try {
//反射一个新的 AssetManager
AssetManager newAssetManager = (AssetManager) AssetManager.class
.getConstructor(new Class[0]).newInstance(new Object[0]);
//反射 addAssetPath 添加新的资源包
Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", new Class[]{String.class});
mAddAssetPath.setAccessible(true);
if (((Integer) mAddAssetPath.invoke(newAssetManager,
new Object[]{externalResourceFile})).intValue() == 0) {
throw new IllegalStateException(
"Could not create new AssetManager");
}
Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks", new Class[0]);
mEnsureStringBlocks.setAccessible(true);
mEnsureStringBlocks.invoke(newAssetManager, []);
(activities != ) {
(Activity activity : activities) {
activity.getResources();
{
Resources.class.getDeclaredField();
mAssets.setAccessible();
mAssets.set(resources, newAssetManager);
} (Throwable ignore) {
Resources.class.getDeclaredField();
mResourcesImpl.setAccessible();
mResourcesImpl.get(resources);
resourceImpl.getClass().getDeclaredField();
implAssets.setAccessible();
implAssets.set(resourceImpl, newAssetManager);
}
Resources. activity.getTheme();
{
{
Resources.Theme.class.getDeclaredField();
ma.setAccessible();
ma.set(theme, newAssetManager);
} (NoSuchFieldException ignore) {
Resources.Theme.class.getDeclaredField();
themeField.setAccessible();
themeField.get(theme);
impl.getClass().getDeclaredField();
ma.setAccessible();
ma.set(impl, newAssetManager);
}
ContextThemeWrapper.class.getDeclaredField();
mt.setAccessible();
mt.set(activity, );
ContextThemeWrapper.class.getDeclaredMethod(, []);
mtm.setAccessible();
mtm.invoke(activity, []);
AssetManager.class.getDeclaredMethod(, []);
mCreateTheme.setAccessible();
mCreateTheme.invoke(newAssetManager, []);
Resources.Theme.class.getDeclaredField();
mTheme.setAccessible();
mTheme.set(theme, internalTheme);
} (Throwable e) {
Log.e(,
+ activity, e);
}
pruneResourceCaches(resources);
}
}
Collection references;
(Build.VERSION.SDK_INT >= ) {
Class.forName();
resourcesManagerClass.getDeclaredMethod(, []);
mGetInstance.setAccessible();
mGetInstance.invoke(, []);
{
resourcesManagerClass.getDeclaredField();
fMActiveResources.setAccessible();
(ArrayMap) fMActiveResources.get(resourcesManager);
references = arrayMap.values();
} (NoSuchFieldException ignore) {
resourcesManagerClass.getDeclaredField();
mResourceReferences.setAccessible();
references = (Collection) mResourceReferences.get(resourcesManager);
}
} {
Class.forName();
activityThread.getDeclaredField();
fMActiveResources.setAccessible();
getActivityThread(context, activityThread);
(HashMap) fMActiveResources.get(thread);
references = map.values();
}
(WeakReference wr : references) {
(Resources) wr.get();
(resources != ) {
{
Resources.class.getDeclaredField();
mAssets.setAccessible();
mAssets.set(resources, newAssetManager);
} (Throwable ignore) {
Resources.class.getDeclaredField();
mResourcesImpl.setAccessible();
mResourcesImpl.get(resources);
resourceImpl.getClass().getDeclaredField();
implAssets.setAccessible();
implAssets.set(resourceImpl, newAssetManager);
}
resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}
}
} (Throwable e) {
(e);
}
}
sdk 提供接口替换 System 默认加载 so 库的接口
SOPatchManger.loadLibrary(String libName)
替换
System.loadLibrary(String libName)
SOPatchManger.loadLibrary 接口加载 so 库的时候优先尝试去加载 sdk 指定目录下补丁的 so。若不存在,则再去加载安装 apk 目录下的 so 库
优点:不需要对不同 sdk 版本进行兼容,所以 sdk 版本都是 System.loadLibrary 这个接口
缺点:需要侵入业务代码,替换掉 System 默认加载 so 库的接口
采取类似类修复反射注入方式,只要把补丁 so 库的路径插入到 nativeLibraryDirectories 数组的最前面,就能够达到加载 so 库的时候是补丁 so 库而不是原来 so 库的目录,从而达到修复。
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);
for (NativeLibraryElement element : nativeLibraryPathElements) {
String path = element.findNativeLibrary(fileName);
if (path != null) {
return path;
}
}
return null;
}
优点:不需侵入用户接口调用
缺点:需要做版本兼容控制,兼容性较差
使用热修复技术后由于发布流程的变化,肯定也需求采用相应的分支管理进行控制。
通常移动开发的分支管理采用特性分支,如下:
| 分支 | 描述 |
|---|---|
| master | 主分支(只能 merge,不能 commit,设置权限),用于管理线上版本,及时设置对应 Tag |
| dev | 开发分支,每个新版本的研发根据版本号基于主分支创建,测试通过验证后,上线合入 master 分支 |
| function X | 功能分支,按需求设定。基于开发分支创建,完成功能开发后合入 dev 开发分支 |
接入热修复后,推荐可参考如下分支策略:
| 分支 | 描述 |
|---|---|
| master | 主分支(只能 merge,不能 commit,设置权限),用于管理线上版本,及时设置对应 Tag(一般 3 位版本号) |
| hot_fix | 热修复分支。基于 master 分支创建,修复紧急问题后,测试推送后,将 hot_fix 再合并到 master 分支。再次为 master 分支打 tag。(一般 4 位版本号) |
| dev | 开发分支,每个新版本的研发根据版本号基于主分支创建,测试通过验证后,上线合入 master 分支 |
| function X | 功能分支,按需求设定。基于开发分支创建,完成功能开发后合入 dev 开发分支 |
注意热修复分支的测试及发布流程应用正常版本流程一致,保证质量。
目前主流的热修复方案,像 Tinker 及 Sophix 都会提供补丁的分发及监控。这也是我们选择热修复技术方案需要考虑的关键因素之一。毕竟为了保证线上版本的质量,分发控制及实时监测必不可少。
想要深入了解热修复,需要了解类加载机制,Instant Run,multidex 以及 java 底层实现细节,JNI,AAPT 和虚拟机的知识,需要庞大的知识贮备才能进行深入理解,当然 Android Framwork 的实现细节是非常重要的。熟悉热修复的原理有助于我们提供自己的编程水平,提升自己解决问题的能力。热修复不是简单的客户端 SDK,它还包含了安全机制和服务端的控制逻辑,整条链路也不是短时间可以快速完成的。开发者应结合项目实际情况,选择合适的热修复方案并建立完善的监控体系。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online