热修复框架--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
2
3
4
# Tinker 版本号
TINKER_VERSION=1.8.1
versionName = 1.0.0
versionCode = 1

然后在 app的「buidl.gradle」中引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//省略无关代码
Properties gradle = new Properties();
gradle.load(new FileInputStream(file("../gradle.properties")));
String configVersionName = gradle.getProperty("versionName");
int configVersionCode = Integer.parseInt(gradle.getProperty("versionCode"));
...
defaultConfig {
...
versionCode configVersionCode
versionName configVersionName
...
}
def gitSha() {
return "${versionName}"
}

这样,每次更新版本的时候,就不怕两个值不同了。

优雅的使补丁生效

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 发版流程

因为加入了热修复方案,传统的发版流程上会受到一些影响。

多渠道打包

关于多渠道打包,为了可以支持热修复补丁,无法使用第三方加固工具来生成多渠道包。所以现在有以下两种方案:

  1. gradle 的 productFlavor 方案
  2. 美团的「walle」方案

首先说说「productFlavor」方案,它会在打包过程中生成你设置好的 N 个渠道包,然后你需要把每个包都用加固工具加固一遍,在分发到对应的应用市场上面。最可怕的不是这个,最可怕的是,如果你想推送热修复补丁的话,通过执行「tinkerPatchAllFlavorXXXXX」来生成每个渠道包对应的补丁,然后全部传到服务器下发,想想都挺恐怖。

所以这里使用「Tinker 常见问题 – 如何兼容多渠道包?」中提到的「walle
它可以有效的解决第一种方案的问题,使用一个补丁包来修复所有的渠道包。

优化后的打包流程是这样的:

具体可以看看这篇文章
集成tinker后使用360加固并使用walle进行多渠道打包

后记

我始终说是「热修复」而不说是「热更新」,是因为我觉得它只能是一种更新的补充方案,而不是完全替代传统更新方式的方案。

一方面是因为从技术上来讲,现在市面上的所有热修复框架都无法做到完全替代传统更新方式,另一方面这个方式始终是不符合 Google Play 的发布规范的。

作者

ChinnSenn

發表於

2017-08-25

更新於

2023-04-20

許可協議

評論