preface

The analysis of important classes is fully annotated, and the code is inGithub.com/ColorfulHor…

Tinker workflow

Tinker hot repair has been changed a lot with the version, but the core concept remains the same. It mainly compares the old and new APK dex files with the DexDiff algorithm to obtain the difference patch.dex, and then delivers patch.dex to the client to produce the new dex to replace the old dex to achieve hot change. Here’s an official picture.

This figure only shows the tip of the iceberg. Compatibility treatment in apK resource files, code confusion, APP reinforcement, multi-dex and other situations can not be ignored. As a user, it is not easy to use Tinker. If you know more about it, it will be easier to use it. This article will analyze the implementation of Tinker step by step from the generation process of patch package.

Code based on Tinker1.9.14.16

Generate difference packets

After integration with Tinker, you can see tinker-related Gradle tasks in the Gradle panel. There are five gradle tasks in total

Tinker Task-related code is in gradle-plugin module

You can see that there are five tasks in total, and their roles are as follows

  • The TinkerManifestTask is used to insert the tinker_id into the manifest file
  • TinkerResourceIdTask keeps the allocation of the new APK resource ID by reading the R.xt file (resource ID address mapping) generated by the old APK
  • TnkerProguardConfigTask reads the old APK obturation rule mapping file to preserve the code obturation rule of the new APK
  • TinkerMultidexConfigTask
  • TinkerPatchSchemaTask is used to compare the old and new APKs to get the difference packages

Except for TinkerPatchSchemaTask, the other four tasks are mounted in the APP packaging process and executed each time when the package is carried out to do specific processing for files in APK.

We will first analyze TinkerPatchPlugin, see the execution timing of these tasks, and then analyze the functions of each task in turn

Because I don’t want to write too much space, the less important points are directly written in the comments, the less important method calls are directly written in the conclusion, the specific implementation can be turned over to see the method implementation

TinkerPatchPlugin

TinkerPatchPlugin creates each Gradle configuration (extension) required by Tinker and the five tasks mentioned above, sets some necessary parameters for them, and then mounts each task at each stage of the packaging process for a brief look at the code.

class TinkerPatchPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) { 
        // This plug-in is used to detect the operating system name and schema
        try {
            mProject.apply plugin: 'osdetector'
        } catch (Throwable e) {
            mProject.apply plugin: 'com.google.osdetector'
        }
        // Create the tinkerPatch configuration in app build.gradle
        mProject.extensions.create('tinkerPatch', TinkerPatchExtension)
        mProject.tinkerPatch.extensions.create('buildConfig', TinkerBuildConfigExtension, mProject)
        mProject.tinkerPatch.extensions.create('dex', TinkerDexExtension, mProject) ...... Omit unimportant code mproject.afterevaluate {...... Omitting unimportant code android. ApplicationVariants. All {ApkVariant variant - > def variantName = variant. The name def capitalizedVariantName = variantName.capitalize()// Create a task for making difference packets (tinkerPatchXXX)
                TinkerPatchSchemaTask tinkerPatchBuildTask = mProject.tasks.create("tinkerPatch${capitalizedVariantName}", TinkerPatchSchemaTask)
                tinkerPatchBuildTask.signConfig = variant.signingConfig
                // Get android Gradle plugin task (ProcessXXXManifest)
                def agpProcessManifestTask = Compatibilities.getProcessManifestTask(project, variant)
                 // Create the tinkerManifestTask (tinkerProcessXXXManifest)
                def tinkerManifestTask = mProject.tasks.create("tinkerProcess${capitalizedVariantName}Manifest", TinkerManifestTask)
                // Ensure that the TinkerManifestTask is executed after ProcessXXXManifest, which processes the merged manifest file
                // ProcessXXXManifest -> TinkerManifestTask
                tinkerManifestTask.mustRunAfter agpProcessManifestTask

                variant.outputs.each { variantOutput ->
                    // Set the newApk path of TinkerPatchSchemaTask, or the apK output path of the project as the default if not configured
                    // And have TinkerPatchSchemaTask rely on Assemble Task as newApk (oldApk path must be configured)
                    setPatchNewApkPath(configuration, variantOutput, variant, tinkerPatchBuildTask)
                    // Set the differential packet output path
                    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}'")}// Calculate and save the manifest file path for each variant package (channel /debug/release) to the TinkerManifestTask
                    def manifestPath = Compatibilities.getOutputManifestPath(project, agpProcessManifestTask, variantOutput)
                    tinkerManifestTask.outputNameToManifestMap.put(outputName, manifestPath)
                }
                // Get the processXXXResources Task in the default packaging process, which compiles all resource files
                def agpProcessResourcesTask = project.tasks.findByName("process${capitalizedVariantName}Resources")
                // Make processXXXResources Task dependent on the TinkerManifestTask
                // You can mount the TinkerManifestTask into the default packaging process
                // Execute the TinkerManifestTask to process the manifest file, and then compile the resource
                agpProcessResourcesTask.dependsOn tinkerManifestTask
                
                // Create TinkerResourceIdTask to hold the resource ID according to oldApk resource ID mapping file
                TinkerResourceIdTask applyResourceTask = mProject.tasks.create("tinkerProcess${capitalizedVariantName}ResourceId", TinkerResourceIdTask)
                ......

                // processXXXResources Task also relies on TinkerResourceIdTask
                // And TinkerResourceIdTask is executed after the TinkerManifestTask
                // ProcessXXXManifest -> TinkerManifestTask -> TinkerResourceIdTask -> processXXXResources
                applyResourceTask.mustRunAfter tinkerManifestTask
                agpProcessResourcesTask.dependsOn applyResourceTask
				
                // MergeResourcesTask MergeResourcesTask merges resource files
                def agpMergeResourcesTask = mProject.tasks.findByName("merge${capitalizedVariantName}Resources")
                 // Make sure that the TinkerResourceIdTask executes after MergeResourcesTask is complete
                // Ensure that the resource merge completes without ID conflicts so that the TinkerResourceIdTask can work properly
                // mergeXXXResources -> ProcessXXXManifest -> TinkerManifestTask -> TinkerResourceIdTask -> processXXXResources
                applyResourceTask.dependsOn agpMergeResourcesTask
                .......
                
                // Whether code optimization/obfuscation is enabled
                boolean proguardEnable = variant.getBuildType().buildType.minifyEnabled

                if (proguardEnable) {
                    // Create TinkerProguardConfigTask, add the custom obturation configuration to the configuration list, and keep newApk obturation mode according to oldApk generated obturation mapping file
                    TinkerProguardConfigTask proguardConfigTask = mProject.tasks.create("tinkerProcess${capitalizedVariantName}Proguard", TinkerProguardConfigTask)
                    proguardConfigTask.applicationVariant = variant
                    // Make sure tinker processes the obfuscating logic after processing the manifest file
                    proguardConfigTask.mustRunAfter tinkerManifestTask
                    // Gets the task for obfuscating/compressing optimized code in the default packaging process
                    / / different gradle version may be a different name, such as transformClassesAndResourcesWithProguardForXXX or minifyXXXWithR8
                    def obfuscateTask = getObfuscateTask(variantName)
                    // Make sure to add the custom obfuscating configuration before executing the obfuscating task
                    obfuscateTask.dependsOn proguardConfigTask
                }
                
                if (multiDexEnabled) {
                    // Create TinkerMultidexConfigTask to handle which classes to keep in the main dex in the case of multiple dex
                    TinkerMultidexConfigTask multidexConfigTask = mProject.tasks.create("tinkerProcess${capitalizedVariantName}MultidexKeep", TinkerMultidexConfigTask)
                    multidexConfigTask.applicationVariant = variant
                    // Get the default Keep profile in multiDex case so that task can write custom rules to the end of the profile
                    multidexConfigTask.multiDexKeepProguard = getManifestMultiDexKeepProguard(variant)
                    // Ensure that this task is executed last
                    multidexConfigTask.mustRunAfter tinkerManifestTask
                    multidexConfigTask.mustRunAfter agpProcessResourcesTask
                    // Get the task that handles multidex
                    def agpMultidexTask = getMultiDexTask(variantName)
                    // Get tasks for compressing and obfuscating code
                    def agpR8Task = getR8Task(variantName)
                    if(agpMultidexTask ! =null) {
                        // Insert custom Multidex keep logic before Multidex processing
                        agpMultidexTask.dependsOn multidexConfigTask
                    } else if (agpMultidexTask == null&& agpR8Task ! =null) {
                        // The following operation is to deal with a Bug in AGP3.4.0 that causes the Multidex keep configuration file to be ignored by R8
                        // Classes that should remain in the main dex do not exist, so add the configuration file manually for R8 to handle
                        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 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
                    }
                }
				// If we have more than one dex, we may have more changes when compiling the patch due to class movement. When keepDexApply mode is turned on, the fix package is compiled based on the class distribution of the base package.
                if (configuration.buildConfig.keepDexApply
                        && FileOperation.isLegalFile(mProject.tinkerPatch.oldApk)) {
                    com.tencent.tinker.build.gradle.transform.ImmutableDexTransform.inject(mProject, variant)
                }
            }
        }
    }
}
Copy the code

Here is a summary of the execution timing of tinker’s four packaged tasks

  • The TinkerManifestTask is executed after the resource file and manifest file are merged to process the final MANIFEST file
  • The TinkerResourceIdTask is executed after the TinkerManifestTask and before the processResourcesTask in order to do some processing before compiling the resource file
  • TinkerProguardConfigTask is executed after the TinkerManifestTask code compression confuses
  • TinkerMultidexConfigTask Is executed after the TinkerManifestTask multidex is executed before the dex is divided

The specific implementation of each task will be analyzed in turn

Insert TinkerManifestTask tinker_id

This task simply takes the merged manifest file after the mergeXXXResources Task, inserts the Tinker_id, and reads the application class to add it to the 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(a) {
        // Tinker_id configured in Gradle
        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
            // Whether to include the variant name as part of tinker_id
            if(appendOutputNameToTinkerId && ! outputName.isEmpty()) { finalTinkerValue +="_${outputName}"
            }

            // Insert a meta-data node into the manifest with name = TINKER_ID, value = configured TINKER_ID
            writeManifestMeta(manifestPath, TINKER_ID, finalTinkerValue)
            / / read the manifest in the application class, application class and com. Tencent. Tinker. Loader. XXX class added to the dex loader configuration
            // Classes in the dex loader should be kept in the main dex and should not be modified
            addApplicationToLoaderPattern(manifestPath)
            File manifestFile = new File(manifestPath)
            if (manifestFile.exists()) {
                def manifestRelPath = agpIntermediatesDir.toPath().relativize(manifestFile.toPath()).toString()
                / / revised the manifest file copy to 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}")}}}}Copy the code

TinkerResourceIdTask keeps the resource ID assigned

This task maintains the allocation of resource ids in the new APK through the old APK’s r.txt file

  • Get and parse the old APK r.txt file and load a map

  • If aapT2 is not enabled in the package, the original ids. XML and public. XML are directly deleted and regenerated from the old APk r.txt file

  • If AAPT2 is enabled, delete the original public. TXT file and then regenerate public. TXT according to r.txt

  • If aapT2 needs to mark resources as public, convert public.txt to public.xml, and then call appt2 to compile the flat file and copy it to mergeXXXResources

The logic behind AAPT2’s handling of resources can be found in a few blogs

Compile process of AAPT2 resources

The resource ID of AAPT2 adaptation is fixed

Aapt2 generates the public flag for resources

public class TinkerResourceIdTask extends DefaultTask {
	@TaskAction
    def applyResourceId(a) {
        // Get the resource ID mapping file of old APk, which holds indexes of various resources
        // The default path for this file is build/intermediates/(symbols or symbol_list or runtime_symbol_list)/ XXX/r.tx
        // If applyResourceMapping is enabled, we need to copy this file from old APk and specify its path when making the differential package
        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
        // Parse R.t file entry like int anim abc_SLIde_out_TOP 0x7f010009
        // The first two bytes represent the resource namespace and type respectively, and the last two bytes represent the ID of the resource in its type
        // map key = resource type, value = all resource items of that type
        Map<RDotTxtEntry.RType, Set<RDotTxtEntry>> rTypeResourceMap = PatchUtil.readRTxt(resourceMappingFile)

        // Whether AAPT2 is enabled or not. In AAPT2, the way to keep resource IDS is different because compilation of resources produces intermediate files (flat)
        if(! isAapt2EnabledCompat(project)) {// Get ids. XML and public. XML defined in res/values
            String idsXml = resDir + "/values/ids.xml";
            String publicXml = resDir + "/values/public.xml";
            // Delete the original ids. XML and public. XML
            FileOperation.deleteFile(idsXml);
            FileOperation.deleteFile(publicXml);
            List<String> resourceDirectoryList = new ArrayList<String>()
            resourceDirectoryList.add(resDir)
            // Generate public. XML and ids. XML from old APk
            AaptResourceCollector aaptResourceCollector = AaptUtil.collectResource(resourceDirectoryList, rTypeResourceMap)
            PatchUtil.generatePublicResourceXml(aaptResourceCollector, idsXml, publicXml)
            File publicFile = new File(publicXml)
            // Public.xml and ids. XML are copied to the /intermediates/tinker_intermediates directory for further processing by 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 {
            // Delete the old public.txt file that holds the resource name to ID mapping list
            // If the AAPT2 compilation parameter specifies -- stables XXX, AAPT2 will use the public. TXT that uses this path as the resource map
            / / in ensureStableIdsArgsWasInjected method specifies the tinker here - stable - ids path
            File stableIdsFile = project.file(TinkerBuildPath.getResourcePublicTxt(project))
            FileOperation.deleteFile(stableIdsFile);
            // Generate the corresponding public. TXT content according to the old APk r.txt file
            ArrayList<String> sortedLines = getSortedStableIds(rTypeResourceMap)
            // Write the content to public.txtsortedLines? .each { stableIdsFile.append("${it}\n")}// Get processXXXResources Task, which is used to compile resources and generate files such as R. Java
            def processResourcesTask = Compatibilities.getProcessResourcesTask(project, variant)
            // Create public. TXT before AAPT2 compile resources to keep resource ids in new APK allocated according to old APK
            processResourcesTask.doFirst {
                // Specify aapT2 -- stables -- ids and let AAPT2 keep the resource ID allocation in the public. TXT file rewritten by Tinker
                ensureStableIdsArgsWasInjected(processResourcesTask)
                // Whether to mark the resource public for reference by other resources
                // This configuration is read from the tinker.aapt2.public field of the gradle.proplerties file
                if (project.hasProperty("tinker.aapt2.public")) {
                    addPublicFlagForAapt2 = project.ext["tinker.aapt2.public"]? .toString()? .toBoolean() }if (addPublicFlagForAapt2) {
                    // Under AAPT2, if you want to put a public tag on a resource
                    Public. TXT needs to be converted to public. XML first and then compiled into flat intermediate file using AAPT2
                    // Finally copy to the mergeResourcesTask output directory
                    File publicXmlFile = project.file(TinkerBuildPath.getResourceToCompilePublicXml(project))
                    // public. TXT converts to public. XML
                    convertPublicTxtToPublicXml(stableIdsFile, publicXmlFile, false)
                    // Compile to flat file and copy to mergeXXXResources Task output directory
                    compileXmlForAapt2(publicXmlFile)
                }
            }
        }
    }
}
Copy the code

TinkerProguardConfigTask handles code obfuscation configurations

What this task does is also simple

  1. Generates the tinker_proguard.pro obfuscation profile
  2. Using -applymapping to specify the obfuscation rule as old APK
  3. Write the confusion rules for Tinker Loader classes into the configuration file and set the classes configured in dex Loader to unconfuse
  4. Add tinker_ProGuard. pro to the AGP obturate profile list so that this configuration applies when new APK is packaged
public class TinkerProguardConfigTask extends DefaultTask {
	// Tinker-related classes confuse configurations
    static final String PROGUARD_CONFIG_SETTINGS = "..."
    def applicationVariant
    boolean shouldApplyMapping = true;
    public TinkerProguardConfigTask(a) {
        group = 'tinker'
    }

    @TaskAction
    def updateTinkerProguardConfig(a) {
        // 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 obfuscated map file path
        / / this file is generated by default in the build/outputs/mapping/XXX/mapping. TXT, need to old apk generated copy out this document and the specified path
        String applyMappingFile = project.extensions.tinkerPatch.buildConfig.applyMapping

        if (shouldApplyMapping && FileOperation.isLegalFile(applyMappingFile)) {
            project.logger.error("try add applymapping ${applyMappingFile} to build the package")
            // Specify the old APk obfuscation map file path. Obfuscation code will read this file and obfuscation the corresponding classes according to the file rules
            / / such as androidx.activity.Com ponentActivity - > androidx. Activity. B: ComponentActivity was confusion into b
            fr.write("-applymapping " + applyMappingFile)
            fr.write("\n")}else {
            project.logger.error("applymapping file ${applyMappingFile} is illegal, just ignore")}// Write the tinker Loader class obfuscation rule
        fr.write(PROGUARD_CONFIG_SETTINGS)

        fr.write("#your dex.loader patterns here\n")
        // Ensure that the classes configured in build.gradle dex loader are not confused
        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()
        // Add the generated obfuscation profile to the list of obfuscation profiles. Agp will read the obfuscation code of these filesapplicationVariant.getBuildType().buildType.proguardFiles(file) ...... }}Copy the code

TinkerMultidexConfigTask Maintains the primary Dex class

This task adds tinker Loader classes to the multiDexKeepProguard file to ensure that these classes are packaged in the main dex.

public class TinkerMultidexConfigTask extends DefaultTask {
    // Tinker multidex keep rule
    static final String MULTIDEX_CONFIG_SETTINGS = "..."
    
    def applicationVariant
    // The default keep profile
    def multiDexKeepProguard

    public TinkerMultidexConfigTask(a) {
        group = 'tinker'
    }

    @TaskAction
    def updateTinkerProguardConfig(a) {
        // Create the keep file, /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()
        // Writing tinker needs to remain in the class configuration of the main 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")
        // Write the classes configured by the developer in 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" +
                    " 
      
       (...) ; \n"
       +
                    "}\n")
                    .append("\n")}// Write the keep rule to the tinker_multidexkeep.pro file
        FileWriter fr = new FileWriter(file.path)
        try {
            for (String line : lines) {
                fr.write(line)
            }
        } finally {
            fr.close()
        }

        // If the module originally had a multiDexKeepProguard file, add the above rule directly to the end of the file.
        // If the multiDexKeepProguard file does not exist, copy the tinker_multidexkeep.pro file to the project directory.
        // Specify its path in 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()
        }
    }
}
Copy the code

TinkerPatchSchemaTask builds the difference package

This task is used to compare the old and new APK to get the difference package. First, build the parameters and output path of the difference package, and then call the tinkerPatch method of the Runner class to start the construction. Here is a brief look at the code to get the general process

public class TinkerPatchSchemaTask extends DefaultTask {
	@TaskAction
    def tinkerPatch(a) {
        // Check tinkerPatch configuration parameters (tinker parameters configured in app build.gradle)
        configuration.checkParameter()
        configuration.buildConfig.checkParameter()
        configuration.res.checkParameter()
        configuration.dex.checkDexMode()
        configuration.sevenZip.resolveZipFinalPath()
		// Differential packet task configuration parameters
        InputParam.Builder builder = new InputParam.Builder()
        if (configuration.useSign) {
            // If signature packing is enabled, check whether app build.gradle is configured with 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()
        // Build differential package task configuration parameters
        builder.setOldApk(oldApk.getAbsolutePath())
        .setNewApk(newApk.getAbsolutePath())
        .setOutBuilder(tmpDir.getAbsolutePath())
        ......
        // Whether to harden the application
        .setIsProtectedApp(configuration.buildConfig.isProtectedApp)
        // Path of dex, so, resource file to be processed
        .setDexFilePattern(new ArrayList<String>(configuration.dex.pattern))
        .setSoFilePattern(new ArrayList<String>(configuration.lib.pattern))
        .setResourceFilePattern(new ArrayList<String>(configuration.res.pattern))
        ......
        // Ark compiler configuration
        .setArkHotPath(configuration.arkHot.path)
        .setArkHotName(configuration.arkHot.name)
        
        InputParam inputParam = builder.create()
        // Enter parameters here to start packing difference packets
        Runner.gradleRun(inputParam)

        def prefix = newApk.name.take(newApk.name.lastIndexOf('. '))
        tmpDir.eachFile(FileType.FILES) {
            if(! it.name.endsWith(".apk")) {
                return
            }
            // copy apk to output/apk/tinkerPatch
            final File dest = new File(outputDir, "${prefix}-${it.name}")
            it.renameTo(dest)
        }
	}
}
Copy the code

Runner

This class in tinker-build-Tinker-patch-lib, mainly in tinkerPatch method by calling the patch method of ApkDecoder class comparison apK to generate difference package

public class Runner {
	protected void tinkerPatch(a) {
        try {
            // This class is to compare the differences between two APK files through various decoders
            ApkDecoder decoder = new ApkDecoder(mConfig);
            decoder.onAllPatchesStart();
            decoder.patch(mConfig.mOldApkFile, mConfig.mNewApkFile);
            decoder.onAllPatchesEnd();
            // Differential configuration
            PatchInfo info = new PatchInfo(mConfig);
            info.gen();
            // Compress all the difference files to get patch.apk
            // build/tmp/tinkerPatch/patch_xxx.apk
            PatchBuilder builder = new PatchBuilder(mConfig);
            builder.buildPatch();
        } catch(Throwable e) { goToError(e, ERRNO_USAGE); }}}Copy the code

ApkDecoder

This class contains ManifestDecoder, UniqueDexDiffDecoder, and other decoders. It is mainly used to compare manifest, dex, and other files in APK. Then will receive the product in the build/TMP/tinkerPatch/tinker_result folder, waiting for the next step.

The ApkDecoder patch method calls ManifestDecoder, DexDiffDecoder(dex contrast), BsDiffDecoder(soPatchDecoder), ResDiffDecoder(resource file contrast), ArkHotDecoder * * * * patch (ark compiler product contrast) method, first will get the product of contrast in the build/TMP/tinkerPatch/tinker_result folder, finally will be various differences files packaged into bags.

We focus here on the ManifestDecoder.

public class ApkDecoder extends BaseDecoder {...public ApkDecoder(Configuration config) throws IOException {
        super(config);
        this.mNewApkDir = config.mTempUnzipNewDir;
        this.mOldApkDir = config.mTempUnzipOldDir;

        / / yuan information file path, path to build/TMP/tinkerPatch/tinker_result/assets/xxx_meta. TXT
        // xxx_meta. TXT Records the difference information of various files in the old and new APK files
        String prePath = TypedValue.FILE_ASSETS + File.separator;
        // Manifest file difference comparator
        this.manifestDecoder = new ManifestDecoder(config);
        // dex file difference comparator
        dexPatchDecoder = new UniqueDexDiffDecoder(config, prePath + TypedValue.DEX_META_FILE, TypedValue.DEX_LOG_FILE);
        // so dynamic library difference comparator
        soPatchDecoder = new BsDiffDecoder(config, prePath + TypedValue.SO_META_FILE, TypedValue.SO_LOG_FILE);
        // Resource file difference comparator
        resPatchDecoder = new ResDiffDecoder(config, prePath + TypedValue.RES_META_TXT, TypedValue.RES_LOG_FILE);
        // Ark compiler product difference comparator
        arkHotDecoder = new ArkHotDecoder(config, prePath + TypedValue.ARKHOT_META_TXT);
    }
    
    // This method produces difference packets
    @Override
    public boolean patch(File oldFile, File newFile) throws Exception {
        writeToLogFile(oldFile, newFile);
        // Compare the manifest file
        manifestDecoder.patch(oldFile, newFile);
        / / decompression apk
        unzipApkFiles(oldFile, newFile);
        // This is not necessary to go into the details of the code, mainly is traversing the apK folder, extract dex, so, res files,
        // Call the patch method dexPatchDecoder, soPatchDecoder, resPatchDecoder
        Files.walkFileTree(mNewApkDir.toPath(), newApkFilesVisitor(config, mNewApkDir.toPath(), mOldApkDir.toPath(), dexPatchDecoder, soPatchDecoder, resPatchDecoder)); soPatchDecoder.onAllPatchesEnd(); dexPatchDecoder.onAllPatchesEnd(); manifestDecoder.onAllPatchesEnd(); resPatchDecoder.onAllPatchesEnd(); arkHotDecoder.onAllPatchesEnd(); .return true; }}Copy the code

ManifestDecoder

The simple thing this class does is compare the old and new manifest XML and find the new activity node to write to the different manifest file

public class ManifestDecoder extends BaseDecoder {
	@Override
    public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {
        try{... Some of the check// Check to see if you have changed something that cannot be modified. If so, raise an exception, such as package name, app name, app icon, etc
            // Manifest file changes are not actually supported. Since 1.9.0, non-export activities have been added
            // So the rest of the changes are either reported here or ignored later
            ensureApkMetaUnchanged(oldAndroidManifest.apkMeta, newAndroidManifest.apkMeta);

            // Count the four new components
            finalSet<String> incActivities = getIncrementActivities(oldAndroidManifest.activities, newAndroidManifest.activities); .final booleanhasIncComponent = (! incActivities.isEmpty() || ! incServices.isEmpty() || ! incProviders.isEmpty() || ! incReceivers.isEmpty());// A new component is supported only when SupportHotplugComponent is set to true in Gradle
            if(! config.mSupportHotplugComponent && hasIncComponent) { ...... }if (hasIncComponent) {
                final Document newXmlDoc = DocumentHelper.parseText(newAndroidManifest.xml);
                // Create the differences XML file
                finalDocument incXmlDoc = DocumentHelper.createDocument(); .// Add the new Activity node
                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);
                    // Add the other three component reservation methods, because currently not supported, this method will throw exceptions
                    final List<Element> incServiceNodes = getIncrementServiceNodes(packageName, newServiceNodes, incServices);
                    for(Element node : incServiceNodes) { incAppNode.add(node.detach()); }}.../ / differences manifest written to build/TMP/tinkerPatch/tinker_result/assets/inc_component_meta. TXT
                final File incXmlOutput = newFile(config.mTempResultDir, TypedValue.INCCOMPONENT_META_FILE); .return false; }}Copy the code

DexDiffDecoder

This class is used to compare dex files, mainly collecting the modified information of classes in DEX in the patch method, and then generating the differential dex in the onAllPatchesEnd method. At the same time to write information into the build/TMP/tinkerPatch/tinker_result/assets/dex_meta. TXT, a rough look at the code here

public class DexDiffDecoder extends BaseDecoder {
    // Store the new classes in new APk
    // key = new description of the new class in APK, value = name of the dex where the new class resides
    private final Map<String, String> addedClassDescToDexNameMap;
    private final Map<String, String> deletedClassDescToDexNameMap;
    // Old and new dex files are matched
    private final List<AbstractMap.SimpleEntry<File, File>> oldAndNewDexFilePairList;
    // Store information about the old and new dex named dexN (MD5, final file, etc.)
    private final Map<String, RelatedInfo> dexNameToRelatedInfoMap;
    // Class description of all dex in the old APK
    private finalSet<String> descOfClassesInApk; .@Override
    public boolean patch(final File oldFile, final File newFile) throws IOException, TinkerPatchException {...// Check if any classes (Application, Tinker Loader, and build.gradle classes configured in dex.loader) are modified that should not be modifiedexcludedClassModifiedChecker.checkIfExcludedClassWasModifiedInNewDex(oldFile, newFile); . File dexDiffOut = getOutputPath(newFile).toFile();// Md5 of the new dex
        final String newMd5 = getRawOrWrappedDexMD5(newFile);

        if (oldFile == null| |! oldFile.exists() || oldFile.length() ==0) {
            hasDexChanged = true;
            / / if there is no corresponding new dex old dex, new dex directly copied to the output path (build/TMP/tinkerPatch/tinker_result)
            // And write the log to dex_meta.txt
            copyNewDexAndLogToDexMeta(newFile, newMd5, dexDiffOut);
            return true;
        }

        // Parse class definitions in the old dex into descOfClassesInApk set
        collectClassesInDex(oldFile);
        oldDexFiles.add(oldFile);
        // Md5 of the old dex
        final String oldMd5 = getRawOrWrappedDexMD5(oldFile);
		// Check whether the old and new dex have changed
        if((oldMd5 ! =null && !oldMd5.equals(newMd5)) || (oldMd5 == null&& newMd5 ! =null)) {
            hasDexChanged = true;
            if(oldMd5 ! =null) {
                // If there is a difference between the old and new dex, collect the difference class
                / / add classes into addedClassDescToDexNameMap, delete classes into deletedClassDescToDexNameMapcollectAddedOrDeletedClasses(oldFile, newFile); }}// Store the information about the old dex and its corresponding new dex for the actual patch operation later
        RelatedInfo relatedInfo = new RelatedInfo();
        relatedInfo.oldMd5 = oldMd5;
        relatedInfo.newMd5 = newMd5;
        oldAndNewDexFilePairList.add(newAbstractMap.SimpleEntry<>(oldFile, newFile)); dexNameToRelatedInfoMap.put(dexName, relatedInfo); }}Copy the code
@Override
public void onAllPatchesEnd(a) throws Exception {
   	// Check whether loader-related classes (classes required to load patches/classes configured in Gradle Loader) reference non-Loader-related classes
    // If the Loader class references other classes that can be modified, the modified classes will cause exceptions because they may not be in the same dex as the Loader class
    checkIfLoaderClassesReferToNonLoaderClasses();
	
    if (config.mIsProtectedApp) {
        // If hardening is performed, the entire class and related information will be written to the patch dex
        // Tag1--------------------
        generateChangedClassesDexFile();
    } else {
        // For the non-hardened APP, the dexDiff algorithm is used to generate the patch dex with smaller patch package in finer granularity
        // Tag2-----------------------
        generatePatchInfoFile();
    }
    // Add test.dex. This dex is used to verify whether the patch is successfully synthesized
    addTestDex();
}
Copy the code

Tag1 generateChangedClassesDexFile method is used to need reinforcement of app patch, at this time do not use dexDiff algorithm, direct will change all the information is written to the patch in the dex, such patches will be relatively large, simple look at the code here

private void generateChangedClassesDexFile(a) throws IOException {...// Go through the dex pairs and separate the new old dex into the 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();
        // Exclude loader-related classes
        collector.setExcludedClassPatterns(config.mDexLoaderPattern);
        collector.setLogger(dexPatcherLoggerBridge);
        // Classes that reference changed classes should also be processed
        collector.setIncludeRefererToRefererAffectedClasses(true);
        // Use this class to compare each pair of old and new dex to get the difference class
        Set<DexClassInfo> classInfosInChangedClassesDex = collector.doCollect(oldDexGroup, newDexGroup);
        // Dex to which the difference class belongs
        Set<Dex> owners = new HashSet<>();
        // Store the difference classes in each dex separately
        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) {
            // Traverses the dex to obtain the difference class set in the dex
            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; }}// Classes whose dex has not been modified will be skipped
            if(! isCurrentDexHasChangedClass) {continue;
            }
            // Build the dex file
            DexBuilder dexBuilder = new DexBuilder(Opcodes.forApi(23));
            for (org.jf.dexlib2.iface.ClassDef classDef : dexFile.getClasses()) {
                // Filter out the changed classes in the new dex
                if(! descOfChangedClassesInCurrDex.contains(classDef.getType())) {continue;
                }

                // Package the changed classes into a differential dex
                List<BuilderField> builderFields = newArrayList<>(); . 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);
            / / rename differences dex in the build/TMP/tinkerPatch/tinker_result
            dexBuilder.writeTo(fileDataStore);
            final String md5 = MD5.getMD5(dest);
            / / difference dex information such as name, md5 write 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);
    }
Copy the code

The generatePatchInfoFile method at Tag2 is used to generate the patch dex for the app that does not need to be hardened. The dexDiff algorithm is used to specify the dex change information to a certain operation, so the size of the generated patch package is relatively small.

There are several steps in this step

  1. First, the patch DEX was obtained by comparing the old and new dex
  2. The old dex and the patch dex were synthesized, and the synthesized dex was compared with the original new dex to verify whether the patch dex could be synthesized correctly
  3. Use the synthesized dex to generate a CRC checksum and write it into dex_meta. TXT so that APP can verify the correct synthesis after receiving the patch synthesis
private void generatePatchInfoFile(a) throws IOException {
    // Generate a patch
    generatePatchedDexInfoFile();
    / / synthesize dex name, md5, after complete the information such as the checksum dex to build/TMP/tinkerPatch/tinker_result/assets/dex_meta. TXT
    logDexesToDexMeta();
    // Check if any classes have been moved from one dex to another, which causes the patch to grow
    checkCrossDexMovingClasses();
}
Copy the code
private void generatePatchedDexInfoFile(a) 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)) {// The patch dex is generated when the old and new dex md5 are different
            diffDexPairAndFillRelatedInfo(oldFile, newFile, relatedInfo);
        } else {
            // Synthesize dex = new dex at the same time of old and new dex to facilitate unified verificationrelatedInfo.newOrFullPatchedFile = newFile; relatedInfo.newOrFullPatchedMd5 = relatedInfo.newMd5; relatedInfo.newOrFullPatchedCRC = FileOperation.getFileCrc32(newFile); }}}Copy the code
private void diffDexPairAndFillRelatedInfo(File oldDexFile, File newDexFile, RelatedInfo relatedInfo) {
    / / patch dex and old dex synthesis after the build output path/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 output path/TMP/tinkerPatch/tinker_result
    File dexDiffOut = getOutputPath(newDexFile).toFile();
    ensureDirectoryExist(dexDiffOut.getParentFile());

    try {
        DexPatchGenerator dexPatchGen = new DexPatchGenerator(oldDexFile, newDexFile);
        dexPatchGen.setAdditionalRemovingClassPatterns(config.mDexLoaderPattern);
        // The patch dex is generated and saved to the output directory
        dexPatchGen.executeAndSaveTo(dexDiffOut);
    } catch (Exception e) {
        throw new TinkerPatchException(e);
    }
	
    relatedInfo.dexDiffFile = dexDiffOut;
    relatedInfo.dexDiffMd5 = MD5.getMD5(dexDiffOut);
    // Combine the dex file
    File tempFullPatchedDexFile = new File(tempFullPatchDexPath, dexName);
    if(! tempFullPatchedDexFile.exists()) { ensureDirectoryExist(tempFullPatchedDexFile.getParentFile()); }try {
        // Combine old dex and patch dex
        new DexPatchApplier(oldDexFile, dexDiffOut).executeAndSaveTo(tempFullPatchedDexFile);

        Dex origNewDex = new Dex(newDexFile);
        Dex patchedNewDex = new Dex(tempFullPatchedDexFile);
        // An exception will be thrown if the dex is different from the old dex
        checkDexChange(origNewDex, patchedNewDex);
		// RelatedInfo stores the synthesized dex. Md5 and CRC write meta in the logDexesToDexMeta method
        relatedInfo.newOrFullPatchedFile = tempFullPatchedDexFile;
        // md5
        relatedInfo.newOrFullPatchedMd5 = MD5.getMD5(tempFullPatchedDexFile);
        // CRC checksumrelatedInfo.newOrFullPatchedCRC = FileOperation.getFileCrc32(tempFullPatchedDexFile); }... }Copy the code

ResDiffDecoder

In the patch method, all resource files in the RES directory are compared by BSDiff algorithm to generate differential files, and then a resource file merger is simulated in the onAllPatchesEnd method. In addition, CRC of resources.arsc file in old APK and MD5 of resources.arsc file in synthesized resource package are written into res_meta. TXT to verify the effectiveness of resource synthesis when APP syntheses patch.

public class ResDiffDecoder extends BaseDecoder {
	// Add resources
	private ArrayList<String> addedSet;
	// Delete the resource
	private ArrayList<String> deletedSet;
	// Changed resources
    private ArrayList<String> modifiedSet;
    // If the difference is too large, replace the resource with the new file directly
    private ArrayList<String> largeModifiedSet;
    // Uncompressed files
    privateArrayList<String> storedSet; .@Override
    public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {
       if (newFile == null| |! newFile.exists()) { String relativeStringByOldDir = getRelativePathStringToOldFile(oldFile);// Gradle is configured with res ignoreChange to ignore matched resources
            if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, relativeStringByOldDir)) {
                return false;
            }
            // Old file exists new file does not exist indicates the file is deleted
            deletedSet.add(relativeStringByOldDir);
            writeResLog(newFile, oldFile, TypedValue.DEL);
            return true;
        }

        File outputFile = getOutputPath(newFile).toFile();

        if (oldFile == null| |! oldFile.exists()) {// Gradle is configured with res ignoreChange to ignore matched added resources
            if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) {
                return false;
            }
            // Write the added resource file
            FileOperation.copyFileUsingStream(newFile, outputFile);
            addedSet.add(name);
            writeResLog(newFile, oldFile, TypedValue.ADD);
            return true; }...// Old and new files have been modified
        if(oldMd5 ! =null && oldMd5.equals(newMd5)) {
            return false;
        }
        if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) {
            return false;
        }
        // Ignore the manifest file
        if (name.equals(TypedValue.RES_MANIFEST)) {
            return false;
        }
        // The arSC file is ignored if it has been modified, but if its essence has not changed
        if (name.equals(TypedValue.RES_ARSC)) {
            if (AndroidParser.resourceTableLogicalChange(config)) {
                return false; }}// The BSDiff algorithm calculates and generates the difference file, saves it to the output directory tinker_result/res, and logs it to res_meta.txt
        dealWithModifyFile(name, newMd5, oldFile, newFile, outputFile);
        return true; }}Copy the code
@Override
    public void onAllPatchesEnd(a) throws IOException, TinkerPatchException {...if (config.mUsingGradle) {
        final boolean ignoreWarning = config.mIgnoreWarning;
        final boolean resourceArscChanged = modifiedSet.contains(TypedValue.RES_ARSC)
            || largeModifiedSet.contains(TypedValue.RES_ARSC);
        // If the old and new ARSC files are changed, you should specify the old APK resource ID mapping file path, otherwise it may cause 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;
    // Zip all files in the tinker_result path
    FileOperation.zipInputDir(tempResFiles, tempResZip, null);
    // tinkerPatch/resources_out.zip This file holds simulated synthetic resource file packages
    File extractToZip = new File(config.mOutFolder + File.separator + TypedValue.RES_OUT);

    // Generate md5 value of resource package zip according to the resources in old APK and the differential resources obtained by patch methodString resZipMd5 = Utils.genResOutputFile(extractToZip, tempResZip, config, addedSet, modifiedSet, deletedSet, largeModifiedSet, largeModifiedMap); .// CRC checksum of resources. Arsc file in old APK, used when app loads patch
    String arscBaseCrc = FileOperation.getZipEntryCrc(config.mOldApkFile, TypedValue.RES_ARSC);
    // Md5 of the synthesized resources.arsc file. This MD5 value will be written to res_meta
    // The app receives the patch composition resource and compares it with the MD5 value to verify that the patch composition is correct
    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");
    }
    // Old resources.arsc file CRC, merged resources.arsc file MD5 write res_meta
    / / sample resources_out.zip, 2709624756, 6 f2e1f50344009c7a71afcdab1e94f0cString resourceMeta = Utils.getResourceMeta(arscBaseCrc, arscMd5); writeMetaFile(resourceMeta); .// Res_meta records which files have changed
    writeMetaFile(largeModifiedSet, TypedValue.LARGE_MOD);
    writeMetaFile(modifiedSet, TypedValue.MOD);
    writeMetaFile(addedSet, TypedValue.ADD);
    writeMetaFile(deletedSet, TypedValue.DEL);
    writeMetaFile(storedSet, TypedValue.STORED);
}
Copy the code

conclusion

Through the analysis down is not difficult to find that for the generation of patch package this step, developers need to have a lot of knowledge about the Android packaging process, with the android Gradle plugin version of the change, a variety of compatible processing is inevitable, need to repeatedly read AGP source code, it is a time and energy consuming thing.

Since the purpose is to understand the generation process of the patch pack, we have done a relatively shallow analysis, but there are two points to note here

  1. During the generation of patch dex, if the App needs to be hardened, the dex difference comparison is carried out based on the class granularity, while the non-hardened APP uses the dex operation as the granularity, so the size of the dex difference will be different
  2. For the non-hardened APP, simulation synthesis will be conducted after the generation of patch dex, and the verification value will be obtained and recorded, so that the APP can synthesize the patch to verify whether the patch is correctly synthesized. In addition, similar validity verification is made for resource files
  3. Aapt2 a series of processing of resource files