腾讯Tinker 热修复 Andriod studio 3.0 多渠道打包和发布补丁方式推荐
本文说明
在之前我已经分享了Tinker 热修复的 Andriod studio3.0 初次配置和集成,时隔这么久来写一下我对Thinker多渠道打包的理解和记录,希望对大家有帮助。这篇文章写的我觉得很浅对于新手完全ok,所以有大佬有更好的理解也可以留言和推荐,毕竟我能力有限哈。为啥这么久没写因为我入职了很多时间去熟悉公司业务了,没有时间。哈哈哈。下一篇将结合腾讯Buly完整实现热修复 集成-补丁-下发-统计。
关于多渠道
为什么多渠道
1 统计用户安装APP来源
知道多渠道的意义就不难理解多渠道打包主要是为了我们统计分析用的。所以,这样就很清楚了我最初的想法不是不行,而是不好,对今后的产品分析没有任何帮助。
2.批量修改生成的apk文件名
根据运营给的命名规则,如果是一个个的右键-重命名,那15个还好,要真是有1000个,运营同事一定会拿刀找我的…而多渠道打包所有的都自动生成。
3.可更改包名
如果有生成不同包名的需求,通过gradle就可以解决。
4.生成不同应用名称或图标
有的时候你会在不同平台看到XX-小米版,XX-魅族版等等,或者beta版的图标和正式的不一样,其实代码还是那个代码,无非做点小小的改动。
多渠道的方式
按原理分
大概有两种,一个是通过gradle,另一个是美团介绍的只打一个包,然后解压替换文件。按打包方式
美团的Walle
360打包
多Flavor打包
python方式打包
区别的话是打包速度和兼容性这里不做分析
本文使用的是美团的Walle
Walle(瓦力):Android Signature V2 Scheme签名下的新一代渠道包打包神器
参考地址 https://tech.meituan.com/mt-apk-packaging.html
https://github.com/Meituan-Dianping/walle
Walle 使用和配置
Gradle插件使用方式
1 配置build.gradle
在位于项目的根目录 build.gradle 文件中添加Walle Gradle插件的依赖, 如下:
buildscript {
dependencies {
classpath 'com.meituan.android.walle:plugin:1.1.5'
}
}
2 并在当前App的 build.gradle 文件中apply这个插件,并添加上用于读取渠道号的AAR
apply plugin: 'walle'
dependencies {
compile 'com.meituan.android.walle:library:1.1.5'
}
3 配置插件
walle {
// 指定渠道包的输出路径
apkOutputFolder = new File("${project.buildDir}/outputs/channels");
// 定制渠道包的APK的文件名称
apkFileNameFormat = '${appName}-${packageName}-${channel}-${buildType}-v${versionName}-${versionCode}-${buildTime}.apk';
// 渠道配置文件
channelFile = new File("${project.getProjectDir()}/channel")
}
4 配置项具体解释:
apkOutputFolder:指定渠道包的输出路径, 默认值为new File(“${project.buildDir}/outputs/apk”)
apkFileNameFormat:定制渠道包的APK的文件名称, 默认值为’ appName− {buildType}-${channel}.apk’
可使用以下变量:
projectName - 项目名字
appName - App模块名字
packageName - applicationId (App包名packageName)
buildType - buildType (release/debug等)
channel - channel名称 (对应渠道打包中的渠道名字)
versionName - versionName (显示用的版本号)
versionCode - versionCode (内部版本号)
buildTime - buildTime (编译构建日期时间)
fileSHA1 - fileSHA1 (最终APK文件的SHA1哈希值)
flavorName - 编译构建 productFlavors 名
- channelFile:包含渠道配置信息的文件路径。
5 如何获取渠道信息
在需要渠道等信息时可以通过下面代码进行获取
String channel = WalleChannelReader.getChannel(this.getApplicationContext());
6 如何生成渠道包
生成渠道包的方式是和assemble${variantName}Channels指令结合,渠道包的生成目录默认存放在 build/outputs/apk/,也可以通过walle闭包中的apkOutputFolder参数来指定输出目录
用法示例:
生成渠道包 ./gradlew clean assembleReleaseChannels
支持 productFlavors ./gradlew clean assembleMeituanReleaseChannels
7 更多用法请移步 https://github.com/Meituan-Dianping/walle
一个补丁修复所有渠道
我们为什么使用Walle 来打包而不用productFlavors
首先使用 productFlavors 用它来打渠道包是一个非常低效的做法,因为它每一次都会走编译流程,你想一下如果每打一个渠道包就要走一下编译流程,100个渠道包那得多慢。
如果你要针对多渠道进行打补丁 你可能会回答,那就针对不同的渠道包进行打补丁。可是这是多么低效。另外你配置渠道超过5个的话,那么就意味着你要一个补丁,一个补丁上传到Bugly补丁管理后台,况且我们也只允许同时下发5个版本的补丁。这里提一下为什么要上传所有渠道的补丁,因为通过productFlavors配置,会修改buildConfig类中的FLAVOR字段,这会导致生成的不同渠道包的dex是不一样的,所以只能针对具体渠道进行打补丁。这就非常的尴尬了
我们使用Walle就没有这些问题了
只需要上传补丁包到补丁管理后台,然后下发即可。
关于Thinker打包和补丁下发的一下常见问题
我应该使用哪个作为补丁包下发,如何做多次修复?
patch_signed_7zip.apk是已签名并且经过7z压缩的补丁包,但是你最好重命名一下,不要让它以.apk结尾,这是因为有些运营商会挟持以.apk结尾的资源。
另外一点,我们在发起补丁请求时,需要先将补丁包先拷贝到dataDir中。因为在sdcard中,补丁包是极其容易被清理软件删除。这里可以参考UpgradePatchRetry.java的实现。
对于补丁包的版本问题,我们可以在packageConfig中增加,例如sample中的
packageConfig {
/**
* patch version via packageConfig
*/
configField("patchVersion", "1.0")
}
- Tinker支持对同一基准版本做多次补丁修复,在生成补丁时,oldApk依然是已经发布出去的那个版本。即补丁版本二的oldApk不能是补丁版本一,它应该依然是用户手机上已经安装的基准版本。
如何对Library文件作补丁?
当前我们并没有直接将补丁的lib路径添加到DexPathList中,理论上这样可以做到程序完全没有感知的对Library文件作补丁。这里主要是因为在多abi的情况下,某些机器获取的并不准确。当前对Library文件作补丁可参考Tinker API概览,这里以后需要考虑优化。
另外一方面,对于第三方库文件的加载我们无法干预,但是只要在我们的代码提前加载第三方的库文件即可。不过这里确保我们使用的是同一个classloader来加载。
无论是对Library还是Application,我们都是采用尽量少去反射的策略,这也是为了提高Tinker框架的兼容性。上线前,我们应当严格测试补丁是否正确加载了修改后的So库。不使用反射的另外一个好处是我们可以做的工作更多,例如加载前验证它的MD5。
如何对资源文件作补丁,为什么有时候会提示大量没有改变的图片发生变更?
Tinker采用全量合成方式实现资源替换,这里有以下几点是使用者需要明确的:
remoteView是无法修改,例如transition动画,notification icon以及桌面图标;
对于资源文件的更新(尤其是assets),需要注意代码中是否采用直接读取sourceApk路径方式读取,这样方式是无法更新的;Tinker只会将满足res pattern的资源放在最后的合成补丁资源包中。一般为了减少合成资源大小,我们不建议输入classes.dex或lib文件的pattern;
若一个文件:assets/classes.dex, 它既满足dex pattern, 又满足res pattern。Tinker只会处理dex pattern, 然后在合成资源包会忽略assets/classes.dex的变更。library也是如此。
只要资源发生变成的前提下我们才会合成新的资源包,这一定程度会增加占Rom体积,请在考虑后使用。
Waringing:若出现资源变更,我们需要使用applyResourceMapping方式编译,这样不仅可以减少补丁包大小,同时防止remote view id变更造成的异常情况。
最后我们应该查看编译过程中生成的resources_out.zip是否满足我们的要求。
有时候会发现大量明明没有改变的png发现变更,解压发现的确两次编译这些png的md5不一致。经分析,aapt在其中一次编译将png优化成8-bit,另外一次却没有,从而导致png改变了。如果你们app出现了这种情况,我们建议关闭aapt对png的优化:
aaptOptions{
cruncherEnabled false
}
若你对安装包大小非常care,可以提前使用命令行工具将所有图片手动优化一次。我们也可以选择一些有损压缩工具,获得更大的压缩效果。
如果你确认png并没有修改,你可以在tinker的配置使用ignoreChange来忽略所有png文件的修改。
res {
ignoreChange = ["*.png"]
}
Tinker中的dex配置’raw’与’jar’模式应该如何选择?
它们应该说各有优劣势,大概应该有以下几条原则:
- 如果你的minSdkVersion小于14, 那你务必要选择’jar’模式;
- 以一个10M的dex为例,它压缩成jar大约为4M,即’jar’模式能节省6M的ROM空间。
- 对于’jar’模式,我们需要验证压缩包流中dex的md5,这会更耗时,在小米2S上数据大约为’raw’模式126ms, ‘jar’模式为246ms。
因为在合成过程中我们已经校验了各个文件的Md5,并将它们存放在/data/data/..目录中。默认每次加载时我们并不会去校验tinker文件的Md5,但是你也可通过开启loadVerifyFlag强制每次加载时校验,但是这会带来一定的时间损耗。
简单来说,’jar’模式更省空间,但是运行时校验的耗时大约为’raw’模式的两倍。如果你没有打开运行时校验,推荐使用’jar’模式。
tinker是否兼容加固?
需要集成升级SDK版本1.3.0以上版本才支持加固。
经过测试的加固产品:
腾讯乐固
爱加密
梆梆加固
360加固(SDK 1.3.1之后版本支持)
其他产品需要大家进行验证。
tinker的一般模式需要Dex的合成,它并不支持加固,一定要使用加固的app可以使用usePreGeneratedPatchDex模式。由于加固会改变apk的dex结构,所以生成补丁包时我们务必要使用加固前的apk。
但是需要注意的是,某些加固工具会将非exported的四大组件的类名替换,对于这部分类即使使用usePreGeneratedPatchDex也无法修改。对于360加固,MainActivity由于被提前加载,也无法修复。大家对于加固的情况,请仔细测试,能否支持与加固的方式有关联。
Google Play版本是否可以有Tinker相关代码?
- 由于Google play的使用者协议,对于GP渠道我们不能使用Tinker动态更新代码,这里会存在应用被下架的风险。但是在Google play版本,我们依然可以存在Tinker的相关代码,但是我们需要屏蔽补丁的网络请求与合成相关操作。
tinkerId应该如何选择?
tinkerId是用了区分基准安装包的,我们需要严格保证一个基准包的唯一性。在设计的初期,我们使用的是基准包的CentralDirectory的CRC,但某些APP为了生成渠道包会对安装包重新打包,导致不同的渠道包的CentralDirectory并不一致。
编译补丁包时,我们会自动读取基准包AndroidManifest的tinkerId作为package_meta.txt中的TINKER_ID。将本次编译传入的tinkerId, 作为package_meta.txt中的NEW_TINKER_ID。当前NEW_TINKER_ID并没有被使用到,只是保留作为配置项。如果我们使用git rev作为tinkerid, 这时只要使用git diff TINKER_ID NEW_TINKER_ID即可获得所有的代码差异。
我们需要保证tinkerId一定是要唯一性的,这里推荐使用git rev或者svn rev. 如果我们升级了客户端版本,但tinkerId与旧版本相同,会导致可能会加载旧版本的补丁。这里我们一定要注意,升级可客户端版本,需要更新tinkerId!
如何使生成的补丁包更小?
对于代码来说,我们最好记住以下几条规则:
编译补丁包时,proguard使用applymapping模式;
对于多dex的情况,保持原本的分包规则,尽量减少由于分包变化而带来的变更。在生成补丁包过程中,对于class分包的变化将会输出Warning:Class Moved日志, 我们应该尽量减少这种变化;
大量静态常量的改变与资源R文件的变更,这里我们推荐使用applyResouceMapping方式保持资源ID。大量类分包的改变对补丁包的影响不大,但是对于合成的时间消耗与占ROM的体积影响更大。我们每次生成补丁后,都应该查看TinkerPatch输出文件夹的日志;
其他的例如使用force jumbo模式以及使用7zip压缩补丁包。
Tinker的最佳实践?
为了使补丁的成功率更高,我们在Sample中还做了以下工作:
由于合成进程可能被各种原因杀死,使用UpgradePatchRetry.java来做重试功能,提高成功率;
防止补丁后程序无法启动,使用SampleUncaughtExceptionHandler.java做crash启动保护。这里更推荐的是进入安全模式,使用配置的方式强制清理或者升级补丁;
为了防止BuildConfig的改变导致大量类的变更,使用BuildInfo.java非final的变量来中转。
为了加快补丁应用同时保持用户体验,在SampleResultService.java在应用退入后台或手机灭屏时,才杀掉进程。你也可以在杀掉进程前,直接通过发送broadcast或service intent的方式尽快的重启进程。
把jumboMode打开,防止由于字符串增多导致force-jumbol,导致更多的变更。
使用zip comment方式生成渠道包。