Tinker的原理
服务端做dex差量,将差量包下发到客户端,在ART模式的机型上本地跟原apk中的classes.dex做merge,merge成为一个新的merge.dex后将merge.dex插入pathClassLoader的dexElement,为了实现差量包的最小化,Tinker自研了DexDiff/DexMerge算法。Tinker还支持资源和So包的更新,So补丁包使用BsDiff来生成,资源补丁包直接使用文件md5对比来生成,针对资源比较大的(默认大于100KB属于大文件)会使用BsDiff来对文件生成差量补丁。
Tinker的集成
添加gradle依赖
在项目的build.gradle中,添加tinker-patch-gradle-plugin的依赖
classpath("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}") {
changing = TINKER_VERSION?.endsWith("-SNAPSHOT")
exclude group: 'com.android.tools.build', module: 'gradle'
}
我是把TINKER_VERSION放到了gradle.properties里
TINKER_VERSION=1.9.9
然后在app的gradle文件app/build.gradle,我们需要添加tinker的库依赖以及apply tinker的gradle插件.
apply plugin: 'com.tencent.tinker.patch'
...
//tinker的核心库
implementation("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
//也需要这个依赖
implementation "com.android.support:multidex:1.0.3"
对于Application我们需要:
1.将我们自己Application类以及它的继承类的所有代码拷贝到自己的ApplicationLike继承类中,例如SampleApplicationLike。
2.Application的attachBaseContext方法实现要单独移动到onBaseContextAttached中;
3.对ApplicationLike中,引用application的地方改成getApplication();
4.对其他引用Application或者它的静态对象与方法的地方,改成引用ApplicationLike的静态对象与方法;
完整项目请查看Tinkerdemo
打包
上线前,先执行installRelease任务,打出Apk,生成在备份目录/bakApk/目录中,同时,需要将该文件夹备份,作为下次热更新的基准包;
打补丁
上线后,若出现bug,需要打补丁:
将之前备份好的文件放置在app模块下的/build/bakApk/目录下(文件包含基准包,mapping.txt,R.txt)修改app模块下build.gradle中基准包目录,如下:
//for normal build
//old apk file to build patch apk
tinkerOldApkPath = "${bakPath}/app-release-1024-17-38-43.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-release-1024-17-38-43-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-release-1024-17-38-43-R.txt"
然后执行tinkerPatchRelease任务,生成补丁:
补丁包与相关日志会保存在/build/outputs/tinkerPatch/。然后我们将patch_signed_7zip.apk推送到手机的sdcard中。
adb push ./app/build/outputs/tinkerPatch/debug/patch_signed_7zip.apk /sdcard/
打开app,点击LOAD PATCH按钮, 如果看到patch success, please restart process的toast,表示加载成功,即可重新启动app,就可以看到修改后的效果。
我们引入tinker之后,我们想debug直接运行时,就会报错,这是我们需要把app的build.gradle的tinkerEnabled 修改为false
ext {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = false
....
}
兼容AndResGuard
我们公司项目用到了资源混淆AndResGuard,tinker对资源混淆AndResGuard也是支持的,我们需要修改如下:
需要将混淆资源的resource_mapping.txt保留下来,等执行tinkerPatchRelease任务时我们需要用到resource_mapping.txt,
andResGuard {
//tinker 需要保留此文件
mappingFile = file("${buildDir}/bakApk/resguard/app-release-1029-17-36-41-resource_mapping.txt")
// mappingFile = null
...
}
同时将r/*也添加到res pattern中
res {
/**
* andresguard 混淆需要添加“r/*“
*/
pattern = ["res/*", "r/*","assets/*", "resources.arsc", "AndroidManifest.xml"]
...
}
具体请看build.gradle
执行resguardRelease任务,打出Apk,把生成的备份放到了/bakApk/resguard/目录中
我们还需要修改build.gradle具体代码为
project.afterEvaluate {
def date = new Date().format("MMdd-HH-mm-ss")
def apkSuffix = "_7zip_aligned_signed.apk"
/**
* bak apk and mapping
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
def taskName = variant.name
String name = variant.name.toLowerCase()
String destFilePrefix = "${project.name}-${name}"
// find resguard task first
def resguardTask = project.tasks.findByName("resguard${taskName.capitalize()}")
if (resguardTask == null) {
println("resguardTask not found, just return")
return
}
def tinkerPatchTask = project.tasks.findByName("tinkerPatch${taskName.capitalize()}")
if (tinkerPatchTask == null) {
println("resguardTask not found, just return")
return
}
tinkerPatchTask.doFirst {
def buildApkPath = "${buildDir}/outputs/apk/${taskName}/AndResGuard_${project.getName()}-${taskName}/${project.getName()}-${taskName}${apkSuffix}"
println("change tinkerPatchTask buildApkPath to resugurad output ${buildApkPath}")
tinkerPatchTask.buildApkPath = buildApkPath
}
tinkerPatchTask.dependsOn resguardTask
resguardTask.doLast {
copy {
from "${buildDir}/outputs/apk/${taskName}/AndResGuard_${project.getName()}-${taskName}/${project.getName()}-${taskName}_7zip_aligned_signed.apk"
into file(bakPath.absolutePath + "/resguard")
rename { String fileName ->
fileName.replace("${project.getName()}-${taskName}_7zip_aligned_signed.apk", "${project.getName()}-${taskName}-${date}.apk")
}
from "${buildDir}/outputs/mapping/${taskName}/mapping.txt"
into file(bakPath.absolutePath + "/resguard")
rename { String fileName ->
fileName.replace("mapping.txt", "${project.getName()}-${taskName}-${date}-mapping.txt")
}
from "${buildDir}/intermediates/symbols/${taskName}/R.txt"
into file(bakPath.absolutePath + "/resguard")
rename { String fileName ->
fileName.replace("R.txt", "${project.getName()}-${taskName}-${date}-R.txt")
}
from "${buildDir}/outputs/apk/${taskName}/AndResGuard_${project.getName()}-${taskName}/resource_mapping_${project.getName()}-release.txt"
into file(bakPath.absolutePath + "/resguard")
rename { String fileName ->
fileName.replace("resource_mapping_${project.getName()}-release.txt", "${project.getName()}-${taskName}-${date}-resource_mapping.txt")
}
}
}
}
}
具体步骤就是
1. 上线前,先执行reguardRelease任务,把备份的了/bakApk/resguard/里的文件保存下来。当做下次的基准包。
- 打补丁时,把将之前备份好的文件放置在模块下的/build/bakApk/resguard/(文件包含基准包,mapping.txt,R.txt,resource_mapping.txt),并修改build.guald
andResGuard {
//tinker 需要保留此文件
mappingFile = file("${buildDir}/bakApk/resguard/app-release-1029-17-36-41-resource_mapping.txt")
// mappingFile = null
...
}
...
ext {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = true
//for normal build
//old apk file to build patch apk
tinkerOldApkPath = "${bakPath}/resguard/app-release-1029-17-36-41.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/resguard/app-release-1029-17-36-41-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/resguard/app-release-1029-17-36-41-R.txt"
}
执行tinkerPatchRelease任务, 补丁包会保存在/build/outputs/tinkerPatch/里,至此我们就拿到了补丁包,就可以用来下发给APP,来加载补丁。
完整项目请查看Tinkerdemo