90、热修复技术:Tinker、Sophix原理、补丁生成、加载与回退

热修复,说白了就是给线上App打补丁。

不用发版本,不用等审核,用户无感知地就把Bug修了。我做了这么多年Android,遇到过最头疼的事就是线上崩溃。用户骂,老板催,发版又来不及。那时候我就想,要是能像Web一样热更新该多好。

嗯,热修复技术就是干这个的。

热修复的核心原理

热修复的本质,是让App在运行时加载修复后的代码或资源,替换掉有问题的部分。市面上主流方案有两大流派:

  • 类加载替换方案:代表是腾讯的Tinker。它把修复后的类打包成dex,放到ClassLoader的前面。因为Android加载类时是线性查找,找到第一个就停。所以只要把补丁dex放在前面,就能覆盖掉旧类。
  • 底层替换方案:代表是阿里的Sophix。它直接修改ArtMethod或Native指针,把有问题的函数指向修复后的实现。这个方案更彻底,但兼容性风险也更高。

我个人习惯用Tinker,因为它兼容性好,覆盖了Android 4.0到现在的所有版本。Sophix我也用过,修复速度快,但有些厂商的ROM会拦截底层操作,容易翻车。

核心区别一句话总结:

Tinker是「换类」——把整个有问题的类替换掉。

Sophix是「改方法」——只把有问题的函数指针改掉。

热修复两大方案原理对比 Tinker 方案(类加载替换) PathClassLoader (加载补丁dex优先) 补丁dex(修复后的类) 优先被加载,覆盖旧类 旧dex(有Bug的类)被忽略 Sophix 方案(底层替换) ArtMethod 结构体 (存储函数入口地址) 修改指针 → 指向修复函数 运行时直接跳转新代码 旧函数不再被调用

补丁生成:怎么把修复打包成补丁

补丁生成,说白了就是「算出差异」。你修复了Bug,编译出新的APK。然后工具对比新旧APK,把变化的部分提取出来,打包成补丁文件。

Tinker的补丁生成流程:

  1. 编译修复后的APK(新包)
  2. 用Tinker的gradle插件对比新旧APK
  3. 生成patch.dex(只包含变化的类)
  4. 打包成patch.zip,上传到服务器

Sophix的补丁生成流程:

  1. 在Sophix控制台上传新APK
  2. 后台自动对比基准包,生成补丁
  3. 补丁可以精确到方法级别,体积更小

我的经验:补丁体积一定要控制。我曾经打过一个补丁,因为包含了资源文件,结果有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