90、热修复技术:Tinker、Sophix原理、补丁生成、加载与回退
热修复,说白了就是给线上App打补丁。
不用发版本,不用等审核,用户无感知地就把Bug修了。我做了这么多年Android,遇到过最头疼的事就是线上崩溃。用户骂,老板催,发版又来不及。那时候我就想,要是能像Web一样热更新该多好。
嗯,热修复技术就是干这个的。
热修复的核心原理
热修复的本质,是让App在运行时加载修复后的代码或资源,替换掉有问题的部分。市面上主流方案有两大流派:
- 类加载替换方案:代表是腾讯的Tinker。它把修复后的类打包成dex,放到ClassLoader的前面。因为Android加载类时是线性查找,找到第一个就停。所以只要把补丁dex放在前面,就能覆盖掉旧类。
- 底层替换方案:代表是阿里的Sophix。它直接修改ArtMethod或Native指针,把有问题的函数指向修复后的实现。这个方案更彻底,但兼容性风险也更高。
我个人习惯用Tinker,因为它兼容性好,覆盖了Android 4.0到现在的所有版本。Sophix我也用过,修复速度快,但有些厂商的ROM会拦截底层操作,容易翻车。
核心区别一句话总结:
Tinker是「换类」——把整个有问题的类替换掉。
Sophix是「改方法」——只把有问题的函数指针改掉。
补丁生成:怎么把修复打包成补丁
补丁生成,说白了就是「算出差异」。你修复了Bug,编译出新的APK。然后工具对比新旧APK,把变化的部分提取出来,打包成补丁文件。
Tinker的补丁生成流程:
- 编译修复后的APK(新包)
- 用Tinker的gradle插件对比新旧APK
- 生成patch.dex(只包含变化的类)
- 打包成patch.zip,上传到服务器
Sophix的补丁生成流程:
- 在Sophix控制台上传新APK
- 后台自动对比基准包,生成补丁
- 补丁可以精确到方法级别,体积更小
我的经验:补丁体积一定要控制。我曾经打过一个补丁,因为包含了资源文件,结果有2MB。用户下载慢,成功率低。后来我改成只打代码补丁,资源单独下发,体积降到了200KB。嗯,用户体验好了很多。
补丁加载:什么时候加载,怎么加载
补丁加载的时机很关键。你想想看,如果App正在运行,你突然把类换了,那正在使用的对象怎么办?
所以,加载时机一般选在App冷启动时。具体流程是这样的:
- App启动,Application.onCreate()里检查是否有补丁
- 如果有,下载补丁文件到本地
- 校验补丁的签名和MD5,防止被篡改
- 调用Tinker/Sophix的API加载补丁
- 加载成功后,下次启动时补丁生效
Sophix支持即时生效,因为它改的是方法指针,不需要重启。但Tinker必须重启App才能生效,因为类加载器在App启动后就不能换了。
注意:Sophix的即时生效在Android 7.0以上有坑。因为Art虚拟机对方法内联做了优化,你改了指针,但内联后的代码可能还在缓存里。我遇到过这个问题,最后只能强制走冷启动修复。
补丁回退:出了问题怎么办
补丁也可能有Bug。你想想看,线上环境千奇百怪,测试没覆盖到的情况太多了。
回退策略一般有三种:
| 回退方式 | 原理 | 适用场景 |
|---|---|---|
| 版本号回退 | 服务器下发回退指令,App删除补丁文件 | 补丁有明显Bug,需要全局回退 |
| 本地回退 | App启动时检测到补丁加载失败,自动删除 | 补丁损坏或兼容性问题 |
| 灰度回退 | 只对部分用户回退,观察效果 | 不确定是补丁问题,先小范围验证 |
我曾经遇到过最坑的事:补丁打上去后,某个机型直接闪退。原因是那个机型的ClassLoader实现有差异,补丁dex加载时抛了异常。还好我提前做了本地回退逻辑,App启动时检测到加载失败,自动删除了补丁,用户重启后就好了。
回退的核心原则:
宁可让Bug继续存在,也不能让App打不开。补丁加载失败时,一定要有兜底逻辑。
代码示例:Tinker集成与补丁加载
下面是一个简单的Tinker集成示例。注意,这只是核心逻辑,实际项目还要处理更多细节。
// Application中初始化Tinker
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 初始化Tinker
TinkerInstaller.install(this);
// 检查是否有补丁需要加载
checkAndLoadPatch();
}
private void checkAndLoadPatch() {
// 从SharedPreferences读取补丁信息
String patchPath = getPatchPathFromLocal();
if (patchPath != null) {
File patchFile = new File(patchPath);
if (patchFile.exists()) {
// 加载补丁
TinkerInstaller.onReceiveUpgradePatch(
getApplicationContext(),
patchPath
);
}
}
}
// 下载补丁(从服务器获取)
public void downloadPatch(String url) {
// 使用OkHttp或DownloadManager下载
// 下载完成后保存到指定路径
// 调用TinkerInstaller.onReceiveUpgradePatch()
}
}
避坑指南:补丁文件一定要做完整性校验。我曾经遇到过下载了一半就触发加载的情况,结果App直接崩溃。后来我加了MD5校验,下载完成后比对MD5,不一致就删除重下。
Sophix与Tinker的选择建议
这两个方案我都用过,说说我的感受:
- 选Tinker的场景:你的App兼容性要求高,需要覆盖各种Android版本和厂商ROM。Tinker的类加载方案经过了多年验证,翻车概率低。
- 选Sophix的场景:你对修复速度要求高,希望补丁即时生效。Sophix的底层替换方案在大部分机型上表现不错,但要做好回退预案。
我个人习惯是:核心功能用Tinker,紧急修复用Sophix。嗯,两个一起用也不是不行,但要小心冲突。
最后说一句:热修复不是万能的。它只能修代码逻辑,修不了Native层的问题,也修不了Android系统本身的Bug。而且,热修复本身也有风险——补丁加载失败、兼容性问题、性能损耗。所以,能发版本解决的,尽量发版本。热修复,是最后一道防线。
公众号:蓝海资料掘金营,微信deep3321