Tinker工作流程
tinker熱修復(fù)實(shí)現(xiàn)隨著版本進(jìn)行過不少改動(dòng),但是核心理念一直沒變,主要是通過DexDiff算法對新舊APK dex文件比對得到差異patch.dex,然后下發(fā)patch.dex到客戶端合成新dex代替舊dex達(dá)到熱更,這里直接貼一張官方圖。

這張圖僅僅展現(xiàn)了冰山一角,對于apk資源文件,代碼混淆,app加固,多dex等情況下的兼容處理也是不可忽視的點(diǎn)。作為一個(gè)用戶,使用tinker也不算是一件簡單的事情,了解得多使用起來才能更順手,這篇先從補(bǔ)丁包的生成過程開始逐步分析tinker實(shí)現(xiàn)。
代碼基于Tinker1.9.14.16
生成差異包
集成Tinker后可以在gradle面板中看到tinker相關(guān)的gradle task,一共有5個(gè)

tinker task 相關(guān)代碼在gradle-plugin模塊里面

可以看到一共有五個(gè)task,他們的作用如下
- TinkerManifestTask用于往manifest文件中插入tinker_id
- TinkerResourceIdTask 通過讀取舊apk生成的R.txt文件(資源ID地址映射)來保持新apk資源ID的分配
- TnkerProguardConfigTask 讀取舊apk的混淆規(guī)則映射文件來保持新apk的代碼混淆規(guī)則
- TinkerMultidexConfigTask
- TinkerPatchSchemaTask用于比對新舊apk得到差異包
這五個(gè)task除了TinkerPatchSchemaTask以外,其他四個(gè)task都掛載到了app打包流程中,每次進(jìn)行打包時(shí)執(zhí)行,對apk中文件做特定處理。
我們先來分析TinkerPatchPlugin,看看這幾個(gè)task執(zhí)行的時(shí)機(jī),然后依次分析各個(gè)task的作用
由于不想寫太多篇幅,不太重要的點(diǎn)直接寫在注釋里面,不太重要的方法調(diào)用直接寫結(jié)論,具體實(shí)現(xiàn)可以自行翻看方法實(shí)現(xiàn)
TinkerPatchPlugin
TinkerPatchPlugin中創(chuàng)建了tinker需要的各個(gè)gradle配置(extension)和上述的五個(gè)task,為它們配置一些必要參數(shù),然后將各個(gè)task掛載在打包流程的各個(gè)階段,簡單看一下代碼。
class TinkerPatchPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
// 這個(gè)插件用于檢測操作系統(tǒng)名稱和架構(gòu)
try {
mProject.apply plugin: 'osdetector'
} catch (Throwable e) {
mProject.apply plugin: 'com.google.osdetector'
}
// 創(chuàng)建app build.gradle中tinkerPatch配置
mProject.extensions.create('tinkerPatch', TinkerPatchExtension)
mProject.tinkerPatch.extensions.create('buildConfig', TinkerBuildConfigExtension, mProject)
mProject.tinkerPatch.extensions.create('dex', TinkerDexExtension, mProject)
......省略不重要代碼
mProject.afterEvaluate {
......省略不重要代碼
android.applicationVariants.all { ApkVariant variant ->
def variantName = variant.name
def capitalizedVariantName = variantName.capitalize()
// 創(chuàng)建用于打差異包的task(tinkerPatchXXX)
TinkerPatchSchemaTask tinkerPatchBuildTask = mProject.tasks.create("tinkerPatch${capitalizedVariantName}", TinkerPatchSchemaTask)
tinkerPatchBuildTask.signConfig = variant.signingConfig
// 獲取android gradle plugin用于合并Manifest文件的task(ProcessXXXManifest)
def agpProcessManifestTask = Compatibilities.getProcessManifestTask(project, variant)
// 創(chuàng)建tinkerManifestTask(tinkerProcessXXXManifest)
def tinkerManifestTask = mProject.tasks.create("tinkerProcess${capitalizedVariantName}Manifest", TinkerManifestTask)
// 確保TinkerManifestTask在ProcessXXXManifest之后執(zhí)行,對合并合的manifest文件做處理
// ProcessXXXManifest -> TinkerManifestTask
tinkerManifestTask.mustRunAfter agpProcessManifestTask
variant.outputs.each { variantOutput ->
// 設(shè)置TinkerPatchSchemaTask的newApk路徑,如果沒有配置的話則將項(xiàng)目的apk輸出路徑作為默認(rèn)值
// 并讓TinkerPatchSchemaTask依賴于assemble task(打包任務(wù)),將打出來的包作為newApk(oldApk路徑則一定要配置)
setPatchNewApkPath(configuration, variantOutput, variant, tinkerPatchBuildTask)
// 設(shè)置差異包輸出路徑
setPatchOutputFolder(configuration, variantOutput, variant, tinkerPatchBuildTask)
def outputName = variantOutput.dirName
if (outputName.endsWith("/")) {
outputName = outputName.substring(0, outputName.length() - 1)
}
if (tinkerManifestTask.outputNameToManifestMap.containsKey(outputName)) {
throw new GradleException("Duplicate tinker manifest output name: '${outputName}'")
}
// 計(jì)算并保存各種變體包(渠道/debug/release)的manifest文件路徑,傳遞給TinkerManifestTask
def manifestPath = Compatibilities.getOutputManifestPath(project, agpProcessManifestTask, variantOutput)
tinkerManifestTask.outputNameToManifestMap.put(outputName, manifestPath)
}
// 獲取默認(rèn)打包流程中的processXXXResources task,這個(gè)任務(wù)作用是編譯所有資源文件
def agpProcessResourcesTask = project.tasks.findByName("process${capitalizedVariantName}Resources")
// 使processXXXResources task依賴于TinkerManifestTask
// 這樣就把TinkerManifestTask掛載在默認(rèn)打包流程中了
// 先執(zhí)行TinkerManifestTask處理manifest文件,再編譯資源
agpProcessResourcesTask.dependsOn tinkerManifestTask
// 創(chuàng)建TinkerResourceIdTask用于根據(jù)oldApk 資源id映射文件保持資源id
TinkerResourceIdTask applyResourceTask = mProject.tasks.create("tinkerProcess${capitalizedVariantName}ResourceId", TinkerResourceIdTask)
......
// processXXXResources task 同樣依賴于TinkerResourceIdTask
// 并且TinkerResourceIdTask在TinkerManifestTask之后執(zhí)行
// ProcessXXXManifest -> TinkerManifestTask -> TinkerResourceIdTask -> processXXXResources
applyResourceTask.mustRunAfter tinkerManifestTask
agpProcessResourcesTask.dependsOn applyResourceTask
// MergeResourcesTask作用是合并有所資源文件
def agpMergeResourcesTask = mProject.tasks.findByName("merge${capitalizedVariantName}Resources")
// 這里保證TinkerResourceIdTask在MergeResourcesTask完成后執(zhí)行
// 保證資源合并完成沒有ID沖突,這樣TinkerResourceIdTask才能正常工作
// mergeXXXResources -> ProcessXXXManifest -> TinkerManifestTask -> TinkerResourceIdTask -> processXXXResources
applyResourceTask.dependsOn agpMergeResourcesTask
.......
// 是否開啟了代碼優(yōu)化/混淆
boolean proguardEnable = variant.getBuildType().buildType.minifyEnabled
if (proguardEnable) {
// 創(chuàng)建TinkerProguardConfigTask,自定義的混淆配置加入到配置列表,并根據(jù)oldApk生成的的混淆映射文件來保持newApk的混淆方式
TinkerProguardConfigTask proguardConfigTask = mProject.tasks.create("tinkerProcess${capitalizedVariantName}Proguard", TinkerProguardConfigTask)
proguardConfigTask.applicationVariant = variant
// 保證tinker處理完manifest文件后再處理混淆邏輯
proguardConfigTask.mustRunAfter tinkerManifestTask
// 獲取默認(rèn)打包流程中的 混淆/壓縮優(yōu)化 代碼的task
// 不同gradle版本可能名稱不同,比如transformClassesAndResourcesWithProguardForXXX或者minifyXXXWithR8
def obfuscateTask = getObfuscateTask(variantName)
// 保證加入自定義的混淆配置之后再執(zhí)行混淆task
obfuscateTask.dependsOn proguardConfigTask
}
if (multiDexEnabled) {
// 創(chuàng)建TinkerMultidexConfigTask用于處理多dex情況下哪些類要保持在主dex中
TinkerMultidexConfigTask multidexConfigTask = mProject.tasks.create("tinkerProcess${capitalizedVariantName}MultidexKeep", TinkerMultidexConfigTask)
multidexConfigTask.applicationVariant = variant
// 獲取multiDex情況下默認(rèn)的keep配置文件,方便task寫入自定義規(guī)則到配置文件尾部
multidexConfigTask.multiDexKeepProguard = getManifestMultiDexKeepProguard(variant)
// 保證此任務(wù)最后執(zhí)行
multidexConfigTask.mustRunAfter tinkerManifestTask
multidexConfigTask.mustRunAfter agpProcessResourcesTask
// 獲取處理multidex的task
def agpMultidexTask = getMultiDexTask(variantName)
// 獲取壓縮和混淆代碼的task
def agpR8Task = getR8Task(variantName)
if (agpMultidexTask != null) {
// 保證插入自定義Multidex keep邏輯后再執(zhí)行multidex處理
agpMultidexTask.dependsOn multidexConfigTask
} else if (agpMultidexTask == null && agpR8Task != null) {
// 下列操作是為了處理agp3.4.0的一個(gè)Bug,該bug會導(dǎo)致multidex keep配置文件被R8忽略
// 導(dǎo)致本應(yīng)該保持在主dex中的類不存在,所以這里手動(dòng)將配置文件加入進(jìn)去使R8處理
agpR8Task.dependsOn multidexConfigTask
try {
Object r8Transform = agpR8Task.getTransform()
//R8 maybe forget to add multidex keep proguard file in agp 3.4.0, it's a agp bug!
//If we don't do it, some classes will not keep in maindex such as loader's classes.
//So tinker will not remove loader's classes, it will crashed in dalvik and will check TinkerTestDexLoad.isPatch failed in art.
if (r8Transform.metaClass.hasProperty(r8Transform, "mainDexRulesFiles")) {
File manifestMultiDexKeepProguard = getManifestMultiDexKeepProguard(variant)
if (manifestMultiDexKeepProguard != null) {
//see difference between mainDexRulesFiles and mainDexListFiles in https://developer.android.com/studio/build/multidex?hl=zh-cn
FileCollection originalFiles = r8Transform.metaClass.getProperty(r8Transform, 'mainDexRulesFiles')
if (!originalFiles.contains(manifestMultiDexKeepProguard)) {
FileCollection replacedFiles = mProject.files(originalFiles, manifestMultiDexKeepProguard)
mProject.logger.error("R8Transform original mainDexRulesFiles: ${originalFiles.files}")
mProject.logger.error("R8Transform replaced mainDexRulesFiles: ${replacedFiles.files}")
//it's final, use reflect to replace it.
replaceKotlinFinalField("com.android.build.gradle.internal.transforms.R8Transform", "mainDexRulesFiles", r8Transform, replacedFiles)
}
}
}
} catch (Exception ignore) {
//Maybe it's not a transform task after agp 3.6.0 so try catch it.
}
}
def collectMultiDexComponentsTask = getCollectMultiDexComponentsTask(variantName)
if (collectMultiDexComponentsTask != null) {
multidexConfigTask.mustRunAfter collectMultiDexComponentsTask
}
}
// 如果我們有多個(gè)dex,編譯補(bǔ)丁時(shí)可能會由于類的移動(dòng)導(dǎo)致變更增多。若打開keepDexApply模式,補(bǔ)丁包將根據(jù)基準(zhǔn)包的類分布來編譯。
if (configuration.buildConfig.keepDexApply
&& FileOperation.isLegalFile(mProject.tinkerPatch.oldApk)) {
com.tencent.tinker.build.gradle.transform.ImmutableDexTransform.inject(mProject, variant)
}
}
}
}
}
這里總結(jié)一下tinker四個(gè)打包時(shí)用到的task的執(zhí)行時(shí)機(jī)
- TinkerManifestTask在資源文件以及manifest文件合并完成后執(zhí)行,以便對最終的manifest文件做處理
- TinkerResourceIdTask在TinkerManifestTask之后processResourcesTask之前執(zhí)行,以便在編譯資源文件之前做一些處理
- TinkerProguardConfigTask在TinkerManifestTask之后代碼壓縮混淆之前執(zhí)行
- TinkerMultidexConfigTask在TinkerManifestTask之后multidex分dex之前執(zhí)行
下面依次來分析一下各個(gè)task的具體實(shí)現(xiàn)
TinkerManifestTask
此task做的事情很簡單,在mergeXXXResources task之后拿到merge過后的manifest文件,插入tinker_id,并讀取application類添加到tinker loader set中
public class TinkerManifestTask extends DefaultTask {
static final String TINKER_ID = "TINKER_ID"
static final String TINKER_ID_PREFIX = "tinker_id_"
@TaskAction
def updateManifest() {
// gradle中配置的tinker_id
String tinkerValue = project.extensions.tinkerPatch.buildConfig.tinkerId
boolean appendOutputNameToTinkerId = project.extensions.tinkerPatch.buildConfig.appendOutputNameToTinkerId
if (tinkerValue == null || tinkerValue.isEmpty()) {
throw new GradleException('tinkerId is not set!!!')
}
tinkerValue = TINKER_ID_PREFIX + tinkerValue
// build/intermediates目錄
def agpIntermediatesDir = new File(project.buildDir, 'intermediates')
outputNameToManifestMap.each { String outputName, File manifest ->
def manifestPath = manifest.getAbsolutePath()
def finalTinkerValue = tinkerValue
// 是否將變體名稱作為tinker_id的一部分
if (appendOutputNameToTinkerId && !outputName.isEmpty()) {
finalTinkerValue += "_${outputName}"
}
// 在manifest中插入meta-data節(jié)點(diǎn),name = TINKER_ID, value = 配置的tinker_id
writeManifestMeta(manifestPath, TINKER_ID, finalTinkerValue)
// 讀取manifest中application類,將application類和com.tencent.tinker.loader.xxx類添加到dex loader配置中
// dex loader中的類應(yīng)該被保持在主dex中,且不應(yīng)該被修改
addApplicationToLoaderPattern(manifestPath)
File manifestFile = new File(manifestPath)
if (manifestFile.exists()) {
def manifestRelPath = agpIntermediatesDir.toPath().relativize(manifestFile.toPath()).toString()
// 修改后的manifest文件拷貝到build/intermediates/tinker_intermediates/merged_manifests/xxx
def manifestDestPath = new File(project.file(TinkerBuildPath.getTinkerIntermediates(project)), manifestRelPath)
FileOperation.copyFileUsingStream(manifestFile, manifestDestPath)
project.logger.error("tinker gen AndroidManifest.xml in ${manifestDestPath}")
}
}
}
}
TinkerResourceIdTask
這個(gè)task主要是通過old apk的R.txt文件來保持new apk中資源ID的分配,主要步驟如下
獲取并解析old apk R.txt文件,裝入一個(gè)map
如果打包沒有開啟aapt2,則直接將原本的ids.xml和public.xml刪除,根據(jù)old apk R.txt文件重新生成
如果開啟了aapt2,先刪除原本的public.txt,然后根據(jù)R.txt重新生成public.txt
如果aapt2下需要為資源打public標(biāo)記,則需要再將public.txt轉(zhuǎn)換成public.xml,然后調(diào)用appt2對它進(jìn)行編譯得到flat文件,拷貝到mergeXXXResources目錄
關(guān)于aapt2對于資源處理的邏輯可以參考一下幾篇博客
aapt2 生成資源 public flag 標(biāo)記
public class TinkerResourceIdTask extends DefaultTask {
@TaskAction
def applyResourceId() {
// 獲取old apk的資源ID映射文件,它保存了各類資源的索引
// 這個(gè)文件默認(rèn)路徑是build/intermediates/(symbols或symbol_list或runtime_symbol_list)/xxx/R.txt
// 如果開啟了applyResourceMapping,我們需要將old apk中的這個(gè)文件復(fù)制出來,然后打差異包的時(shí)候指定其路徑
String resourceMappingFile = project.extensions.tinkerPatch.buildConfig.applyResourceMapping
// Parse the public.xml and ids.xml
if (!FileOperation.isLegalFile(resourceMappingFile)) {
project.logger.error("apply resource mapping file ${resourceMappingFile} is illegal, just ignore")
return
}
project.extensions.tinkerPatch.buildConfig.usingResourceMapping = true
// 解析R.txt,R.txt文件條目類似 int anim abc_slide_out_top 0x7f010009
// 前兩字節(jié)分別代表資源命名空間和類型,后兩字節(jié)表示資源在所處類型中的ID
// 這里的map key = 資源類型,value = 該類型所有資源項(xiàng)
Map<RDotTxtEntry.RType, Set<RDotTxtEntry>> rTypeResourceMap = PatchUtil.readRTxt(resourceMappingFile)
// 是否開啟了AAPT2,aapt2中由于編譯資源會生成中間文件(flat),所以保持資源ID的方式有所區(qū)別
if (!isAapt2EnabledCompat(project)) {
// 獲取res/values中定義的ids.xml和public.xml
String idsXml = resDir + "/values/ids.xml";
String publicXml = resDir + "/values/public.xml";
// 刪除原本的ids.xml和public.xml
FileOperation.deleteFile(idsXml);
FileOperation.deleteFile(publicXml);
List<String> resourceDirectoryList = new ArrayList<String>()
resourceDirectoryList.add(resDir)
// 根據(jù)old apk的R.txt重新生成public.xml和ids.xml
AaptResourceCollector aaptResourceCollector = AaptUtil.collectResource(resourceDirectoryList, rTypeResourceMap)
PatchUtil.generatePublicResourceXml(aaptResourceCollector, idsXml, publicXml)
File publicFile = new File(publicXml)
// public.xml和ids.xml拷貝到/intermediates/tinker_intermediates目錄等待apg下一步處理
if (publicFile.exists()) {
String resourcePublicXml = TinkerBuildPath.getResourcePublicXml(project)
FileOperation.copyFileUsingStream(publicFile, project.file(resourcePublicXml))
project.logger.error("tinker gen resource public.xml in ${resourcePublicXml}")
}
File idxFile = new File(idsXml)
if (idxFile.exists()) {
String resourceIdxXml = TinkerBuildPath.getResourceIdxXml(project)
FileOperation.copyFileUsingStream(idxFile, project.file(resourceIdxXml))
project.logger.error("tinker gen resource idx.xml in ${resourceIdxXml}")
}
} else {
// 刪除舊的public.txt,這個(gè)文件保存了資源名到ID的映射列表
// 如果aapt2編譯參數(shù)指定了--stable-ids xxx,則aapt2會使用使用該路徑的public.txt作為資源映射
// tinker這里在ensureStableIdsArgsWasInjected方法中指定了--stable-ids路徑
File stableIdsFile = project.file(TinkerBuildPath.getResourcePublicTxt(project))
FileOperation.deleteFile(stableIdsFile);
// 根據(jù)old apk的R.txt文件生成對應(yīng)的public.txt內(nèi)容
ArrayList<String> sortedLines = getSortedStableIds(rTypeResourceMap)
// 寫入內(nèi)容到public.txt
sortedLines?.each {
stableIdsFile.append("${it}\n")
}
// 獲取processXXXResources task,這個(gè)task是作用是編譯資源,生成R.java等文件的
def processResourcesTask = Compatibilities.getProcessResourcesTask(project, variant)
// 在aapt2編譯資源前創(chuàng)建public.txt以保持new apk中資源id的依照old apk分配
processResourcesTask.doFirst {
// 指定aapt2 --stable-ids路徑,讓aapt2通過tinker重寫的public.txt文件保持資源ID分配
ensureStableIdsArgsWasInjected(processResourcesTask)
// 是否要為資源打public標(biāo)記,供其他資源引用
// 這個(gè)配置從gradle.proplerties文件的tinker.aapt2.public字段讀取
if (project.hasProperty("tinker.aapt2.public")) {
addPublicFlagForAapt2 = project.ext["tinker.aapt2.public"]?.toString()?.toBoolean()
}
if (addPublicFlagForAapt2) {
// 在aapt2機(jī)制下,如果想要為資源打上public標(biāo)記
// 需要先將public.txt轉(zhuǎn)化成public.xml,然后使用aapt2將它編譯成flat中間文件
// 最后拷貝到mergeResourcesTask的輸出目錄
File publicXmlFile = project.file(TinkerBuildPath.getResourceToCompilePublicXml(project))
// public.txt轉(zhuǎn)化成public.xml
convertPublicTxtToPublicXml(stableIdsFile, publicXmlFile, false)
// 編譯成flat文件并拷貝到mergeXXXResources task輸出目錄
compileXmlForAapt2(publicXmlFile)
}
}
}
}
}
TinkerProguardConfigTask
這個(gè)task做的事情也很簡單
- 生成tinker_proguard.pro混淆配置文件
- 通過-applymapping指定混淆規(guī)則為old apk的混淆規(guī)則
- 將tinker loader相關(guān)類的混淆規(guī)則寫入配置文件,同時(shí)將dex loader中配置的類設(shè)為不混淆
- 將tinker_proguard.pro加入到agp混淆配置文件list,從而使new apk打包時(shí)應(yīng)用此配置
public class TinkerProguardConfigTask extends DefaultTask {
// tinker相關(guān)類混淆配置
static final String PROGUARD_CONFIG_SETTINGS = "..."
def applicationVariant
boolean shouldApplyMapping = true;
public TinkerProguardConfigTask() {
group = 'tinker'
}
@TaskAction
def updateTinkerProguardConfig() {
// build/intermediates/tinker_intermediates/tinker_proguard.pro
def file = project.file(TinkerBuildPath.getProguardConfigPath(project))
file.getParentFile().mkdirs()
FileWriter fr = new FileWriter(file.path)
// old apk的混淆映射文件路徑
// 這個(gè)文件默認(rèn)生成在build/outputs/mapping/xxx/mapping.txt,需要將old apk生成的這個(gè)文件拷貝出來并指定路徑
String applyMappingFile = project.extensions.tinkerPatch.buildConfig.applyMapping
if (shouldApplyMapping && FileOperation.isLegalFile(applyMappingFile)) {
project.logger.error("try add applymapping ${applyMappingFile} to build the package")
// 指定old apk混淆映射文件路徑,混淆代碼時(shí)會讀取這個(gè)文件將相應(yīng)類根據(jù)文件規(guī)則進(jìn)行混淆
// 如androidx.activity.ComponentActivity -> androidx.activity.b: ComponentActivity被混淆成b
fr.write("-applymapping " + applyMappingFile)
fr.write("\n")
} else {
project.logger.error("applymapping file ${applyMappingFile} is illegal, just ignore")
}
// 寫入tinker loader相關(guān)類混淆規(guī)則
fr.write(PROGUARD_CONFIG_SETTINGS)
fr.write("#your dex.loader patterns here\n")
// 保證build.gradle dex loader中配置的類不被混淆
Iterable<String> loader = project.extensions.tinkerPatch.dex.loader
for (String pattern : loader) {
if (pattern.endsWith("*") && !pattern.endsWith("**")) {
pattern += "*"
}
fr.write("-keep class " + pattern)
fr.write("\n")
}
fr.close()
// 將生成的混淆配置文件加入到混淆配置文件列表,agp會讀取這些文件混淆代碼
applicationVariant.getBuildType().buildType.proguardFiles(file)
......
}
}
TinkerMultidexConfigTask
這個(gè)task作用是將tinker loader相關(guān)類加入到multiDexKeepProguard文件中,保證這些類被打包在主dex中。
public class TinkerMultidexConfigTask extends DefaultTask {
// tinker multidex keep規(guī)則
static final String MULTIDEX_CONFIG_SETTINGS = "..."
def applicationVariant
// 默認(rèn)的keep配置文件
def multiDexKeepProguard
public TinkerMultidexConfigTask() {
group = 'tinker'
}
@TaskAction
def updateTinkerProguardConfig() {
// 創(chuàng)建keep文件,/intermediates/tinker_intermediates/tinker_multidexkeep.pro
File file = project.file(TinkerBuildPath.getMultidexConfigPath(project))
project.logger.error("try update tinker multidex keep proguard file with ${file}")
// Create the directory if it doesn't exist already
file.getParentFile().mkdirs()
// 寫入tinker需要保持在主dex的類配置
StringBuffer lines = new StringBuffer()
lines.append("\n")
.append("#tinker multidex keep patterns:\n")
.append(MULTIDEX_CONFIG_SETTINGS)
.append("\n")
lines.append("-keep class com.tencent.tinker.loader.TinkerTestAndroidNClassLoader {\n" +
" <init>(...);\n" +
"}\n")
.append("\n")
lines.append("#your dex.loader patterns here\n")
// 寫入開發(fā)者在build.gradle dex loader中配置的類
Iterable<String> loader = project.extensions.tinkerPatch.dex.loader
for (String pattern : loader) {
if (pattern.endsWith("*")) {
if (!pattern.endsWith("**")) {
pattern += "*"
}
}
lines.append("-keep class " + pattern + " {\n" +
" <init>(...);\n" +
"}\n")
.append("\n")
}
// keep規(guī)則寫入tinker_multidexkeep.pro文件
FileWriter fr = new FileWriter(file.path)
try {
for (String line : lines) {
fr.write(line)
}
} finally {
fr.close()
}
// 如果該模塊本來存在multiDexKeepProguard文件,則直接將上述規(guī)則添加到該文件結(jié)尾。
// 如果不存在multiDexKeepProguard文件,則需要將tinker_multidexkeep.pro文件拷貝到項(xiàng)目目錄,
// 并且在build.gradle defaultConfig中指定它的路徑。
if (multiDexKeepProguard == null) {
project.logger.error("auto add multidex keep pattern fail, you can only copy ${file} to your own multiDex keep proguard file yourself.")
return
}
FileWriter manifestWriter = new FileWriter(multiDexKeepProguard, true)
try {
for (String line : lines) {
manifestWriter.write(line)
}
} finally {
manifestWriter.close()
}
}
}
TinkerPatchSchemaTask
這個(gè)任務(wù)用于對比新舊apk得到差異包,先構(gòu)建差異包的參數(shù)和輸出路徑,然后調(diào)用Runner類tinkerPatch方法開始構(gòu)建,這里簡單看一下代碼捋捋大概流程
public class TinkerPatchSchemaTask extends DefaultTask {
@TaskAction
def tinkerPatch() {
// 檢查tinkerPatch需要的的配置參數(shù)(app build.gradle中配置的tinker參數(shù))
configuration.checkParameter()
configuration.buildConfig.checkParameter()
configuration.res.checkParameter()
configuration.dex.checkDexMode()
configuration.sevenZip.resolveZipFinalPath()
// 差異包任務(wù)配置參數(shù)
InputParam.Builder builder = new InputParam.Builder()
if (configuration.useSign) {
// 開啟了簽名打包的話要檢查app build.gradle是否配置了keystore
if (signConfig == null) {
throw new GradleException("can't the get signConfig for this build")
}
builder.setSignFile(signConfig.storeFile)
.setKeypass(signConfig.keyPassword)
.setStorealias(signConfig.keyAlias)
.setStorepass(signConfig.storePassword)
}
......
// 差異包輸出目錄(build/tmp/tinkerPatch)
def tmpDir = new File("${project.buildDir}/tmp/tinkerPatch")
tmpDir.mkdirs()
def outputDir = new File(outputFolder)
outputDir.mkdirs()
// 構(gòu)建差異包任務(wù)配置參數(shù)
builder.setOldApk(oldApk.getAbsolutePath())
.setNewApk(newApk.getAbsolutePath())
.setOutBuilder(tmpDir.getAbsolutePath())
......
// 是否加固應(yīng)用
.setIsProtectedApp(configuration.buildConfig.isProtectedApp)
// 需要處理的 dex, so, 資源文件的路徑
.setDexFilePattern(new ArrayList<String>(configuration.dex.pattern))
.setSoFilePattern(new ArrayList<String>(configuration.lib.pattern))
.setResourceFilePattern(new ArrayList<String>(configuration.res.pattern))
......
// 方舟編譯器配置
.setArkHotPath(configuration.arkHot.path)
.setArkHotName(configuration.arkHot.name)
InputParam inputParam = builder.create()
// 這里輸入?yún)?shù)開始打差異包
Runner.gradleRun(inputParam)
def prefix = newApk.name.take(newApk.name.lastIndexOf('.'))
tmpDir.eachFile(FileType.FILES) {
if (!it.name.endsWith(".apk")) {
return
}
// apk改名拷貝到output/apk/tinkerPatch
final File dest = new File(outputDir, "${prefix}-${it.name}")
it.renameTo(dest)
}
}
}
Runner
這個(gè)類在tinker-build-tinker-patch-lib中,主要在tinkerPatch方法中通過調(diào)用ApkDecoder類patch方法對比apk產(chǎn)生差異包
public class Runner {
protected void tinkerPatch() {
try {
// 這個(gè)類是比對通過各類Decoder比較兩個(gè)apk中各類文件的差異
ApkDecoder decoder = new ApkDecoder(mConfig);
decoder.onAllPatchesStart();
decoder.patch(mConfig.mOldApkFile, mConfig.mNewApkFile);
decoder.onAllPatchesEnd();
// 差異配置
PatchInfo info = new PatchInfo(mConfig);
info.gen();
// 將所有差異文件進(jìn)行壓縮得到patch.apk
// build/tmp/tinkerPatch/patch_xxx.apk
PatchBuilder builder = new PatchBuilder(mConfig);
builder.buildPatch();
} catch (Throwable e) {
goToError(e, ERRNO_USAGE);
}
}
}
ApkDecoder
這個(gè)類中包含ManifestDecoder,UniqueDexDiffDecoder,等各類Decoder,主要用于將apk中的manifest,dex等文件分別進(jìn)行比對,然后將得到的產(chǎn)物放在build/tmp/tinkerPatch/tinker_result文件夾中,等待下一步處理。
ApkDecoder patch方法中依次調(diào)用ManifestDecoder,DexDiffDecoder(dex對比),BsDiffDecoder(soPatchDecoder),ResDiffDecoder(資源文件對比),**ArkHotDecoder(方舟編譯器產(chǎn)物對比) **patch方法,先將對比得到的產(chǎn)物放在build/tmp/tinkerPatch/tinker_result文件夾,最后將各類差異文件打包成差異包。
這里我們主要看下ManifestDecoder,DexDiffDecoder,ResDiffDecoder做了哪些事情。
public class ApkDecoder extends BaseDecoder {
......
public ApkDecoder(Configuration config) throws IOException {
super(config);
this.mNewApkDir = config.mTempUnzipNewDir;
this.mOldApkDir = config.mTempUnzipOldDir;
// 元信息文件的路徑,路徑為build/tmp/tinkerPatch/tinker_result/assets/xxx_meta.txt
// xxx_meta.txt記錄新舊apk中各類文件的差異信息
String prePath = TypedValue.FILE_ASSETS + File.separator;
// manifest文件差異對比器
this.manifestDecoder = new ManifestDecoder(config);
// dex文件差異對比器
dexPatchDecoder = new UniqueDexDiffDecoder(config, prePath + TypedValue.DEX_META_FILE, TypedValue.DEX_LOG_FILE);
// so動(dòng)態(tài)庫差異對比器
soPatchDecoder = new BsDiffDecoder(config, prePath + TypedValue.SO_META_FILE, TypedValue.SO_LOG_FILE);
// 資源文件差異對比器
resPatchDecoder = new ResDiffDecoder(config, prePath + TypedValue.RES_META_TXT, TypedValue.RES_LOG_FILE);
// 方舟編譯器產(chǎn)物差異對比器
arkHotDecoder = new ArkHotDecoder(config, prePath + TypedValue.ARKHOT_META_TXT);
}
// 此方法產(chǎn)生差異包
@Override
public boolean patch(File oldFile, File newFile) throws Exception {
writeToLogFile(oldFile, newFile);
// 比較manifest文件
manifestDecoder.patch(oldFile, newFile);
// 解壓apk
unzipApkFiles(oldFile, newFile);
// 這里面的具體代碼不必深入,主要就是遍歷apk中的文件夾,提取dex、so、res文件,
// 調(diào)用dexPatchDecoder、soPatchDecoder、resPatchDecoder的patch方法
Files.walkFileTree(mNewApkDir.toPath(), new ApkFilesVisitor(config, mNewApkDir.toPath(), mOldApkDir.toPath(), dexPatchDecoder, soPatchDecoder, resPatchDecoder));
soPatchDecoder.onAllPatchesEnd();
dexPatchDecoder.onAllPatchesEnd();
manifestDecoder.onAllPatchesEnd();
resPatchDecoder.onAllPatchesEnd();
arkHotDecoder.onAllPatchesEnd();
......
return true;
}
}
ManifestDecoder
這個(gè)類做的事情很簡單,對比新舊manifest xml,找出新增的activity節(jié)點(diǎn)寫入差異manifest文件
public class ManifestDecoder extends BaseDecoder {
@Override
public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {
try {
........一些檢查
// 檢查是否更改了不能夠被修改的東西,有的話會拋出異常,比如包名,app名稱,app圖標(biāo)等
// 實(shí)際上本來就不支持Manifest文件的修改,1.9.0以后支持增加非export的Activity
// 所以其余修改要么在這里報(bào)錯(cuò),要么在后面被忽略掉
ensureApkMetaUnchanged(oldAndroidManifest.apkMeta, newAndroidManifest.apkMeta);
// 統(tǒng)計(jì)新增的四大組件
final Set<String> incActivities = getIncrementActivities(oldAndroidManifest.activities, newAndroidManifest.activities);
......
final boolean hasIncComponent = (!incActivities.isEmpty() || !incServices.isEmpty()
|| !incProviders.isEmpty() || !incReceivers.isEmpty());
// gradle中配置了SupportHotplugComponent為true才支持新增組件
if (!config.mSupportHotplugComponent && hasIncComponent) {
......
}
if (hasIncComponent) {
final Document newXmlDoc = DocumentHelper.parseText(newAndroidManifest.xml);
// 創(chuàng)建差異xml文件
final Document incXmlDoc = DocumentHelper.createDocument();
......
// 添加新增Activity節(jié)點(diǎn)
if (!incActivities.isEmpty()) {
final List<Element> newActivityNodes = newAppNode.elements(XML_NODENAME_ACTIVITY);
final List<Element> incActivityNodes = getIncrementActivityNodes(packageName, newActivityNodes, incActivities);
for (Element node : incActivityNodes) {
incAppNode.add(node.detach());
}
}
if (!incServices.isEmpty()) {
final List<Element> newServiceNodes = newAppNode.elements(XML_NODENAME_SERVICE);
// 新增其他三大組件預(yù)留方法,由于目前不支持,這個(gè)方法中會拋異常
final List<Element> incServiceNodes = getIncrementServiceNodes(packageName, newServiceNodes, incServices);
for (Element node : incServiceNodes) {
incAppNode.add(node.detach());
}
}
......
// 差異manifest寫入到build/tmp/tinkerPatch/tinker_result/assets/inc_component_meta.txt
final File incXmlOutput = new File(config.mTempResultDir, TypedValue.INCCOMPONENT_META_FILE);
......
return false;
}
}
DexDiffDecoder
這個(gè)類用于對比dex文件,主要在patch方法中對比收集dex中類的修改信息,然后在onAllPatchesEnd方法中生成差異dex,同時(shí)將相關(guān)信息寫入build/tmp/tinkerPatch/tinker_result/assets/dex_meta.txt,這里粗略看一下代碼
public class DexDiffDecoder extends BaseDecoder {
// 存放new apk中的新增類
// key = new apk中新增的類的描述信息,value = 該新增類所在的dex名稱
private final Map<String, String> addedClassDescToDexNameMap;
private final Map<String, String> deletedClassDescToDexNameMap;
// 新舊dex文件對
private final List<AbstractMap.SimpleEntry<File, File>> oldAndNewDexFilePairList;
// 存儲名稱為dexN的新舊dex相關(guān)信息(md5, 最終文件等)
private final Map<String, RelatedInfo> dexNameToRelatedInfoMap;
// 舊apk中的所有dex中的類描述信息
private final Set<String> descOfClassesInApk;
......
@Override
public boolean patch(final File oldFile, final File newFile) throws IOException, TinkerPatchException {
......
// 檢查是否有不應(yīng)該被修改的類(Application、tinker loader、以及build.gradle中配置在dex.loader中的類)修改了
excludedClassModifiedChecker.checkIfExcludedClassWasModifiedInNewDex(oldFile, newFile);
......
File dexDiffOut = getOutputPath(newFile).toFile();
// 新dex的md5
final String newMd5 = getRawOrWrappedDexMD5(newFile);
if (oldFile == null || !oldFile.exists() || oldFile.length() == 0) {
hasDexChanged = true;
// 如果新dex沒有對應(yīng)的舊dex,直接把新dex復(fù)制到輸出路徑(build/tmp/tinkerPatch/tinker_result)
// 并且寫入日志到dex_meta.txt
copyNewDexAndLogToDexMeta(newFile, newMd5, dexDiffOut);
return true;
}
// 解析舊dex中的類定義放入descOfClassesInApk set
collectClassesInDex(oldFile);
oldDexFiles.add(oldFile);
// 舊dex的md5
final String oldMd5 = getRawOrWrappedDexMD5(oldFile);
// 檢查新舊dex是否有更改
if ((oldMd5 != null && !oldMd5.equals(newMd5)) || (oldMd5 == null && newMd5 != null)) {
hasDexChanged = true;
if (oldMd5 != null) {
// 新舊dex有差異則收集差異類
// 新增類放入addedClassDescToDexNameMap,刪除類放入deletedClassDescToDexNameMap
collectAddedOrDeletedClasses(oldFile, newFile);
}
}
// 存儲舊dex以及它對應(yīng)的新dex的信息,為后面真正的patch操作做準(zhǔn)備
RelatedInfo relatedInfo = new RelatedInfo();
relatedInfo.oldMd5 = oldMd5;
relatedInfo.newMd5 = newMd5;
oldAndNewDexFilePairList.add(new AbstractMap.SimpleEntry<>(oldFile, newFile));
dexNameToRelatedInfoMap.put(dexName, relatedInfo);
}
}
@Override
public void onAllPatchesEnd() throws Exception {
// 檢查loader相關(guān)類(加載補(bǔ)丁時(shí)需要用到的類/配置在gradle loader中的類),是否引用了非loader相關(guān)類
// 如果loader類引用了其他可以被修改的類,那些類被補(bǔ)丁修改后由于和loader類可能不在同一個(gè)dex會導(dǎo)致異常
checkIfLoaderClassesReferToNonLoaderClasses();
if (config.mIsProtectedApp) {
// 如果會進(jìn)行加固,則將變更的整個(gè)類以及相關(guān)信息寫入patch dex
// Tag1--------------------
generateChangedClassesDexFile();
} else {
// 對于非加固app,使用dexDiff算法在更細(xì)的粒度下生成patch dex,補(bǔ)丁包更小
// Tag2-----------------------
generatePatchInfoFile();
}
//
addTestDex();
}
Tag1處generateChangedClassesDexFile方法用于對需要加固的app打補(bǔ)丁,此時(shí)不使用dexDiff算法,直接將變更類的全部信息都寫入到patch dex中了,因此這樣補(bǔ)丁包也會相對較大,這里簡單看下代碼
private void generateChangedClassesDexFile() throws IOException {
......
// 遍歷dex對,將new old dex拆開分別裝入list
for (AbstractMap.SimpleEntry<File, File> oldAndNewDexFilePair : oldAndNewDexFilePairList) {
File oldDexFile = oldAndNewDexFilePair.getKey();
File newDexFile = oldAndNewDexFilePair.getValue();
if (oldDexFile != null) {
oldDexList.add(oldDexFile);
}
if (newDexFile != null) {
newDexList.add(newDexFile);
}
}
DexGroup oldDexGroup = DexGroup.wrap(oldDexList);
DexGroup newDexGroup = DexGroup.wrap(newDexList);
ChangedClassesDexClassInfoCollector collector = new ChangedClassesDexClassInfoCollector();
// 排除loader相關(guān)類
collector.setExcludedClassPatterns(config.mDexLoaderPattern);
collector.setLogger(dexPatcherLoggerBridge);
// 引用了變更類的類也應(yīng)該被處理
collector.setIncludeRefererToRefererAffectedClasses(true);
// 通過這個(gè)類對比每對新舊dex,得到差異類
Set<DexClassInfo> classInfosInChangedClassesDex = collector.doCollect(oldDexGroup, newDexGroup);
// 差異類所屬的dex
Set<Dex> owners = new HashSet<>();
// 分別存儲每個(gè)dex中的差異類
Map<Dex, Set<String>> ownerToDescOfChangedClassesMap = new HashMap<>();
for (DexClassInfo classInfo : classInfosInChangedClassesDex) {
owners.add(classInfo.owner);
Set<String> descOfChangedClasses = ownerToDescOfChangedClassesMap.get(classInfo.owner);
if (descOfChangedClasses == null) {
descOfChangedClasses = new HashSet<>();
ownerToDescOfChangedClassesMap.put(classInfo.owner, descOfChangedClasses);
}
descOfChangedClasses.add(classInfo.classDesc);
}
StringBuilder metaBuilder = new StringBuilder();
int changedDexId = 1;
for (Dex dex : owners) {
// 遍歷dex,獲得該dex中差異類set
Set<String> descOfChangedClassesInCurrDex = ownerToDescOfChangedClassesMap.get(dex);
DexFile dexFile = new DexBackedDexFile(org.jf.dexlib2.Opcodes.forApi(20), dex.getBytes());
boolean isCurrentDexHasChangedClass = false;
for (org.jf.dexlib2.iface.ClassDef classDef : dexFile.getClasses()) {
if (descOfChangedClassesInCurrDex.contains(classDef.getType())) {
isCurrentDexHasChangedClass = true;
break;
}
}
// 當(dāng)前dex沒有被修改的類則跳過
if (!isCurrentDexHasChangedClass) {
continue;
}
// 構(gòu)建差異dex文件
DexBuilder dexBuilder = new DexBuilder(Opcodes.forApi(23));
for (org.jf.dexlib2.iface.ClassDef classDef : dexFile.getClasses()) {
// 遍歷過濾出在new dex中變更過的類
if (!descOfChangedClassesInCurrDex.contains(classDef.getType())) {
continue;
}
// 將變更過的類打包成差異dex
List<BuilderField> builderFields = new ArrayList<>();
......
dexBuilder.internClassDef(
classDef.getType(),
classDef.getAccessFlags(),
classDef.getSuperclass(),
classDef.getInterfaces(),
classDef.getSourceFile(),
classDef.getAnnotations(),
builderFields,
builderMethods
);
}
String changedDexName = null;
if (changedDexId == 1) {
changedDexName = "classes.dex";
} else {
changedDexName = "classes" + changedDexId + ".dex";
}
final File dest = new File(config.mTempResultDir + "/" + changedDexName);
final FileDataStore fileDataStore = new FileDataStore(dest);
// 重命名差異dex寫入build/tmp/tinkerPatch/tinker_result
dexBuilder.writeTo(fileDataStore);
final String md5 = MD5.getMD5(dest);
// 差異dex名稱、md5等信息寫入build/tmp/tinkerPatch/tinker_result/assets/dex_meta.txt
appendMetaLine(metaBuilder, changedDexName, "", md5, md5, 0, 0, 0, dexMode);
++changedDexId;
}
final String meta = metaBuilder.toString();
metaWriter.writeLineToInfoFile(meta);
}
Tag2處generatePatchInfoFile方法用于對不需要加固的app生成patch dex,使用dexDiff算法,將dex的變更信息具體到某一個(gè)操作,所以產(chǎn)生的補(bǔ)丁包體積比較小。
這一步有以下幾個(gè)步驟
- 首先對比新舊dex得到patch dex
- 將old dex和patch dex合成,將合成后的dex和原本的new dex做對比,驗(yàn)證patch dex是否能正確合成
- 使用合成后的dex生成一個(gè)crc校驗(yàn)和,寫入到dex_meta.txt中,以便app收到補(bǔ)丁合成后進(jìn)行校驗(yàn)是否正確合成
private void generatePatchInfoFile() throws IOException {
// 生成補(bǔ)丁
generatePatchedDexInfoFile();
// 將dex名稱、md5,合成后完整dex的校驗(yàn)和等信息寫入build/tmp/tinkerPatch/tinker_result/assets/dex_meta.txt
logDexesToDexMeta();
// 檢查是否有類從一個(gè)dex移動(dòng)到了另外一個(gè)dex中,這樣會導(dǎo)致補(bǔ)丁增大
checkCrossDexMovingClasses();
}
private void generatePatchedDexInfoFile() throws IOException {
for (AbstractMap.SimpleEntry<File, File> oldAndNewDexFilePair : oldAndNewDexFilePairList) {
File oldFile = oldAndNewDexFilePair.getKey();
File newFile = oldAndNewDexFilePair.getValue();
final String dexName = getRelativeDexName(oldFile, newFile);
RelatedInfo relatedInfo = dexNameToRelatedInfoMap.get(dexName);
if (!relatedInfo.oldMd5.equals(relatedInfo.newMd5)) {
// 新舊dex md5不同開始對比生成patch dex
diffDexPairAndFillRelatedInfo(oldFile, newFile, relatedInfo);
} else {
// 新舊dex相同時(shí)合成dex = new dex,方便統(tǒng)一校驗(yàn)
relatedInfo.newOrFullPatchedFile = newFile;
relatedInfo.newOrFullPatchedMd5 = relatedInfo.newMd5;
relatedInfo.newOrFullPatchedCRC = FileOperation.getFileCrc32(newFile);
}
}
}
private void diffDexPairAndFillRelatedInfo(File oldDexFile, File newDexFile, RelatedInfo relatedInfo) {
// patch dex 和 old dex合成后的輸出路徑build/tmp/tinkerPatch/tempPatchedDexes
File tempFullPatchDexPath = new File(config.mOutFolder + File.separator + TypedValue.DEX_TEMP_PATCH_DIR);
final String dexName = getRelativeDexName(oldDexFile, newDexFile);
// patch dex輸出路徑build/tmp/tinkerPatch/tinker_result
File dexDiffOut = getOutputPath(newDexFile).toFile();
ensureDirectoryExist(dexDiffOut.getParentFile());
try {
DexPatchGenerator dexPatchGen = new DexPatchGenerator(oldDexFile, newDexFile);
dexPatchGen.setAdditionalRemovingClassPatterns(config.mDexLoaderPattern);
// 開始生成patch dex保存到輸出目錄
dexPatchGen.executeAndSaveTo(dexDiffOut);
} catch (Exception e) {
throw new TinkerPatchException(e);
}
relatedInfo.dexDiffFile = dexDiffOut;
relatedInfo.dexDiffMd5 = MD5.getMD5(dexDiffOut);
// 合成后dex文件
File tempFullPatchedDexFile = new File(tempFullPatchDexPath, dexName);
if (!tempFullPatchedDexFile.exists()) {
ensureDirectoryExist(tempFullPatchedDexFile.getParentFile());
}
try {
// 合成old dex 和 patch dex
new DexPatchApplier(oldDexFile, dexDiffOut).executeAndSaveTo(tempFullPatchedDexFile);
Dex origNewDex = new Dex(newDexFile);
Dex patchedNewDex = new Dex(tempFullPatchedDexFile);
// 對比new dex和old dex合成補(bǔ)丁后的dex,不相同會拋異常
checkDexChange(origNewDex, patchedNewDex);
// RelatedInfo存儲合成后dex信息,md5和crc會在logDexesToDexMeta方法中寫入meta
relatedInfo.newOrFullPatchedFile = tempFullPatchedDexFile;
// md5
relatedInfo.newOrFullPatchedMd5 = MD5.getMD5(tempFullPatchedDexFile);
// crc校驗(yàn)和
relatedInfo.newOrFullPatchedCRC = FileOperation.getFileCrc32(tempFullPatchedDexFile);
}
......
}
ResDiffDecoder
此類在patch方法中通過BSDiff算法對比res目錄下的所有資源文件,生成差異文件,然后在onAllPatchesEnd方法中模擬一次資源文件合并,并將old apk中resources.arsc文件crc和合成后資源包中resources.arsc文件md5寫入res_meta.txt,以待app合成補(bǔ)丁時(shí)驗(yàn)證資源合成有效性。
public class ResDiffDecoder extends BaseDecoder {
// 新增資源
private ArrayList<String> addedSet;
// 刪除資源
private ArrayList<String> deletedSet;
// 更改的資源
private ArrayList<String> modifiedSet;
// 差異過大直接替換成新文件的資源
private ArrayList<String> largeModifiedSet;
// 不進(jìn)行壓縮的文件
private ArrayList<String> storedSet;
......
@Override
public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {
if (newFile == null || !newFile.exists()) {
String relativeStringByOldDir = getRelativePathStringToOldFile(oldFile);
// gradle中配置了res ignoreChange則忽略匹配的資源
if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, relativeStringByOldDir)) {
return false;
}
// 舊文件存在新文件不存在說明該文件是被刪除的
deletedSet.add(relativeStringByOldDir);
writeResLog(newFile, oldFile, TypedValue.DEL);
return true;
}
File outputFile = getOutputPath(newFile).toFile();
if (oldFile == null || !oldFile.exists()) {
// gradle中配置了res ignoreChange則忽略匹配的添加資源
if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) {
return false;
}
// 寫入添加的資源文件
FileOperation.copyFileUsingStream(newFile, outputFile);
addedSet.add(name);
writeResLog(newFile, oldFile, TypedValue.ADD);
return true;
}
......
// 新舊文件有修改
if (oldMd5 != null && oldMd5.equals(newMd5)) {
return false;
}
if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) {
return false;
}
// 忽略manifest文件
if (name.equals(TypedValue.RES_MANIFEST)) {
return false;
}
// arsc文件如果有修改,但是如果本質(zhì)內(nèi)容沒有改變的話也被忽略
if (name.equals(TypedValue.RES_ARSC)) {
if (AndroidParser.resourceTableLogicalChange(config)) {
return false;
}
}
// BSDiff算法計(jì)算生成差異文件,保存到輸出目錄tinker_result/res,并且記錄日志到res_meta.txt
dealWithModifyFile(name, newMd5, oldFile, newFile, outputFile);
return true;
}
}
@Override
public void onAllPatchesEnd() throws IOException, TinkerPatchException {
......
if (config.mUsingGradle) {
final boolean ignoreWarning = config.mIgnoreWarning;
final boolean resourceArscChanged = modifiedSet.contains(TypedValue.RES_ARSC)
|| largeModifiedSet.contains(TypedValue.RES_ARSC);
// 如果新舊arsc文件產(chǎn)生了變更,應(yīng)該指定舊apk資源id映射文件路徑,否則可能導(dǎo)致id錯(cuò)亂crash
if (resourceArscChanged && !config.mUseApplyResource) {
throw new TinkerPatchException(
String.format("ignoreWarning is false, but resources.arsc is changed, you should use applyResourceMapping mode to build the new apk, otherwise, it may be crash at some times")
}
}
......
File tempResZip = new File(config.mOutFolder + File.separator + TEMP_RES_ZIP);
final File tempResFiles = config.mTempResultDir;
// 將tinker_result路徑中的所有文件壓縮成zip
FileOperation.zipInputDir(tempResFiles, tempResZip, null);
// tinkerPatch/resources_out.zip 這個(gè)文件保存模擬合成的資源文件包
File extractToZip = new File(config.mOutFolder + File.separator + TypedValue.RES_OUT);
// 根據(jù)舊apk中的資源以及patch方法得到的差異資源模擬合成,并生成資源包zip的md5值
String resZipMd5 = Utils.genResOutputFile(extractToZip, tempResZip, config, addedSet, modifiedSet, deletedSet, largeModifiedSet, largeModifiedMap);
......
// old apk中resources.arsc文件的crc校驗(yàn)和,app加載補(bǔ)丁時(shí)驗(yàn)證用到
String arscBaseCrc = FileOperation.getZipEntryCrc(config.mOldApkFile, TypedValue.RES_ARSC);
// 合成后的resources.arsc文件的md5,這個(gè)md5值會被寫入res_meta
// app收到補(bǔ)丁合成資源之后要和這個(gè)md5值做對比驗(yàn)證是否正確合成
String arscMd5 = FileOperation.getZipEntryMd5(extractToZip, TypedValue.RES_ARSC);
if (arscBaseCrc == null || arscMd5 == null) {
throw new TinkerPatchException("can't find resources.arsc's base crc or md5");
}
// 舊resources.arsc文件crc,合并后resources.arsc文件md5寫入res_meta
// 示例resources_out.zip,2709624756,6f2e1f50344009c7a71afcdab1e94f0c
String resourceMeta = Utils.getResourceMeta(arscBaseCrc, arscMd5);
writeMetaFile(resourceMeta);
......
// res_meta中記錄哪些文件產(chǎn)生了變更
writeMetaFile(largeModifiedSet, TypedValue.LARGE_MOD);
writeMetaFile(modifiedSet, TypedValue.MOD);
writeMetaFile(addedSet, TypedValue.ADD);
writeMetaFile(deletedSet, TypedValue.DEL);
writeMetaFile(storedSet, TypedValue.STORED);
}
總結(jié)
通篇分析下來不難發(fā)現(xiàn)對于生成補(bǔ)丁包這一步,需要開發(fā)者對于android打包流程相關(guān)知識有不少的了解,隨著android gradle plugin版本的變遷,各種兼容處理也在所難免,需要反復(fù)去讀agp源碼,實(shí)在是一件耗時(shí)耗力的事情。
由于目的是了解補(bǔ)丁包的生成過程,我們只進(jìn)行了比較淺的分析,這里需要注意的有兩個(gè)點(diǎn)
- 生成patch dex時(shí),如果app需要加固的話是以類為粒度進(jìn)行dex差異對比的,非加固app則以dex操作為粒度,所以差異dex大小會有差異
- 對于非加固app,生成patch dex后會進(jìn)行模擬合成,得到校驗(yàn)值記錄下來,以便app拿到補(bǔ)丁后合成校驗(yàn)補(bǔ)丁是否正確合成,另外對于資源文件,也做了類似的有效性校驗(yàn)
- aapt2對于資源文件的一系列處理