三、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 生命周期