Tinker 官网
Tinker GitHub 主页
Tinker 常见问题
大概目录:
* Tinker 编译相关问题?
* Tinker 库中有什么类是不能修改的?
* 什么类需要放在主 dex 中?
* 我应该使用哪个作为补丁包下发,如何做多次修复?
* 如何对 Library 文件作补丁?
* 如何对资源文件作补丁,为什么有时候会提示大量没有改变的图片发生变更?
* Tinker 中的 dex 配置 'raw' 与 'jar' 模式应该如何选择?
* 如何兼容多渠道包?
* tinker 是否兼容加固?
* Google Play 版本是否可以有 Tinker 相关代码?
* tinker 与 instant run 的兼容问题?
* 每次编译我应该保留哪些文件,如何兼容 AndResGuard?
* tinkerId 应该如何选择?
* 如何使生成的补丁包更小?
* 关于使用的 ClassLoader 问题?
* 什么时候调用 installTinker?
* Proguard 5.2.1 applymapping 出现 Warning?
* TinkerPatch 补丁管理后台与 Tinker 的关系?
* Tinker 的最佳实践?
一、Tinker 编译相关问题?
编译过程相关的 issue 请先查看是否是以下情况:
无法打开 sample 工程: 请使用单独的 IDE 窗口打开
tinker-sample-android
工程;tinkerId is not set
:是因为没有正确地配置 IDE 的git
路径,若不是通过clone
方式下载tinker
,需要本地手动commit
一次。这里你也可以使用其他字符作为tinkerId
;对于编译与补丁时发生的异常,请到 Tinker 自定义扩展 中查看具体错误码的原因。并通过
"Tinker."
过滤Tinker
相关的日志提交到issue
中;若自定义
TinkerResultService
,请务必将新的Service
添加到Manifest
中;权限问题;请务必已经将读取
sdk
权限添加到AndroidManifest.xml
中,并且已允许权限运行;若使用
DefaultLifeCycle
注解生成 Application,需要将原来 Application 的实现移动到ApplicationLike
中,并将原来的 Application 类删掉;关于 Application 的改造这一块大家比较疑惑,这块请认真阅读自定义 Application 类,大部分的 app 应该都能在半小时内完成改造。
如果出现
Class ref in pre-verified class resolved to unexpected implementation
异常,请确认以下几点:Application中传入ApplicationLike
的参数时是否采用字符串而不是Class.getName
方式;新的 Application 是否已经加入到dex loader pattern
中;额外添加到dex loader pattern
中类的引用类也需要加载到loader pattern
中。
二、Tinker 库中有什么类是不能修改的?
Tinker 库中不能修改的类一共有 26 个,即 com.tencent.tinker.loader.*
类。加上你的 Appliction 类,只有 26 个类是无法通过 Tinker 来修改的。即使类似 Tinker.java
等管理类,也是可以通过 Tinker 本身来修改。
注意,在 1.7.6 版本之前,我们需要手动将不能修改的类添加到 tinkerPatch.dex.loader pattern
中。对于 1.7.6 以后的版本会自动生成。
但是若使用 newApk 或者命令行编译,需要手动添加 Application 类与 loader 类
三、什么类需要放在主 dex 中?
Tinker 并不干涉你分包与多 dex 的加载逻辑,但是你需要确保以下几点:
com.tencent.tinker.loader.*
类,你的 Application 类需要在主 dex,并且已经在dex.loader
中配置;若你自定义了
TinkerLoader
类,你需要将TinkerLoader
的自定义类,以及它用的到类也放在主 dex,并且已经在dex.loader
中配置;ApplicationLike
的继承类也需要放在主 dex 中,但是它无须在dex.loader
中配置,因为它是可以使用 Tinker 修改的类。最后,如果你需要在加载其他 dex 之前加载 Tinker 的管理类,你也可以将com.tencent.tinker.*
都加入到主 dex。你的
ApplicationLike
实现类的直接引用类以及在调用Multidex install
之前加载的类也都需要放到主 dex 中。
注意:Tinker 会自动生成需要放在主 dex 的 keep 规则。在 1.7.6 版本之前,你需要手动将生成规则拷贝到自己的 multiDexKeepProguard
文件中。例如 Sample 中的 multiDexKeepProguard file("keep_in_main_dex.txt")
。在 1.7.6 版本之后,这里会通过脚本自动处理,无须手动填写。
另外,如果 minsdkverion >= 21
,multiDexEnabled
会被忽略。我们可以在 build/intermediates/multi-dex
查找最终的 keep
规则以及结果。
四、我应该使用哪个作为补丁包下发,如何做多次修复?
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 概览,tinker 1.7.7 版本中也提供了一键反射的方案给大家选择。
大家可以根据自己的项目需要选择合适的方案,事实上,无论是对 Library 还是 Application,官方都是采用尽量少去反射的策略,这也是为了提高 Tinker 框架的兼容性。上线前,我们应当严格测试补丁是否正确加载了修改后的 So 库。
六、如何对资源文件作补丁,为什么有时候会提示大量没有改变的图片发生变更?
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 也是如此。只要资源发生变成的前提下 Tinker 才会合成新的资源包,这一定程度会增加占 Rom 体积,请在考虑后使用。
注意:若出现资源变更,我们需要使用 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' 模式
126 ms
,'jar' 模式为246 ms
。
因为在合成过程中 Tinker 已经校验了各个文件的 Md5,并将它们存放在 /data/data/..
目录中。默认每次加载时 Tinker 并不会去校验 tinker 文件的 Md5,但是你也可通过开启 loadVerifyFlag
强制每次加载时校验,但是这会带来一定的时间损耗。
简单来说,'jar' 模式更省空间,但是运行时校验的耗时大约为 'raw' 模式的两倍。如果你没有打开运行时校验,推荐使用 'jar' 模式。
八、如何兼容多渠道包?
关于渠道包的问题,若使用 flavor
编译渠道包,会导致不同的渠道包由于 BuildConfig
变化导致 classes.dex
差异。这里建议的方式有:
将渠道信息写在
AndroidManifest.xml
或文件中,例如channel.ini
;将渠道信息写在
apk
文件的zip comment
中,这种是建议方式,例如可以使用项目 packer-ng-plugin 或者可使用V2 Scheme
的 walle;若不同渠道存在功能上的差异,建议将差异部分放于单独的 dex 或采用相同代码不同配置方式实现;
事实上,tinker
也支持多 flavor
直接编译多个补丁包,具体可参考多 Flavor 打包。
九、tinker 是否兼容加固?
tinker 1.7.8 可以通过 isProtectedApp
开启加固支持,这种模式仅仅可以使用在加固应用中。
加固厂商 | 测试 |
---|---|
腾讯云·乐固 | Tested |
爱加密 | Tested |
梆梆加固 | Tested |
360加固 | Tested,需要 5 月 8 日以后加固的版本 |
其他 | 请自行测试,只要满足下面规则的都可以支持 |
这里是否支持加固,需要加固厂商明确以下两点:
不能提前导入类;
在
art
平台若要编译oat
文件,需要将内联取消。
十、Google Play 版本是否可以有 Tinker 相关代码?
由于 Google play 的使用者协议,对于 GP 渠道我们不能使用 Tinker 动态更新代码,这里会存在应用被下架的风险。但是在 Google play 版本,我们依然可以存在 Tinker 的相关代码,但是我们需要屏蔽补丁的网络请求与合成相关操作。
十一、tinker 与 instant run 的兼容问题?
事实上,若编译时都使用 assemble*
,tinker
与 instant run
是可以兼容的。但是不少用户基础包与补丁包混用两种模式导致补丁过大,所以 tinker
编译时禁用 instant run
,我们可以在设置中禁用 instant run
或使用 assemble
方式编译。
大家日常 debug
时若想开启 instant run
功能,可以将 tinker
暂时关闭:
ext {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = false
}
十二、每次编译我应该保留哪些文件,如何兼容 AndResGuard?
正如 sample 中 app/build.gradle,每个可能用到 Tinker 发布补丁的版本,需要在编译后保存以下几个文件:
编译后生成的
apk
文件,即用来编译补丁的基础版本;若使用
proguard
混淆,需要保持mapping.txt
文件;需要保留编译时的
R.txt
文件;若你同时使用了资源混淆组件 AndResGuard,你也需要将混淆资源的
resource_mapping.txt
保留下来,同时将r/*
也添加到res pattern
中。具体我们可以参考 build.gradle。
微信通过将补丁编译与 Jenkins 很好的结合起来,只需要点击一个按钮,即可方便的生成补丁包。也可以参考 tinkerpatch-andresguard-sample。
十三、tinkerId 应该如何选择?
tinkerId
是用来区分基准安装包的,我们需要严格保证一个基准包的唯一性。在设计的初期,官方使用的是基准包的 CentralDirectory
的 CRC
,但某些 APP 为了生成渠道包会对安装包重新打包,导致不同的渠道包的 CentralDirectory
并不一致。
编译补丁包时,Tinker 会自动读取基准包 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
压缩补丁包。
十五、关于使用的 ClassLoader 问题?
Tinker 没有使用 parent classloader
方案,而是使用 Multidex
插入 dexPathList
方式,这里主要考虑到分平台内部类可能存在校验 classloader
的问题。
若
SDK >= 24
,即 Android N 版本,当补丁存在时,Tinker 将PathClassloader
替换为AndroidNClassLoader
,但是它依然继承与PathClassLoader
。我们依然可以像以往那样对它进行类似makeDexElements
的操作。;若
SDK < 14
,Tinker 没有对classloader
做处理,这里需要注意补丁的Dex
是插入在dexElement
的前方。
十六、什么时候调用 installTinker?
首先我们推荐在最开始的时候就是执行 installTinker
操作,但是即使你不去 installTinker
,也不会影响 Tinker
对代码、So 与资源的加载。installTinker
只是做了以下几件事件:
回调
LoadReporter
,返回加载结果;初始化各个自定义类与 Tinker 实例,可以调用 Tinker 相关 API,发起升级补丁以及处理相关的回调。
事实上,微信只在主进程与 :patch
进程执行 installTinker
操作。其他进程只要不处理回调结果,不发起补丁请求即可。在 SampleUncaughtExceptionHandler 中,为了防止 Crash
并没有执行 installTinker
,全部使用的是 TinkerApplicationHelper 中的 API,详细可以查看 Tinker API 概览。
十七、Proguard 5.2.1 applymapping 出现 Warning?
这是因为 5.2.1 增加了内联函数的行输出信息导致,你可以使用以下几种方法解决:
使用 5.1 版本
proguard
;将内联函数的优化关掉;
自己对
mapping
文件去除内联函数的行信息。
如果使用 4.X 版本的 Proguard
强烈建议升级到 5.1 版本。可以先下载 5.1 的 Proguard
, 然后通过以下方式指定:
classpath files('proguard-5.1.jar')
若使用 gradle 编译,与 multiDexKeepProguard
不同,我们无需将生成的 tinker_proguard.pro
拷贝到自己的配置中。另外一个方面,若 applymapping
过程出现冲突,我们可以采取以下几个方法:
添加
ignoreWarning
;需要注意的是如果某些类的确需要采用新的mapping
,这样补丁后 App 会出问题,一般并不建议采用这种方式;修改基准包的
mapping
文件;我们需要根据新的mapping
文件,修正基准包的mapping
文件。例如将warning
项删掉或者将新mapping
中keep
的项复写到基准的mapping
中。可以参考脚本 proguard_warning.py 与 merge_mapping.py。
注意,如果想通过直接删除旧 mapping
文件的冲突项,需要注意删除类的内部类是否存在混淆冲突。
十八、TinkerPatch 补丁管理后台与 Tinker 的关系?
TinkerPatch 平台是第三方开发基于 CDN 分发的补丁管理后台。它提供了补丁后台托管,版本管理,一键傻瓜式接入等功能,让我们可以无需修改任何代码即可轻松接入Tinker。
我们可以根据自己的需要选择接入,它是独立于 Tinker 项目之外。
十九、Tinker 的最佳实践?
为了使补丁的成功率更高,官方在 Sample 中还做了以下工作:
由于合成进程可能被各种原因杀死,使用 UpgradePatchRetry.java 来做重试功能,提高成功率;
防止补丁后程序无法启动,使用 SampleUncaughtExceptionHandler.java 做
crash
启动保护。这里更推荐的是进入安全模式,使用配置的方式强制清理或者升级补丁;为了防止
BuildConfig
的改变导致大量类的变更,使用 BuildInfo.java 非 final 的变量来中转。为了加快补丁应用同时保持用户体验,SampleResultService.java 在应用退入后台或手机灭屏时,才杀掉进程。你也可以在杀掉进程前,直接通过发送 broadcast 或 service intent 的方式尽快的重启进程。
把
jumboMode
打开,防止由于字符串增多导致force-jumbol
,导致更多的变更。使用
zip comment
方式生成渠道包。