热修复框架--Tinker
公司项目要接入热修复框架,让我去做一下预研,做完分享了,现在记录下来做一下总结。
为什么用热修复框架
大家都知道,每一次 App 发版都是一次漫长的流程,上线成功以后。用户接到打开 App 接收到更新通知时还不一定更新。
好不容易用户更新了,因为测试的疏忽或者其他玄学的问题,出现了大面积的紧急 Bug ,这时候又要经历发版流程修复 Bug。对自己和用户都不是一个好的体验。
这时候轮到热修复登场了,当我们需要修复线上紧急 Bug ,或者一些小的迭代时,可以以补丁的方式推送。它对于用户来说是无感知的,不用等重新安装 Apk。
为什么用 Tinker
其实我试了 alibaba 的「Sophix」、美团点评的「Robust」、微信的「Tinker」三个比较全面的热修复方案。
| 比较 | Tinker | Robust | Sophix |
|---|---|---|---|
| 接入复杂度 | 复杂 | 中等 | 简单 |
| 修复成功率 | 较高 | 最高 | 较高 |
| 类、资源、So 替换 | yes | no(内测) | yes |
| 补丁包大小 | 较小 | 较小 | 最小 |
| 代码维护成本 | 不变 | 增加 | 不变 |
| wiki 支持 | 完善 | 混乱 | 完善 |
| 兼容性 | Android 2.x ~ 7.x | Android 2.x ~ 7.x | Android 2.x ~ 7.x |
| 性能损耗 | 较小 | 较小 | 较小 |
| 即时生效 | no | yes | no(少数情况 yes) |
| 加固 | 部分 | 支持 | 阿里云加固 |
可以看出来三者各有胜场,都不是十全十美的。其实我我测试下来,觉得「Sophix」是使用起来最优雅的,它接入简单,有专门的补丁生成工具。
那我为什么不使用它呢?因为「Sophix」不是开源的,想要在你项目接入它,必须接入「阿里云」的后台,而它是有免费限额的,超过限额就要收费,你懂得,公司在能不花钱的地方绝对不会花钱的。
而我为什么不用「Robust」呢?它修复成功率最高啊。注意在表格中提到「代码维护成本」一项,在其它两项不增加成本的情况下,「Robust」增加了代码维护成本。
如果你看过「Robust」的修复方法就知道,你要在你修复的方法前面加「@Modify」或者在方法体内调用「RobustModify.modify()」来,或者添加类的时候使用 @Add。这为什么会增加维护成本呢?首先,我们在正常开发工作时,是不需要这些注解的,只有当我们要修复线上 BUG(或者迭代小功能)去生成「补丁 1」时才会使用,那我们生成补丁后呢?是不是还是要去掉这些注解,把他 merge 到正常的开发代码里去。
可是,在下发补丁以后,又发现了一个 BUG(或者迭代小功能),又要生成一个「补丁 2」,又因为你生成补丁是基于线上 Apk 来比较获得的,所以你的「补丁 2」必须包括「补丁 1」的内容,可是这时候你已经把「补丁 1」的注解已经去掉了,那就不得不把注解加回来,等到情况复杂起来,你会记得你之前修复了哪些方法吗?更不用说线上用户安装的版本参差不齐的。
有点冗长了,接下来讲 Tinker 。
Tinker 接入建议
Tinker 的接入方法我这里就不多说了,可以查看「Tinker 接入指南」。
这里根据我接入的经验提出几个建议。
tinkerId
tinkerId 是用于校验基准 APK 的重要标识,只有它们匹配时才能打上补丁。官方建议使用 git 版本号,或是 versionName 等等。
这里我们改造一下官方的配置,将版本后在「gradle.properties」文件中统一管理:
1 | # Tinker 版本号 |
然后在 app的「buidl.gradle」中引用:
1 | //省略无关代码 |
这样,每次更新版本的时候,就不怕两个值不同了。
优雅的使补丁生效
Tinker 是不支持即使生效的,需要在补丁成功以后重启 App 才能生效。在默认的实现中,在补丁成功以后,会粗暴的把 App 进程杀掉,这显然是不够优雅的。但是等用户关掉 App ,又太慢。我们有希望补丁能尽快生效又让用户没有什么感知。
查看 Tinker 源代码可以知道,杀掉进程的代码在「DefaultTinkerResultService」中,所以我们要自己重写「Tinker 自定义扩展」中的方法。
我们可以在代码中维护一个全局的 boolean 类型变量,默认为 false,当补丁完成以后,在自定义的「ResultService」中相关代码将这个变量赋值为 true。接下来,我们监听 App 进入后台(按下 HOME 键、切换 App)时,如果这个变量为 true 时,就将 App 进程杀掉,这样既不会让用户以为 App 闪退了,又能尽快的使补丁生效。
补丁版本管理
除了 tinkerId 用来匹配基准 Apk,补丁本身也有「patchVersion」,用于表示补丁的新旧,新补丁是包含就旧补丁(相同 tinkerId)的内容的。可以用「packageConfig」中的「configField(key,value)」来管理设置补丁版本。然后使用
1 | Tinker.with(this).getTinkerLoadResultIfPresent().getPackageConfigByName(key) |
获取当前补丁版本,当新获取的补丁版本大于已装版本(或未装补丁)时,开始加载补丁。
APK 发版流程
因为加入了热修复方案,传统的发版流程上会受到一些影响。
多渠道打包
关于多渠道打包,为了可以支持热修复补丁,无法使用第三方加固工具来生成多渠道包。所以现在有以下两种方案:
- gradle 的 productFlavor 方案
- 美团的「walle」方案
首先说说「productFlavor」方案,它会在打包过程中生成你设置好的 N 个渠道包,然后你需要把每个包都用加固工具加固一遍,在分发到对应的应用市场上面。最可怕的不是这个,最可怕的是,如果你想推送热修复补丁的话,通过执行「tinkerPatchAllFlavorXXXXX」来生成每个渠道包对应的补丁,然后全部传到服务器下发,想想都挺恐怖。
所以这里使用「Tinker 常见问题 – 如何兼容多渠道包?」中提到的「walle」
它可以有效的解决第一种方案的问题,使用一个补丁包来修复所有的渠道包。
优化后的打包流程是这样的:
具体可以看看这篇文章
集成tinker后使用360加固并使用walle进行多渠道打包
后记
我始终说是「热修复」而不说是「热更新」,是因为我觉得它只能是一种更新的补充方案,而不是完全替代传统更新方式的方案。
一方面是因为从技术上来讲,现在市面上的所有热修复框架都无法做到完全替代传统更新方式,另一方面这个方式始终是不符合 Google Play 的发布规范的。
热修复框架--Tinker