Android 熱修復(fù)Tinker源碼分析(一)補(bǔ)丁包的生成

Tinker工作流程

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


hotfix flow

這張圖僅僅展現(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è)


image.png

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

image.png

可以看到一共有五個(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 資源 compile 過程

aapt2 適配之資源 id 固定

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做的事情也很簡單

  1. 生成tinker_proguard.pro混淆配置文件
  2. 通過-applymapping指定混淆規(guī)則為old apk的混淆規(guī)則
  3. 將tinker loader相關(guān)類的混淆規(guī)則寫入配置文件,同時(shí)將dex loader中配置的類設(shè)為不混淆
  4. 將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)用RunnertinkerPatch方法開始構(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)用ApkDecoderpatch方法對比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è)類中包含ManifestDecoderUniqueDexDiffDecoder,等各類Decoder,主要用于將apk中的manifest,dex等文件分別進(jìn)行比對,然后將得到的產(chǎn)物放在build/tmp/tinkerPatch/tinker_result文件夾中,等待下一步處理。

ApkDecoder patch方法中依次調(diào)用ManifestDecoderDexDiffDecoder(dex對比),BsDiffDecoder(soPatchDecoder),ResDiffDecoder(資源文件對比),**ArkHotDecoder(方舟編譯器產(chǎn)物對比) **patch方法,先將對比得到的產(chǎn)物放在build/tmp/tinkerPatch/tinker_result文件夾,最后將各類差異文件打包成差異包。

這里我們主要看下ManifestDecoderDexDiffDecoder,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();
}

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

Tag2generatePatchInfoFile方法用于對不需要加固的app生成patch dex,使用dexDiff算法,將dex的變更信息具體到某一個(gè)操作,所以產(chǎn)生的補(bǔ)丁包體積比較小。

這一步有以下幾個(gè)步驟

  1. 首先對比新舊dex得到patch dex
  2. 將old dex和patch dex合成,將合成后的dex和原本的new dex做對比,驗(yàn)證patch dex是否能正確合成
  3. 使用合成后的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)

  1. 生成patch dex時(shí),如果app需要加固的話是以類為粒度進(jìn)行dex差異對比的,非加固app則以dex操作為粒度,所以差異dex大小會有差異
  2. 對于非加固app,生成patch dex后會進(jìn)行模擬合成,得到校驗(yàn)值記錄下來,以便app拿到補(bǔ)丁后合成校驗(yàn)補(bǔ)丁是否正確合成,另外對于資源文件,也做了類似的有效性校驗(yàn)
  3. aapt2對于資源文件的一系列處理
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容