三、Tinker 的配置及任务
1、开启支持大工程模式
Tinker 文档中推荐将jumboMode 设置为true。
```
android {
dexOptions {
// 支持大工程模式
jumboMode = true
}
...
}
```
2、配置Tinker 与任务
将下面的配置全部复制粘贴到app 的gradle 文件(app/build.gradle)末尾,内容很多,但现在只需要看懂bakPath 与ext 括号内的东东就好了。
```
// Tinker 配置与任务
def bakPath = file("${buildDir}/bakApk/")
ext {
// 是否使用Tinker(当你的项目处于开发调试阶段时,可以改为false)
tinkerEnabled = true
// 基础包文件路径(名字这里写死为old-app.apk。用于比较新旧app 以生成补丁包,不管是debug
还是release 编译)
tinkerOldApkPath = "${bakPath}/old-app.apk"
// 基础包的mapping.txt 文件路径(用于辅助混淆补丁包的生成,一般在生成release 版app 时会
使用到混淆,所以这个mapping.txt 文件一般只是用于release 安装包补丁的生成)
tinkerApplyMappingPath = "${bakPath}/old-app-mapping.txt"
// 基础包的R.txt 文件路径(如果你的安装包中资源文件有改动,则需要使用该R.txt 文件来辅助生
成补丁包)
tinkerApplyResourcePath = "${bakPath}/old-app-R.txt"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/flavor"
}
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
}
def getTinkerIdValue() {
return hasProperty("TINKER_ID") ? TINKER_ID : android.defaultConfig.versionName
}
def buildWithTinker() {
return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}
def getTinkerBuildFlavorDirectory() {
return ext.tinkerBuildFlavorDirectory
}
if (buildWithTinker()) {
//apply tinker 插件
apply plugin: 'com.tencent.tinker.patch'
// 全局信息相关的配置项
tinkerPatch {
tinkerEnable = buildWithTinker()// 是否打开tinker 的功能。
oldApk = getOldApkPath() // 基准apk 包的路径,必须输入,否则会报错。
ignoreWarning = false // 是否忽略有风险的补丁包。这里选择不忽略,当补丁包风
险时会中断编译。
useSign = true // 在运行过程中,我们需要验证基准apk 包与补丁包的签名
是否一致,我们是否需要为你签名。
// 编译相关的配置项
buildConfig {
applyMapping = getApplyMappingPath()
// 可选参数;在编译新的apk 时候,我们希望通过保持旧apk 的proguard 混淆方式,从
而减少补丁包的大小。这个只是推荐设置,不设置applyMapping 也不会影响任何的assemble 编译。
applyResourceMapping = getApplyResourceMappingPath()
// 可选参数;在编译新的apk 时候,我们希望通过旧apk 的R.txt 文件保持ResId 的分配,
这样不仅可以减少补丁包的大小,同时也避免由于ResId 改变导致remote view 异常。
tinkerId = getTinkerIdValue()
// 在运行过程中,我们需要验证基准apk 包的tinkerId 是否等于补丁包的tinkerId。这个
是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git 版本号、versionName 等等。
keepDexApply = false
// 如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。若打开
keepDexApply 模式,补丁包将根据基准包的类分布来编译。
isProtectedApp = false // 是否使用加固模式,仅仅将变更的类合成补丁。注意,这种模
式仅仅可以用于加固应用中。
supportHotplugComponent = false // 是否支持新增非export 的Activity(1.9.0 版本
开始才有的新功能)
}
// dex 相关的配置项
dex {
dexMode = "jar"
// 只能是'raw'或者'jar'。对于'raw'模式,我们将会保持输入dex 的格式。对于'jar'模式,我们将会把
输入dex 重新压缩封装到jar。如果你的minSdkVersion 小于14,你必须选择‘jar’模式,而且它更省存
储空间,但是验证md5 时比'raw'模式耗时。默认我们并不会去校验md5,一般情况下选择jar 模式即可。
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
// 需要处理dex 路径,支持*、?通配符,必须使用'/'分割。路径是相对安装包的,例如assets/...
loader = [
// 定义哪些类在加载补丁包的时候会用到。这些类是通过Tinker 无法修改的类,也
是一定要放在main dex 的类。
// 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader 中;
// 其他一些你不希望被更改的类,例如Sample 中的BaseBuildInfo 类。这里需要
注意的是,这些类的直接引用类也需要加入到loader 中。或者你需要将这个类变成非preverify。
]
}
// lib 相关的配置项
lib {
pattern = ["lib/*/*.so","src/main/jniLibs/*/*.so"]
// 需要处理lib 路径,支持*、?通配符,必须使用'/'分割。与dex.pattern 一致, 路径是相
对安装包的,例如assets/...
}
// res 相关的配置项
res {
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
// 需要处理res 路径,支持*、?通配符,必须使用'/'分割。与dex.pattern 一致, 路径是相
对安装包的,例如assets/...,务必注意的是,只有满足pattern 的资源才会放到合成后的资源包。
ignoreChange = [
// 支持*、?通配符,必须使用'/'分割。若满足ignoreChange 的pattern,在编译
时会忽略该文件的新增、删除与修改。最极端的情况,ignoreChange 与上面的pattern 一致,即会完
全忽略所有资源的修改。
"assets/sample_meta.txt"
]
largeModSize = 100
// 对于修改的资源,如果大于largeModSize,我们将使用bsdiff 算法。这可以降低补丁包
的大小,但是会增加合成时的复杂度。默认大小为100kb
}
// 用于生成补丁包中的'package_meta.txt'文件
packageConfig {
// configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest 中读取
tinkerId,并自动写入configField。
// 在这里,你可以定义其他的信息,在运行时可以通过
TinkerLoadResult.getPackageConfigByName 得到相应的数值。
// 但是建议直接通过修改代码来实现,例如BuildConfig。
configField("platform", "all")
configField("patchVersion", "1.0")
// configField("patchMessage", "tinker is sample to use")
}
// 7zip 路径配置项,执行前提是useSign 为true
sevenZip {
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
}
}
List<String> 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")
/**
* bak apk and mapping
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
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}"
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")
}
from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
into destPath
rename { String fileName ->
fileName.replace("mapping.txt","${newFileNamePrefix}-mapping.txt")
}
from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
into destPath
rename { String fileName ->
fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
}
}
}
}
}
}
project.afterEvaluate {
//sample use for build all flavor for one time
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"
}
}
}
}
}
}
```
其中,有几点配置在这里说明一下,方便理解后续的操作(当tinkerEnabled =
true 的情况下):
1.app 的生成目录是:主Module(一般是名为app)/build/bakApk 文件夹。
2.补丁包的生成路径:主Module(一般是名为app)/build/outputs/apk/tinkerPatch/debug/patch_signed_7zip.apk。
3.基础包的名字:old-app.apk,放于bakApk 文件夹下。
4.基础包的mapping.txt 和R.txt 文件一般在编译release 签名的apk 时才会用到。
5.在用到mapping.txt 文件时,需要重命名为old-app-mapping.txt,放于bakApk 文件夹下。
6.在用到R.txt 文件时,需要重命名为old-app-R.txt,放于bakApk 文件夹下。
对于mapping.txt 和R.txt 文件,在配置中有说明,请回配置中仔细看。上面只是我项目中的配置,这些其实都是可以自定义的,建议在搞清楚配置内容之后再去自定义修改。
3、什么是基础包??
基础包就是已经上架的apk 文件(假设是1.0 版本)。这其实很好理解,在新版
本的App 上架之前(假设是2.0 版本),我们会用到Tinker 来修复1.0 版App
中存在的bug,这时就需要用到Tinker 来产生补丁包文件,而补丁包文件的本
质,就是修复好Bug 的App 与1.0 版本App 之间的文件差异。在2.0 版本上架
之前,我们可能会多次产生新的补丁包,用于修复在用户手机上的1.0 版App,
所以补丁包必须以1.0 版App 作为参考标准,也就是说用户手机上的app 就是
基础包,即当前应用市场上的apk 文件(前面说的1.0 版本)。
- 第一章 热修复设计
- 第一节、AOT/JIT & dexopt 与dex2oat
- 一、AOT/JIT
- 二、dexopt 与dex2oat
- 第二节、热修复设计之CLASS_ISPREVERIFIED 问题
- 一、前言
- 二、建立测试Demo
- 三、制作补丁
- 四、加载补丁
- 五、CLASS_ISPREVERIFIED
- 第三节、热修复设计之热修复原理
- 一、Android 热修复
- 二、Android 虚拟机和编译加载顺序
- 三、混合模式的理解
- 四、源码类到机器执行的文件过程
- 五、补丁包
- 六、类补丁生效原理
- 七、Davlik 虚拟机的限制
- 八、Davlik Class resolved by unexpected DEX: 限制和处理方式
- 九、类加载器的双亲委派加载机制
- 第四节、Tinker 的集成与使用(自动补丁包生成)
- 一、简述
- 二、Tinker 组件依赖
- 三、Tinker 的配置及任务
- 四、Tinker 封装与拓展
- 五、编写Application 的代理类
- 六、常用API
- 七、测试
- 八、细节
- 第二章 插件化设计
- 第一节、Class 文件与Dex 文件的结构解读
- 一、Class 文件
- 二、Dex 文件
- 三、Class 文件和Dex 文件对比
- 第二节、Android 资源加载机制详解
- 第三节、四大组件调用原理
- 第四节、so 文件加载机制
- 第五节、Android 系统服务实现原理
- 第三章 组件化框架设计
- 第一节、阿里巴巴开源路由框——ARouter 原理分析
- 第二节、APT 编译时期自动生成代码&动态类加载
- 第三节、Java SPI 机制
- 第四节、AOP&IOC
- 第五节、手写组件化架构
- 第四章 图片加载框架
- 第一节 图片加载框架选型
- 第二节 Glide 原理分析
- 第三节 手写图片加载框架实战
- 第五章 网络访问框架设计
- 第一节 网络通信必备基础
- 第二节 OkHttp 源码解读
- 第三节 Retrofit2 源码解析
- 第六章 RXJava响应式编程框架设计
- 第一节 RXJava之链式调用
- 第二节 RXJava之扩展的观察者模式
- 第三节 RXJava之事件变换设计
- 第四节 Scheduler 线程控制
- 第七章 IOC架构设计
- 第一节 依赖注入与控制反转
- 第二节 ButterKnife 原理上篇、中篇、下篇
- 第三节 IOC架构设计之Dagger2架构设计
- 第八章 Android架构组件 JetPack
- 第一节 LiveData的工作原理
- 第二节 Navigation 如何解决tabLayout 问题
- 第三节 ViewModel 如何感知View 生命周期及内核原理
- 第四节 Room 架构方式方法
- 第五节 dataBinding 为什么能够支持MVVM
- 第六节 WorkManager 内核揭秘
- 第七节 Lifecycles 生命周期