Tinker工作流程
tinker热修复实现随着版本进行过不少改动,但是核心理念一直没变,主要是通过DexDiff算法对新旧APK dex文件比对得到差异patch.dex,然后下发patch.dex到客户端合成新dex代替旧dex达到热更,这里直接贴一张官方图。
这张图仅仅展现了冰山一角,对于apk资源文件,代码混淆,app加固,多dex等情况下的兼容处理也是不可忽视的点。作为一个用户,使用tinker也不算是一件简单的事情,了解得多使用起来才能更顺手,这篇先从补丁包的生成过程开始逐步分析tinker实现。
代码基于Tinker1.9.14.16
生成差异包
集成Tinker后可以在gradle面板中看到tinker相关的gradle task,一共有5个
tinker task 相关代码在gradle-plugin模块里面
可以看到一共有五个task,他们的作用如下
- TinkerManifestTask用于往manifest文件中插入tinker_id
- TinkerResourceIdTask 通过读取旧apk生成的R.txt文件(资源ID地址映射)来保持新apk资源ID的分配
- TnkerProguardConfigTask 读取旧apk的混淆规则映射文件来保持新apk的代码混淆规则
- TinkerMultidexConfigTask
- TinkerPatchSchemaTask用于比对新旧apk得到差异包
这五个task除了TinkerPatchSchemaTask以外,其他四个task都挂载到了app打包流程中,每次进行打包时执行,对apk中文件做特定处理。
我们先来分析TinkerPatchPlugin,看看这几个task执行的时机,然后依次分析各个task的作用
由于不想写太多篇幅,不太重要的点直接写在注释里面,不太重要的方法调用直接写结论,具体实现可以自行翻看方法实现
TinkerPatchPlugin
TinkerPatchPlugin中创建了tinker需要的各个gradle配置(extension)和上述的五个task,为它们配置一些必要参数,然后将各个task挂载在打包流程的各个阶段,简单看一下代码。
class TinkerPatchPlugin implements Plugin {
@Override
public void apply(Project project) {
// 这个插件用于检测操作系统名称和架构
try {
mProject.apply plugin: 'osdetector'
} catch (Throwable e) {
mProject.apply plugin: 'com.google.osdetector'
}
// 创建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()
// 创建用于打差异包的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)
// 创建tinkerManifestTask(tinkerProcessXXXManifest)
def tinkerManifestTask = mProject.tasks.create("tinkerProcess${capitalizedVariantName}Manifest", TinkerManifestTask)
// 确保TinkerManifestTask在ProcessXXXManifest之后执行,对合并合的manifest文件做处理
// ProcessXXXManifest -> TinkerManifestTask
tinkerManifestTask.mustRunAfter agpProcessManifestTask
variant.outputs.each { variantOutput ->
// 设置TinkerPatchSchemaTask的newApk路径,如果没有配置的话则将项目的apk输出路径作为默认值
// 并让TinkerPatchSchemaTask依赖于assemble task(打包任务),将打出来的包作为newApk(oldApk路径则一定要配置)
setPatchNewApkPath(configuration, variantOutput, variant, tinkerPatchBuildTask)
// 设置差异包输出路径
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}'")
}
// 计算并保存各种变体包(渠道/debug/release)的manifest文件路径,传递给TinkerManifestTask
def manifestPath = Compatibilities.getOutputManifestPath(project, agpProcessManifestTask, variantOutput)
tinkerManifestTask.outputNameToManifestMap.put(outputName, manifestPath)
}
// 获取默认打包流程中的processXXXResources task,这个任务作用是编译所有资源文件
def agpProcessResourcesTask = project.tasks.findByName("process${capitalizedVariantName}Resources")
// 使processXXXResources task依赖于TinkerManifestTask
// 这样就把TinkerManifestTask挂载在默认打包流程中了
// 先执行TinkerManifestTask处理manifest文件,再编译资源
agpProcessResourcesTask.dependsOn tinkerManifestTask
// 创建TinkerResourceIdTask用于根据oldApk 资源id映射文件保持资源id
TinkerResourceIdTask applyResourceTask = mProject.tasks.create("tinkerProcess${capitalizedVariantName}ResourceId", TinkerResourceIdTask)
......
// processXXXResources task 同样依赖于TinkerResourceIdTask
// 并且TinkerResourceIdTask在TinkerManifestTask之后执行
// ProcessXXXManifest -> TinkerManifestTask -> TinkerResourceIdTask -> processXXXResources
applyResourceTask.mustRunAfter tinkerManifestTask
agpProcessResourcesTask.dependsOn applyResourceTask
// MergeResourcesTask作用是合并有所资源文件
def agpMergeResourcesTask = mProject.tasks.findByName("merge${capitalizedVariantName}Resources")
// 这里保证TinkerResourceIdTask在MergeResourcesTask完成后执行
// 保证资源合并完成没有ID冲突,这样TinkerResourceIdTask才能正常工作
// mergeXXXResources -> ProcessXXXManifest -> TinkerManifestTask -> TinkerResourceIdTask -> processXXXResources
applyResourceTask.dependsOn agpMergeResourcesTask
.......
// 是否开启了代码优化/混淆
boolean proguardEnable = variant.getBuildType().buildType.minifyEnabled
if (proguardEnable) {
// 创建TinkerProguardConfigTask,自定义的混淆配置加入到配置列表,并根据oldApk生成的的混淆映射文件来保持newApk的混淆方式
TinkerProguardConfigTask proguardConfigTask = mProject.tasks.create("tinkerProcess${capitalizedVariantName}Proguard", TinkerProguardConfigTask)
proguardConfigTask.applicationVariant = variant
// 保证tinker处理完manifest文件后再处理混淆逻辑
proguardConfigTask.mustRunAfter tinkerManifestTask
// 获取默认打包流程中的 混淆/压缩优化 代码的task
// 不同gradle版本可能名称不同,比如transformClassesAndResourcesWithProguardForXXX或者minifyXXXWithR8
def obfuscateTask = getObfuscateTask(variantName)
// 保证加入自定义的混淆配置之后再执行混淆task
obfuscateTask.dependsOn proguardConfigTask
}
if (multiDexEnabled) {
// 创建TinkerMultidexConfigTask用于处理多dex情况下哪些类要保持在主dex中
TinkerMultidexConfigTask multidexConfigTask = mProject.tasks.create("tinkerProcess${capitalizedVariantName}MultidexKeep", TinkerMultidexConfigTask)
multidexConfigTask.applicationVariant = variant
// 获取multiDex情况下默认的keep配置文件,方便task写入自定义规则到配置文件尾部
multidexConfigTask.multiDexKeepProguard = getManifestMultiDexKeepProguard(variant)
// 保证此任务最后执行
multidexConfigTask.mustRunAfter tinkerManifestTask
multidexConfigTask.mustRunAfter agpProcessResourcesTask
// 获取处理multidex的task
def agpMultidexTask = getMultiDexTask(variantName)
// 获取压缩和混淆代码的task
def agpR8Task = getR8Task(variantName)
if (agpMultidexTask != null) {
// 保证插入自定义Multidex keep逻辑后再执行multidex处理
agpMultidexTask.dependsOn multidexConfigTask
} else if (agpMultidexTask == null && agpR8Task != null) {
// 下列操作是为了处理agp3.4.0的一个Bug,该bug会导致multidex keep配置文件被R8忽略
// 导致本应该保持在主dex中的类不存在,所以这里手动将配置文件加入进去使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
}
}
// 如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。
if (configuration.buildConfig.keepDexApply
&& FileOperation.isLegalFile(mProject.tinkerPatch.oldApk)) {
com.tencent.tinker.build.gradle.transform.ImmutableDexTransform.inject(mProject, variant)
}
}
}
}
}
这里总结一下tinker四个打包时用到的task的执行时机
- TinkerManifestTask在资源文件以及manifest文件合并完成后执行,以便对最终的manifest文件做处理
- TinkerResourceIdTask在TinkerManifestTask之后processResourcesTask之前执行,以便在编译资源文件之前做一些处理
- TinkerProguardConfigTask在TinkerManifestTask之后代码压缩混淆之前执行
- TinkerMultidexConfigTask在TinkerManifestTask之后multidex分dex之前执行
下面依次来分析一下各个task的具体实现
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节点,name = TINKER_ID, value = 配置的tinker_id
writeManifestMeta(manifestPath, TINKER_ID, finalTinkerValue)
// 读取manifest中application类,将application类和com.tencent.tinker.loader.xxx类添加到dex loader配置中
// dex loader中的类应该被保持在主dex中,且不应该被修改
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
这个task主要是通过old apk的R.txt文件来保持new apk中资源ID的分配,主要步骤如下
获取并解析old apk R.txt文件,装入一个map
如果打包没有开启aapt2,则直接将原本的ids.xml和public.xml删除,根据old apk R.txt文件重新生成
如果开启了aapt2,先删除原本的public.txt,然后根据R.txt重新生成public.txt
如果aapt2下需要为资源打public标记,则需要再将public.txt转换成public.xml,然后调用appt2对它进行编译得到flat文件,拷贝到mergeXXXResources目录
关于aapt2对于资源处理的逻辑可以参考一下几篇博客
aapt2 资源 compile 过程
aapt2 适配之资源 id 固定
aapt2 生成资源 public flag 标记
public class TinkerResourceIdTask extends DefaultTask {
@TaskAction
def applyResourceId() {
// 获取old apk的资源ID映射文件,它保存了各类资源的索引
// 这个文件默认路径是build/intermediates/(symbols或symbol_list或runtime_symbol_list)/xxx/R.txt
// 如果开启了applyResourceMapping,我们需要将old apk中的这个文件复制出来,然后打差异包的时候指定其路径
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
// 前两字节分别代表资源命名空间和类型,后两字节表示资源在所处类型中的ID
// 这里的map key = 资源类型,value = 该类型所有资源项
Map> rTypeResourceMap = PatchUtil.readRTxt(resourceMappingFile)
// 是否开启了AAPT2,aapt2中由于编译资源会生成中间文件(flat),所以保持资源ID的方式有所区别
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 resourceDirectoryList = new ArrayList()
resourceDirectoryList.add(resDir)
// 根据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,这个文件保存了资源名到ID的映射列表
// 如果aapt2编译参数指定了--stable-ids xxx,则aapt2会使用使用该路径的public.txt作为资源映射
// tinker这里在ensureStableIdsArgsWasInjected方法中指定了--stable-ids路径
File stableIdsFile = project.file(TinkerBuildPath.getResourcePublicTxt(project))
FileOperation.deleteFile(stableIdsFile);
// 根据old apk的R.txt文件生成对应的public.txt内容
ArrayList sortedLines = getSortedStableIds(rTypeResourceMap)
// 写入内容到public.txt
sortedLines?.each {
stableIdsFile.append("${it}\n")
}
// 获取processXXXResources task,这个task是作用是编译资源,生成R.java等文件的
def processResourcesTask = Compatibilities.getProcessResourcesTask(project, variant)
// 在aapt2编译资源前创建public.txt以保持new apk中资源id的依照old apk分配
processResourcesTask.doFirst {
// 指定aapt2 --stable-ids路径,让aapt2通过tinker重写的public.txt文件保持资源ID分配
ensureStableIdsArgsWasInjected(processResourcesTask)
// 是否要为资源打public标记,供其他资源引用
// 这个配置从gradle.proplerties文件的tinker.aapt2.public字段读取
if (project.hasProperty("tinker.aapt2.public")) {
addPublicFlagForAapt2 = project.ext["tinker.aapt2.public"]?.toString()?.toBoolean()
}
if (addPublicFlagForAapt2) {
// 在aapt2机制下,如果想要为资源打上public标记
// 需要先将public.txt转化成public.xml,然后使用aapt2将它编译成flat中间文件
// 最后拷贝到mergeResourcesTask的输出目录
File publicXmlFile = project.file(TinkerBuildPath.getResourceToCompilePublicXml(project))
// public.txt转化成public.xml
convertPublicTxtToPublicXml(stableIdsFile, publicXmlFile, false)
// 编译成flat文件并拷贝到mergeXXXResources task输出目录
compileXmlForAapt2(publicXmlFile)
}
}
}
}
}
TinkerProguardConfigTask
这个task做的事情也很简单
- 生成tinker_proguard.pro混淆配置文件
- 通过-applymapping指定混淆规则为old apk的混淆规则
- 将tinker loader相关类的混淆规则写入配置文件,同时将dex loader中配置的类设为不混淆
- 将tinker_proguard.pro加入到agp混淆配置文件list,从而使new apk打包时应用此配置
public class TinkerProguardConfigTask extends DefaultTask {
// tinker相关类混淆配置
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的混淆映射文件路径
// 这个文件默认生成在build/outputs/mapping/xxx/mapping.txt,需要将old apk生成的这个文件拷贝出来并指定路径
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混淆映射文件路径,混淆代码时会读取这个文件将相应类根据文件规则进行混淆
// 如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相关类混淆规则
fr.write(PROGUARD_CONFIG_SETTINGS)
fr.write("#your dex.loader patterns here\n")
// 保证build.gradle dex loader中配置的类不被混淆
Iterable 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
这个task作用是将tinker loader相关类加入到multiDexKeepProguard文件中,保证这些类被打包在主dex中。
public class TinkerMultidexConfigTask extends DefaultTask {
// tinker multidex keep规则
static final String MULTIDEX_CONFIG_SETTINGS = "..."
def applicationVariant
// 默认的keep配置文件
def multiDexKeepProguard
public TinkerMultidexConfigTask() {
group = 'tinker'
}
@TaskAction
def updateTinkerProguardConfig() {
// 创建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" +
" (...);\n" +
"}\n")
.append("\n")
lines.append("#your dex.loader patterns here\n")
// 写入开发者在build.gradle dex loader中配置的类
Iterable loader = project.extensions.tinkerPatch.dex.loader
for (String pattern : loader) {
if (pattern.endsWith("*")) {
if (!pattern.endsWith("**")) {
pattern += "*"
}
}
lines.append("-keep class " + pattern + " {\n" +
" (...);\n" +
"}\n")
.append("\n")
}
// keep规则写入tinker_multidexkeep.pro文件
FileWriter fr = new FileWriter(file.path)
try {
for (String line : lines) {
fr.write(line)
}
} finally {
fr.close()
}
// 如果该模块本来存在multiDexKeepProguard文件,则直接将上述规则添加到该文件结尾。
// 如果不存在multiDexKeepProguard文件,则需要将tinker_multidexkeep.pro文件拷贝到项目目录,
// 并且在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
这个任务用于对比新旧apk得到差异包,先构建差异包的参数和输出路径,然后调用Runner类tinkerPatch方法开始构建,这里简单看一下代码捋捋大概流程
public class TinkerPatchSchemaTask extends DefaultTask {
@TaskAction
def tinkerPatch() {
// 检查tinkerPatch需要的的配置参数(app build.gradle中配置的tinker参数)
configuration.checkParameter()
configuration.buildConfig.checkParameter()
configuration.res.checkParameter()
configuration.dex.checkDexMode()
configuration.sevenZip.resolveZipFinalPath()
// 差异包任务配置参数
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()
// 构建差异包任务配置参数
builder.setOldApk(oldApk.getAbsolutePath())
.setNewApk(newApk.getAbsolutePath())
.setOutBuilder(tmpDir.getAbsolutePath())
......
// 是否加固应用
.setIsProtectedApp(configuration.buildConfig.isProtectedApp)
// 需要处理的 dex, so, 资源文件的路径
.setDexFilePattern(new ArrayList(configuration.dex.pattern))
.setSoFilePattern(new ArrayList(configuration.lib.pattern))
.setResourceFilePattern(new ArrayList(configuration.res.pattern))
......
// 方舟编译器配置
.setArkHotPath(configuration.arkHot.path)
.setArkHotName(configuration.arkHot.name)
InputParam inputParam = builder.create()
// 这里输入参数开始打差异包
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
这个类在tinker-build-tinker-patch-lib中,主要在tinkerPatch方法中通过调用ApkDecoder类patch方法对比apk产生差异包
public class Runner {
protected void tinkerPatch() {
try {
// 这个类是比对通过各类Decoder比较两个apk中各类文件的差异
ApkDecoder decoder = new ApkDecoder(mConfig);
decoder.onAllPatchesStart();
decoder.patch(mConfig.mOldApkFile, mConfig.mNewApkFile);
decoder.onAllPatchesEnd();
// 差异配置
PatchInfo info = new PatchInfo(mConfig);
info.gen();
// 将所有差异文件进行压缩得到patch.apk
// build/tmp/tinkerPatch/patch_xxx.apk
PatchBuilder builder = new PatchBuilder(mConfig);
builder.buildPatch();
} catch (Throwable e) {
goToError(e, ERRNO_USAGE);
}
}
}
ApkDecoder
这个类中包含ManifestDecoder,UniqueDexDiffDecoder,等各类Decoder,主要用于将apk中的manifest,dex等文件分别进行比对,然后将得到的产物放在build/tmp/tinkerPatch/tinker_result
文件夹中,等待下一步处理。
ApkDecoder patch方法中依次调用ManifestDecoder,DexDiffDecoder(dex对比),BsDiffDecoder(soPatchDecoder),ResDiffDecoder(资源文件对比),**ArkHotDecoder(方舟编译器产物对比) **patch方法,先将对比得到的产物放在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动态库差异对比器
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);
// 方舟编译器产物差异对比器
arkHotDecoder = new ArkHotDecoder(config, prePath + TypedValue.ARKHOT_META_TXT);
}
// 此方法产生差异包
@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文件,
// 调用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
这个类做的事情很简单,对比新旧manifest xml,找出新增的activity节点写入差异manifest文件
public class ManifestDecoder extends BaseDecoder {
@Override
public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {
try {
........一些检查
// 检查是否更改了不能够被修改的东西,有的话会抛出异常,比如包名,app名称,app图标等
// 实际上本来就不支持Manifest文件的修改,1.9.0以后支持增加非export的Activity
// 所以其余修改要么在这里报错,要么在后面被忽略掉
ensureApkMetaUnchanged(oldAndroidManifest.apkMeta, newAndroidManifest.apkMeta);
// 统计新增的四大组件
final Set 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);
// 创建差异xml文件
final Document incXmlDoc = DocumentHelper.createDocument();
......
// 添加新增Activity节点
if (!incActivities.isEmpty()) {
final List newActivityNodes = newAppNode.elements(XML_NODENAME_ACTIVITY);
final List incActivityNodes = getIncrementActivityNodes(packageName, newActivityNodes, incActivities);
for (Element node : incActivityNodes) {
incAppNode.add(node.detach());
}
}
if (!incServices.isEmpty()) {
final List newServiceNodes = newAppNode.elements(XML_NODENAME_SERVICE);
// 新增其他三大组件预留方法,由于目前不支持,这个方法中会抛异常
final List 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
这个类用于对比dex文件,主要在patch方法中对比收集dex中类的修改信息,然后在onAllPatchesEnd方法中生成差异dex,同时将相关信息写入build/tmp/tinkerPatch/tinker_result/assets/dex_meta.txt,这里粗略看一下代码
public class DexDiffDecoder extends BaseDecoder {
// 存放new apk中的新增类
// key = new apk中新增的类的描述信息,value = 该新增类所在的dex名称
private final Map addedClassDescToDexNameMap;
private final Map deletedClassDescToDexNameMap;
// 新旧dex文件对
private final List> oldAndNewDexFilePairList;
// 存储名称为dexN的新旧dex相关信息(md5, 最终文件等)
private final Map dexNameToRelatedInfoMap;
// 旧apk中的所有dex中的类描述信息
private final Set descOfClassesInApk;
......
@Override
public boolean patch(final File oldFile, final File newFile) throws IOException, TinkerPatchException {
......
// 检查是否有不应该被修改的类(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没有对应的旧dex,直接把新dex复制到输出路径(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以及它对应的新dex的信息,为后面真正的patch操作做准备
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相关类(加载补丁时需要用到的类/配置在gradle loader中的类),是否引用了非loader相关类
// 如果loader类引用了其他可以被修改的类,那些类被补丁修改后由于和loader类可能不在同一个dex会导致异常
checkIfLoaderClassesReferToNonLoaderClasses();
if (config.mIsProtectedApp) {
// 如果会进行加固,则将变更的整个类以及相关信息写入patch dex
// Tag1--------------------
generateChangedClassesDexFile();
} else {
// 对于非加固app,使用dexDiff算法在更细的粒度下生成patch dex,补丁包更小
// Tag2-----------------------
generatePatchInfoFile();
}
//
addTestDex();
}
Tag1处generateChangedClassesDexFile方法用于对需要加固的app打补丁,此时不使用dexDiff算法,直接将变更类的全部信息都写入到patch dex中了,因此这样补丁包也会相对较大,这里简单看下代码
private void generateChangedClassesDexFile() throws IOException {
......
// 遍历dex对,将new old dex拆开分别装入list
for (AbstractMap.SimpleEntry 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相关类
collector.setExcludedClassPatterns(config.mDexLoaderPattern);
collector.setLogger(dexPatcherLoggerBridge);
// 引用了变更类的类也应该被处理
collector.setIncludeRefererToRefererAffectedClasses(true);
// 通过这个类对比每对新旧dex,得到差异类
Set classInfosInChangedClassesDex = collector.doCollect(oldDexGroup, newDexGroup);
// 差异类所属的dex
Set owners = new HashSet<>();
// 分别存储每个dex中的差异类
Map> ownerToDescOfChangedClassesMap = new HashMap<>();
for (DexClassInfo classInfo : classInfosInChangedClassesDex) {
owners.add(classInfo.owner);
Set 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 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;
}
}
// 当前dex没有被修改的类则跳过
if (!isCurrentDexHasChangedClass) {
continue;
}
// 构建差异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 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的变更信息具体到某一个操作,所以产生的补丁包体积比较小。
这一步有以下几个步骤
- 首先对比新旧dex得到patch dex
- 将old dex和patch dex合成,将合成后的dex和原本的new dex做对比,验证patch dex是否能正确合成
- 使用合成后的dex生成一个crc校验和,写入到dex_meta.txt中,以便app收到补丁合成后进行校验是否正确合成
private void generatePatchInfoFile() throws IOException {
// 生成补丁
generatePatchedDexInfoFile();
// 将dex名称、md5,合成后完整dex的校验和等信息写入build/tmp/tinkerPatch/tinker_result/assets/dex_meta.txt
logDexesToDexMeta();
// 检查是否有类从一个dex移动到了另外一个dex中,这样会导致补丁增大
checkCrossDexMovingClasses();
}
private void generatePatchedDexInfoFile() throws IOException {
for (AbstractMap.SimpleEntry 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相同时合成dex = new dex,方便统一校验
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合成补丁后的dex,不相同会抛异常
checkDexChange(origNewDex, patchedNewDex);
// RelatedInfo存储合成后dex信息,md5和crc会在logDexesToDexMeta方法中写入meta
relatedInfo.newOrFullPatchedFile = tempFullPatchedDexFile;
// md5
relatedInfo.newOrFullPatchedMd5 = MD5.getMD5(tempFullPatchedDexFile);
// crc校验和
relatedInfo.newOrFullPatchedCRC = FileOperation.getFileCrc32(tempFullPatchedDexFile);
}
......
}
ResDiffDecoder
此类在patch方法中通过BSDiff算法对比res目录下的所有资源文件,生成差异文件,然后在onAllPatchesEnd方法中模拟一次资源文件合并,并将old apk中resources.arsc文件crc和合成后资源包中resources.arsc文件md5写入res_meta.txt,以待app合成补丁时验证资源合成有效性。
public class ResDiffDecoder extends BaseDecoder {
// 新增资源
private ArrayList addedSet;
// 删除资源
private ArrayList deletedSet;
// 更改的资源
private ArrayList modifiedSet;
// 差异过大直接替换成新文件的资源
private ArrayList largeModifiedSet;
// 不进行压缩的文件
private ArrayList 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文件如果有修改,但是如果本质内容没有改变的话也被忽略
if (name.equals(TypedValue.RES_ARSC)) {
if (AndroidParser.resourceTableLogicalChange(config)) {
return false;
}
}
// BSDiff算法计算生成差异文件,保存到输出目录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文件产生了变更,应该指定旧apk资源id映射文件路径,否则可能导致id错乱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 这个文件保存模拟合成的资源文件包
File extractToZip = new File(config.mOutFolder + File.separator + TypedValue.RES_OUT);
// 根据旧apk中的资源以及patch方法得到的差异资源模拟合成,并生成资源包zip的md5值
String resZipMd5 = Utils.genResOutputFile(extractToZip, tempResZip, config, addedSet, modifiedSet, deletedSet, largeModifiedSet, largeModifiedMap);
......
// old apk中resources.arsc文件的crc校验和,app加载补丁时验证用到
String arscBaseCrc = FileOperation.getZipEntryCrc(config.mOldApkFile, TypedValue.RES_ARSC);
// 合成后的resources.arsc文件的md5,这个md5值会被写入res_meta
// app收到补丁合成资源之后要和这个md5值做对比验证是否正确合成
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中记录哪些文件产生了变更
writeMetaFile(largeModifiedSet, TypedValue.LARGE_MOD);
writeMetaFile(modifiedSet, TypedValue.MOD);
writeMetaFile(addedSet, TypedValue.ADD);
writeMetaFile(deletedSet, TypedValue.DEL);
writeMetaFile(storedSet, TypedValue.STORED);
}
总结
通篇分析下来不难发现对于生成补丁包这一步,需要开发者对于android打包流程相关知识有不少的了解,随着android gradle plugin版本的变迁,各种兼容处理也在所难免,需要反复去读agp源码,实在是一件耗时耗力的事情。
由于目的是了解补丁包的生成过程,我们只进行了比较浅的分析,这里需要注意的有两个点
- 生成patch dex时,如果app需要加固的话是以类为粒度进行dex差异对比的,非加固app则以dex操作为粒度,所以差异dex大小会有差异
- 对于非加固app,生成patch dex后会进行模拟合成,得到校验值记录下来,以便app拿到补丁后合成校验补丁是否正确合成,另外对于资源文件,也做了类似的有效性校验
- aapt2对于资源文件的一系列处理