本文使用的是tinker的1.9.6版本,使用gradle方式接入。具体的接入方式可参考官方接入指南。
需要特别注意:
1. Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件(1.9.0开始支持新增非export的Activity);
2. 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码(实际上大概率会过不了审);
3. 在Android N上,补丁对应用启动时间有轻微的影响;
4. 不支持部分三星android-21机型,加载补丁时会主动抛出”TinkerRuntimeException:checkDexInstall failed”;
5. 对于资源替换,不支持修改remoteView,例如transition动画,notification icon以及桌面图标;
6. 目前tinker热更新so包的话,只支持armeabi目录下的库文件。
在项目根build.gradle中:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.1'
// 引入tinker插件
classpath "com.tencent.tinker:tinker-patch-gradle-plugin:1.9.6"
}
}
在项目app的build.gradle里配置基本引入:
// 用git号做TINKER_ID
def getTinkerIdValue() {
return hasProperty("TINKER_ID") ? TINKER_ID : gitSha()
}
def gitSha() {
try {
String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
if (gitRev == null) {
throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
return gitRev
} catch (Exception e) {
throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
}
android {
...
defaultConfig {
...
// 配置TINKER_ID
buildConfigField "String", "TINKER_ID", "\"${getTinkerIdValue()}\""
// 必须开启multiDex
multiDexEnabled true
}
// tinker建议开启此项,否则若Application所直接使用的类没有被打在main dex中,就会导致patch失败
dexOptions {
// 忽略方法数限制的检查
jumboMode = true
}
sourceSets {
main {
...
// 若要热修复so包,需要指明so包路径
jniLibs.srcDirs = ['libs']
}
}
}
dependencies {
compile fileTree(include: ['*.*'], dir: 'libs')
...
// 若使用>=3的gradle,tinker_version=1.9.6
implementation "com.android.support:multidex:1.0.1"
implementation("com.tencent.tinker:tinker-android-lib:${rootProject.ext.tinker_version}") {
changing = true
}
// 用于编译时生成application类
annotationProcessor("com.tencent.tinker:tinker-android-anno:${rootProject.ext.tinker_version}") {
changing = true
}
compileOnly("com.tencent.tinker:tinker-android-anno:${rootProject.ext.tinker_version}") {
changing = true
}
// 若使用<3的gradle
//compile "com.android.support:multidex:1.0.1"
//compile("com.tencent.tinker:tinker-android-lib:${rootProject.ext.tinker_version}") {
// changing = true
//}
//provided("com.tencent.tinker:tinker-android-anno:${rootProject.ext.tinker_version}") {
// changing = true
//}
}
// 其他签名啊,混淆啊啥的都正常配置就行
代码引入方面,此处基本可以照搬示例代码:
在Application接入时,Tinker借助注解来生成Application:
/**
* 用于生成实际Application的注解类,这样做是因为,所有与Application直接使用的类都需要被打在主dex中。
* 同时,尽量避免在Application中做多余的工作。
*/
@SuppressWarnings("unused")
@DefaultLifeCycle(
// 定义生成的Application类,与AndroidManifest中的application节点name属性一致,为避免错误,必须是全名
application = "cn.com.bluemoon.delivery.AppContext",
// TINKER_ENABLE_ALL:支持dex、lib(so包)、资源的更新
flags = ShareConstants.TINKER_ENABLE_ALL,
// 在加载时是否校验dex、lib与res的md5,默认false
loadVerifyFlag = false
// loaderClass: 定义tinker的类加载器,默认为TinkerLoader)
public class HFApplicationLike extends DefaultApplicationLike {
...
public HFApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
/**
* 在安装tinker前安装multiDex,以避免将tinker放进主dex
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
// 必须先执行MultiDex的install
MultiDex.install(base);
// 赋值全局使用的application单例以及其context,其实就是生成的application的实例
AppContextHolder.application = getApplication();
AppContextHolder.context = getApplication();
TinkerManager.setTinkerApplicationLike(this);
// 没设置uncaughtExceptionHandler,就用默认的SampleUncaughtExceptionHandler
TinkerManager.initFastCrashProtect();
// 必须在tinker安装前设置
TinkerManager.setUpgradeRetryEnable(true);
// 设置log的实现,可以使用默认实现
TinkerInstaller.setLogIml(new MyLogImp());
// 在加载multiDex后执行,否则就需要将com.tencent.tinker.**手动放到主dex,麻烦
// 配置tinker其他项
TinkerManager.installTinker(this);
Tinker tinker = Tinker.with(getApplication());
}
/**
* 可重写onCreate() 、onLowMemory()、onTrimMemory(int level)、onTerminate()、onConfigurationChanged,对应的调用时机与实际Application中的一致
*/
@Override
public void onCreate() {
super.onCreate();
}
其中TinkerManager是协助Tinker初始化的辅助类,具体可见sample代码,此处只关注:
public class TinkerManager {
...
public static void installTinker(ApplicationLike appLike) {
if (isInstalled) {
TinkerLog.w(TAG, "install tinker, but has installed, ignore");
return;
}
// Tinker在加载补丁时的一些回调,默认实现为DefaultLoadReporter
LoadReporter loadReporter = new SampleLoadReporter(appLike.getApplication());
// Tinker在修复或者升级补丁时的一些回调,默认实现为DefaultPatchReporter
PatchReporter patchReporter = new SamplePatchReporter(appLike.getApplication());
// 用来过滤Tinker收到的补丁包的修复、升级请求,也就是决定是不是真的要唤起:patch进程去尝试补丁合成,默认实现为DefaultPatchListener
PatchListener patchListener = new SamplePatchListener(appLike.getApplication());
// 用来升级当前补丁包的处理类,一般来说不需要复写
AbstractPatch upgradePatchProcessor = new UpgradePatch();
TinkerInstaller.install(appLike,
loadReporter, patchReporter, patchListener,
SampleResultService.class, upgradePatchProcessor);
isInstalled = true;
}
}
详细的配置说明可见Tinker自定义扩展。
最后,需要在AndroidManifest中做做些微修改:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:name=".AppContext"
...>
<service
android:name=".tinker.service.SampleResultService"
android:exported="false"/>
application>
patch的配置在app的build.gradle中定义,详细的配置可见官方指南
// 生成备份的apk、混淆mapping文件、资源R文件的目录,其实就是将每次build出来的相应文件以别名备份
def bakPath = file("${buildDir}/bakApk/")
// bakPath里的备份包名称,用于与新包作比较得出补丁包
def curApkName = "月亮天使_20180514_09_测试版_4.9.4(1726)_0514-09-20-59"
/**
* 手动的话,可以先使用assembleRelease编译出基础包,
* 再使用"tinkerPatchRelease -POLD_APK=... -PAPPLY_MAPPING=... -PAPPLY_RESOURCE=..."命令生成补丁包
*/
ext {
// 是否执行tinker。开发期间,可以将这个关了来使用instant run。btw,目前instant run与tinker是互斥的。
tinkerEnabled = true
// 旧包(用于打补丁的基础旧包)apk名
tinkerOldApkPath = "${bakPath}/${curApkName}.apk"
// 旧包mapping文件
tinkerApplyMappingPath = "${bakPath}/${curApkName}-mapping.txt"
// 旧包R文件
tinkerApplyResourcePath = "${bakPath}/${curApkName}-R.txt"
//only use for build all flavor, if not, just ignore this field,暂未用过
tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}
////////// 以下都是一些辅助方法(start) /////////////
def getOldApkPath() {
return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}
def getApplyMappingPath() {
return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}
def getApplyResourceMappingPath() {
return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}
// 用git号做版本号
def gitSha() {
try {
String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
if (gitRev == null) {
throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
return gitRev
} catch (Exception e) {
throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
}
def getTinkerIdValue() {
return hasProperty("TINKER_ID") ? TINKER_ID : gitSha()
}
def buildWithTinker() {
return hasProperty("TINKER_ENABLE") ? Boolean.parseBoolean(TINKER_ENABLE) : ext.tinkerEnabled
}
def getTinkerBuildFlavorDirectory() {
return ext.tinkerBuildFlavorDirectory
}
////////// 以上都是一些辅助方法(end) /////////////
if (buildWithTinker()) {
// 引入tinker的patch插件
apply plugin: 'com.tencent.tinker.patch'
// tinkerPatch参数配置
tinkerPatch {
/**
* 必填,默认为null
* 旧包路径,用作与新包比对后得出差分包
* 一般是build/bakApk下
*/
oldApk = getOldApkPath()
/**
* 选填,默认为false
* 以下情况会出现warnings,若设置ignoreWarning为true,只会assert加载补丁进程:
* 1、minSdkVersion < 14, 但设置了dexMode为raw,此骚操作在加载补丁时必崩;
* 2、在AndroidManifest.xml中添加新的四大组件(1.9.0版本以上非export的Activity除外),同上,必崩;
* 3、在以下dex.loader中的用于加载补丁的类没有在主dex中,这样的话,tinker不会起作用;
* 4、dex.loader中的用于加载补丁的类修改,加载器类是用来加载补丁的,在新包修改的话也不会起作用。此操作不会引发崩溃,但也不会起效,可以忽略;
* 5、resources.arsc变更了,但没有设置applyResourceMapping(applyResourceMapping=null)用于编译
*/
ignoreWarning = false
/**
* 选填,默认为true
* 是否需要对补丁签名
* false时需要手动签名,否则在加载补丁时无法通过检查
* 这里使用的是android.buildTypes.xx.signingConfig的配置
*/
useSign = true
/**
* 选填,默认为true
* 此处同ext.tinkerEnabled
*/
tinkerEnable = buildWithTinker()
/**
* applyMapping会影响正常的编译
*/
buildConfig {
/**
* 选填,默认为null
* 若使用tinkerPatch命令去打补丁包, 并且开启了minifyEnabled混淆,建议用旧包的mapping文件。
* 在编译新的apk时候,通过保持旧apk的proguard混淆方式,从而减少补丁包的大小。
* 警告:
* 此操作会影响正常的编译过程
*/
applyMapping = getApplyMappingPath()
/**
* 选填,默认为null
* 使用旧apk的R.txt文件保持ResId的分配,这样不仅可以减少补丁包的大小,同时也避免由于ResId改变导致remote view异常。
*/
applyResourceMapping = getApplyResourceMappingPath()
/**
* 必填,默认为null
* 由于不希望在运行时检测基础apk(旧包)的md5(慢),
* 这里在打补丁包的时候,使用了tinkerId来标识基础apk的版本。
* 此处使用的是gitSha(),同时,thinkerId会自动的被写到AndroidManifest中
*/
tinkerId = getTinkerIdValue()
/**
* 若为true,多dex时会按照基准包的类分布来编译,可以减少dex差分包大小。低版本的tinker此选项开启后有bug。
*/
keepDexApply = false
/**
* 选填,默认为false
* 是否使用加固模式,仅仅将变更的类合成补丁。注意,这种模式仅仅可以用于加固应用中。
*/
isProtectedApp = false
/**
* 选填,默认为false
* 是否支持新增非export的Activity,只有在再次启动加载补丁后,此Activity才起作用
*/
supportHotplugComponent = true
}
// dex相关的配置项
dex {
/**
* 选填,默认为jar
* 取值为raw或jar,
* raw模式会保持输入dex的格式,
* jar模式,会把输入dex重新压缩封装到jar,如果minSdkVersion<14,必须选择jar模式,而且它更省存储空间,但是验证md5时比raw模式耗时。默认并不会去校验md5,一般情况下选择jar模式即可。
dexMode = "jar"
/**
* 必填,默认为[]
* 需要处理dex路径,支持*、?通配符,必须使用'/'分割。
* 路径是相对安装包的,例如assets/...
*/
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
/**
* 必填,默认为[]
* 此项非常重要,它定义了哪些类在加载补丁包的时候会用到。这些类是通过tinker无法修改的类,也是一定要放在main dex的类。
* 必须把以下的类放进这里:
* 1、自定义的Application类(为避免版本问题,此处最好把ApplicationLike生成的Application也加进来);
* 2、Tinker库中用于加载补丁包的部分类,即com.tencent.tinker.loader.*;
* 3、若自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中;
* 4、其他一些不希望被更改的类,例如Sample中的BaseBuildInfo类。这里需要注意的是,这些类的直接引用类也需要加入到loader中,或者需要将这个类变成非preverify。
* 5、使用1.7.6版本之后的gradle版本,参数1、2会自动填写。若使用newApk或者命令行版本编译,1、2依然需要手动填写
*/
loader = [
"cn.com.bluemoon.delivery.AppContext",
//use sample, let BaseBuildInfo unchangeable with tinker
"cn.com.bluemoon.delivery.tinker.app.BaseBuildInfo"
]
}
// lib相关的配置项
lib {
/**
* 选填,默认为[]
* 需要处理的lib路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的。
* 对于在assets中的lib,tinker只在补丁包中回复它,可以在TinkerLoadResult中拿到
*/
pattern = ["lib/*/*.so"]
}
// res相关的配置项
res {
/**
* 选填,默认为[]
* 需要处理的res路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的。
* 所有的资源文件都必须包含进来,否则否则不会再新包中被重新打包
*/
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
/**
* 选填,默认为[]
* 支持*、?通配符,必须使用'/'分割。若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改。
* 只能用于不与resources.arsc相关联的文件
*/
ignoreChange = ["assets/sample_meta.txt"]
/**
* 默认100kb
* 对于修改的资源,如果大于largeModSize,将使用bsdiff算法。这可以降低补丁包的大小,但是会增加合成时的复杂度。
*/
largeModSize = 100
}
// 用于生成补丁包中的'package_meta.txt'文件
packageConfig {
/**
* 选填
*
* we will get the TINKER_ID from the old apk manifest for you automatic,
* other config files (such as patchMessage below)is not necessary
* 默认自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动写入configField。在这里,可以定义其他的信息,在运行时可以通过自定义的ownPackageCheck方法里的securityCheck.getPackageProperties()或TinkerLoadResult.getPackageConfigByName得到相应的数值。但是建议直接通过修改代码来实现,例如BuildConfig。
* 以下都是例子
*/
configField("patchMessage", "tinker is sample to use")
configField("platform", "all")
configField("patchVersion", "1.0")
}
/**
* 7zip路径配置项,执行前提是useSign为true
* 若不使用zipArtifact或者path, 会自动试用7za。
*/
sevenZip {
/**
* 选填,默认'7za'
* 例如"com.tencent.mm:SevenZip:1.1.10",将自动根据机器属性获得对应的7za运行文件,推荐使用。
*/
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
/**
* 选填,默认'7za'
* 系统中的7za路径,例如"/usr/local/bin/7za"。path设置会覆盖zipArtifact,若都不设置,将直接使用7za去尝试。
*/
// path = "/usr/local/bin/7za"
}
}
}
// 多flavors
List flavors = new ArrayList<>()
project.android.productFlavors.each { flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
def date = new Date().format("MMdd-HH-mm-ss")
/**
* 备份任务
*/
android.applicationVariants.all { variant ->
def taskName = variant.name
tasks.all {
if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {
it.doLast {
copy {
def fileNamePrefix = "${project.name}-${variant.baseName}"
// 备份文件名
def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"
// 将生成的apk包备份到bak文件夹下,以newFileNamePrefix.apk的名字
def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
from variant.outputs.first().outputFile
into destPath
rename { String fileName ->
fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
}
// 同理,备份mapping.txt
from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
into destPath
rename { String fileName ->
fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
}
// 同理,备份R.txt
from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
into destPath
rename { String fileName ->
fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
}
}
}
}
}
}
// 以下为多flavors打包用到的,本文未涉及
project.afterEvaluate {
if (hasFlavors) {
task(tinkerPatchAllFlavorRelease) {
group = 'tinker'
def originOldPath = getTinkerBuildFlavorDirectory()
for (String flavor : flavors) {
def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")
dependsOn tinkerTask
def preAssembleTask = tasks.getByName("process${flavor.capitalize()}ReleaseManifest")
preAssembleTask.doFirst {
String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15)
project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release.apk"
project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-mapping.txt"
project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-R.txt"
}
}
}
task(tinkerPatchAllFlavorDebug) {
group = 'tinker'
def originOldPath = getTinkerBuildFlavorDirectory()
for (String flavor : flavors) {
def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Debug")
dependsOn tinkerTask
def preAssembleTask = tasks.getByName("process${flavor.capitalize()}DebugManifest")
preAssembleTask.doFirst {
String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13)
project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk"
project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-mapping.txt"
project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-R.txt"
}
}
}
}
}
使用的时候,通常用的是生成的7zip包,在得到补丁包后,调用:
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
// 补丁包绝对路径
Environment.getExternalStorageDirectory().getAbsolutePath() + "/Download/patch_signed_7zip.apk");
若要手动清除补丁,可调用:
// 清除补丁
Tinker.with(getApplicationContext()).cleanPatch();
文件名 | 描述 |
---|---|
patch_unsigned.apk | 没有签名的补丁包 |
patch_signed.apk | 签名后的补丁包 |
patch_signed_7zip.apk | 签名后并使用7zip压缩的补丁包,也是我们通常使用的补丁包。但正式发布的时候,最好不要以.apk结尾,防止被运营商挟持 |
log.txt | 在编译补丁包过程的控制台日志 |
dex_log.txt | 在编译补丁包过程关于dex的日志 |
so_log.txt | 在编译补丁包过程关于lib的日志 |
tinker_result | 最终在补丁包的内容,包括diff的dex、lib以及assets下面的meta文件 |
resources_out.zip | 最终在手机上合成的全量资源apk,你可以在这里查看是否有文件遗漏 |
tempPatchedDexes | 在Dalvik与Art平台,最终在手机上合成的完整Dex,我们可以在这里查看dex合成的产物 |
将补丁文件保存到设备,如上上节中的Environment.getExternalStorageDirectory().getAbsolutePath() + “/Download/patch_signed_7zip.apk”,调用TinkerInstaller.onReceiveUpgradePatch即可加载补丁,加载完成的回调中一般需要将补丁包删除。等待patch进程完成后,重新启动应用(必须完全kill掉进程重新启动),即可完成补丁加载。
你必须知道的APT、annotationProcessor、android-apt、Provided、自定义注解
类加载机制系列2——深入理解Android中的类加载器
Android热修复技术选型——三大流派解析
Tinker 接入指南
Android 热修复 Tinker接入及源码浅析