Android 热修复Tinker源码分析(一)补丁包的生成

Tinker工作流程

tinker热修复实现随着版本进行过不少改动,但是核心理念一直没变,主要是通过DexDiff算法对新旧APK dex文件比对得到差异patch.dex,然后下发patch.dex到客户端合成新dex代替旧dex达到热更,这里直接贴一张官方图。


hotfix flow

这张图仅仅展现了冰山一角,对于apk资源文件,代码混淆,app加固,多dex等情况下的兼容处理也是不可忽视的点。作为一个用户,使用tinker也不算是一件简单的事情,了解得多使用起来才能更顺手,这篇先从补丁包的生成过程开始逐步分析tinker实现。

代码基于Tinker1.9.14.16

生成差异包

集成Tinker后可以在gradle面板中看到tinker相关的gradle task,一共有5个


image.png

tinker task 相关代码在gradle-plugin模块里面

image.png

可以看到一共有五个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做的事情也很简单

  1. 生成tinker_proguard.pro混淆配置文件
  2. 通过-applymapping指定混淆规则为old apk的混淆规则
  3. 将tinker loader相关类的混淆规则写入配置文件,同时将dex loader中配置的类设为不混淆
  4. 将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得到差异包,先构建差异包的参数和输出路径,然后调用RunnertinkerPatch方法开始构建,这里简单看一下代码捋捋大概流程

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方法中通过调用ApkDecoderpatch方法对比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

这个类中包含ManifestDecoderUniqueDexDiffDecoder,等各类Decoder,主要用于将apk中的manifest,dex等文件分别进行比对,然后将得到的产物放在build/tmp/tinkerPatch/tinker_result文件夹中,等待下一步处理。

ApkDecoder patch方法中依次调用ManifestDecoderDexDiffDecoder(dex对比)BsDiffDecoder(soPatchDecoder),ResDiffDecoder(资源文件对比),**ArkHotDecoder(方舟编译器产物对比) **patch方法,先将对比得到的产物放在build/tmp/tinkerPatch/tinker_result文件夹,最后将各类差异文件打包成差异包。

这里我们主要看下ManifestDecoderDexDiffDecoderResDiffDecoder做了哪些事情。

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();
}

Tag1generateChangedClassesDexFile方法用于对需要加固的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);
    }

Tag2generatePatchInfoFile方法用于对不需要加固的app生成patch dex,使用dexDiff算法,将dex的变更信息具体到某一个操作,所以产生的补丁包体积比较小。

这一步有以下几个步骤

  1. 首先对比新旧dex得到patch dex
  2. 将old dex和patch dex合成,将合成后的dex和原本的new dex做对比,验证patch dex是否能正确合成
  3. 使用合成后的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源码,实在是一件耗时耗力的事情。

由于目的是了解补丁包的生成过程,我们只进行了比较浅的分析,这里需要注意的有两个点

  1. 生成patch dex时,如果app需要加固的话是以类为粒度进行dex差异对比的,非加固app则以dex操作为粒度,所以差异dex大小会有差异
  2. 对于非加固app,生成patch dex后会进行模拟合成,得到校验值记录下来,以便app拿到补丁后合成校验补丁是否正确合成,另外对于资源文件,也做了类似的有效性校验
  3. aapt2对于资源文件的一系列处理

你可能感兴趣的:(Android 热修复Tinker源码分析(一)补丁包的生成)