编译过程相关的issue请先查看是否是以下情况:
无法打开sample工程
: 请使用单独的IDE窗口打开tinker-sample-android工程;tinkerId is not set
: 这是因为没有正确的配置IDE的git路径, 若不是通过clone方式下载tinker,需要本地手动commit一次。这里你也可以使用其他字符作为tinkerId;DefaultLifeCycle
注解生成Application,需要将原来Application的实现移动到ApplicationLike中,并将原来的Application类删掉;Class ref in pre-verified class resolved to unexpected implementation
异常, 请确认以下几点:Application中传入ApplicationLike的参数时是否采用字符串而不是Class.getName方式;新的Application是否已经加入到dex loader pattern中; 额外添加到dex loader pattern中类的引用类也需要加载到loader pattern中。在提交issue之前,我们应该先查询是否已经有相关的issue。提交issue时,我们需要写明issue的原因,以及编译或运行过程的日志(加载进程以及Patch进程)。日志我们需要过滤"Tinker."关键字,对于合成失败的情况,请给出:patch进程的日志。这里需要将Android Moniter右上角设为No Filter。
Tinker库中不能修改的类一共有25个,即com.tencent.tinker.loader.*类。加上你的Appliction类,只有25个类是无法通过Tinker来修改的。即使类似Tinker.java等管理类,也是可以通过Tinker本身来修改。
注意,请务必将不能修改的类,特别是你的Application类加入到tinkerPatch.dex.loader pattern中。
Tinker并不干涉你分包与多dex的加载逻辑,但是你需要确保以下几点:
如果你需要在加载其他dex之前加载Tinker的管理类,你也可以将com.tencent.tinker.*都加入到主dex
。 注意:Tinker会自动生成需要放在主dex的keep规则,你需要手动将生成规则拷贝到自己的multiDexKeepProguard文件中。例如Sample中的multiDexKeepProguard file("keep_in_main_dex.txt")
。
另外,如果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不能是补丁版本一,它应该依然是用户手机上已经安装的基准版本。
当前我们并没有直接将补丁的lib路径添加到DexPathList
中,理论上这样可以做到程序完全没有感知的对Library文件作补丁。这里主要是因为在多abi的情况下,某些机器获取的并不准确。当前对Library文件作补丁可参考Tinker API概览,这里以后需要考虑优化。
另外一方面,对于第三方库文件的加载我们无法干预,但是只要在我们的代码提前加载第三方的库文件即可。不过这里确保我们使用的是同一个classloader来加载。
无论是对Library还是Application,我们都是采用尽量少去反射的策略,这也是为了提高Tinker框架的兼容性。上线前,我们应当严格测试补丁是否正确加载了修改后的So库。不使用反射的另外一个好处是我们可以做的工作更多,例如加载前验证它的MD5。
Tinker采用全量合成方式实现资源替换,这里有以下几点是使用者需要明确的:
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"]
}
它们应该说各有优劣势,大概应该有以下几条原则:
10M
的dex为例,它压缩成jar大约为4M
,即'jar'模式能节省6M
的ROM空间。小米2S
上数据大约为'raw'模式126ms
, 'jar'模式为246ms
。 因为在合成过程中我们已经校验了各个文件的Md5,并将它们存放在/data/data/..目录中。默认每次加载时我们并不会去校验tinker文件的Md5
,但是你也可通过开启loadVerifyFlag强制每次加载时校验,但是这会带来一定的时间损耗。
简单来说,'jar'模式更省空间,但是运行时校验的耗时大约为'raw'模式的两倍。如果你没有打开运行时校验,推荐使用'jar'模式。
usePreGeneratedPatchDex模式即提前生成最终需要的Dex, 在补丁时无须再合成。简单来说这边就是类似Qzone原理的方案,但是这套方案有两个问题:
事实上,这里并不建议大家使用这种模式。提供这种模式是为了解决tinker无法支持加固、多flavor等场景。大家需要谨慎的选择。特别对于多渠道包问题,这里更建议大家使用下面推荐的方式。另外一方面,这种方案在Android N之后可能会产生问题,建议过滤N之后的用户。
关于渠道包的问题,若使用flavor编译渠道包,会导致不同的渠道包由于BuildConfig变化导致classes.dex差异。这里建议的方式有:
事实上,tinker也支持多flavor直接编译多个补丁包,具体可参考多Flavor打包。
tinker的一般模式需要Dex的合成,它并不支持加固,一定要使用加固的app可以使用usePreGeneratedPatchDex模式。由于加固会改变apk的dex结构,所以生成补丁包时我们务必要使用加固前的apk。
但是需要注意的是,某些加固工具会将非exported的四大组件的类名替换,对于这部分类即使使用usePreGeneratedPatchDex也无法修改。对于360加固,MainActivity由于被提前加载,也无法修复。大家对于加固的情况,请仔细测试,能否支持与加固的方式有关联。
由于Google play的使用者协议,对于GP渠道我们不能使用Tinker动态更新代码,这里会存在应用被下架的风险。但是在Google play版本,我们依然可以存在Tinker的相关代码,但是我们需要屏蔽补丁的网络请求与合成相关操作。
若不使用usePreGeneratedPatchDex模式,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
}
正如sample中app/build.gradle,每个可能用到Tinker发布补丁的版本,需要在编译后保存以下几个文件:
r/*
也添加到res pattern中。具体我们可以参考build.gradle。微信通过将补丁编译与Jenkins很好的结合起来,只需要点击一个按钮,即可方便的生成补丁包。
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!
对于代码来说,我们最好记住以下几条规则:
保持原本的分包规则
,尽量减少由于分包变化而带来的变更。在生成补丁包过程中,对于class分包的变化将会输出Warning:Class Moved
日志, 我们应该尽量减少这种变化;合成的时间消耗
与占ROM的体积
影响更大。我们每次生成补丁后,都应该查看TinkerPatch
输出文件夹的日志;Tinker没有使用parent classloader方案,而是使用Multidex插入dexPathList方式,这里主要考虑到分平台内部类可能存在校验classloader的问题。
首先我们推荐在最开始的时候就是执行installTinker操作,但是即使你不去installTinker,也不会影响Tinker对代码、So与资源的加载。installTinker只是做了以下几件事件:
事实上,微信只在主进程与:patch进程执行installTinker操作。其他进程只要不处理回调结果,不发起补丁请求即可。在SampleUncaughtExceptionHandler中,为了防止Crash时并没有执行installTinker
,全部使用的是TinkerApplicationHelper中的API,详细可以查看Tinker API概览。
这是因为5.2.1增加了内联函数的行输出信息导致,你可以使用以下几种方法解决:
若使用gradle编译,与multiDexKeepProguard不同,我们无需将生成的tinker_proguard.pro拷贝到自己的配置中。
TinkerPatch平台 是第三方开发基于CDN分发的补丁管理后台。它提供了脚本后台托管,版本管理,保证传输安全等功能,让我们更加容易的接入Tinker。
我们可以根据自己的需要选择接入,它是独立于Tinker项目之外。对于Tencent/tinker
, 我们依然会以它的稳定性与性能作为第一要务。
为了使补丁的成功率更高,我们在Sample中还做了以下工作:
这里更推荐的是进入安全模式
,使用配置的方式强制清理或者升级补丁;更多的使用范例,大家请仔细阅读Sample。Tinker框架支持高度自定义,若使用过程中有任何问题或建议,欢迎联系我们!