Welcome to follow the official wechat account: FSA Full stack action 👋

A, problem,

Tinker, Tencent’s hot repair solution, provides support for hardened applications. You need to configure isProtectedApp in gradle script to determine whether the current base package (BASE APK) is hardened APK, and this configuration is global. Tinker does not provide a separate configuration for multiple channels, which means that if your app project does not uniformly use hardened or unhardened across all channels, you will always have to consider whether you need to change the value of isProtectedApp when creating patches for online APK. In order to improve work efficiency and ensure the accuracy of the patches produced, it is necessary to solidify the value of isProtectedApp of each channel.

Second, grope for

Let’s have a preliminary understanding of isProtectedApp. What is its role? Note isProtectedApp in official Demo Tinker-sample-Android:

tinkerPatch {
    buildConfig {
        ...
        /** * optional, default 'false' * Whether tinker should treat the base apk as the one being protected by app * protection tools. * If this attribute is true, the generated patch package will contain a * dex including all changed classes instead of any dexdiff patch-info files. * /
        isProtectedApp = false // Whether to use hardening mode, only the changed classes are combined into patches. Note that this mode can only be used in hardened applications.}... }Copy the code

Code from: github.com/Tencent/tin…

IsProtectedApp is optional and defaults to false. The purpose is to let Tinker know whether the base APK should be treated as a protected APK hardened by the hardening tool. If true, the resulting patch pack will be a dex file containing all the changed classes, not the Dexdiff patch information file. In short, the generated patch pack files will be different.

The next step is to figure out how isProtectedApp is used inside Tinker. In order to find out this problem, I clone a copy of Tinker source code and study the generation process of patch. The following is the analysis and conclusion of the key process.

1, TinkerPatchPlugin

When generating patches, we need to execute tinkerPatchXXX task (such as tinkerPatchRelease, tinkerPatchXiaomiRelease) in Gradle panel, and then wait for Tinker to help us generate patch packages for corresponding channels. This function is provided by Gradle plug-in developed by Tinker. The corresponding plug-in class is TinkerPatchPlugin. The source code is as follows:

class TinkerPatchPlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {... mProject.afterEvaluate { ... android.applicationVariants.all { ApkVariant variant -> ...// Create tinkerPatchXXX task
            TinkerPatchSchemaTask tinkerPatchBuildTask = mProject.tasks.create("tinkerPatch${capitalizedVariantName}", TinkerPatchSchemaTask)
            tinkerPatchBuildTask.signConfig = variant.signingConfig

            variant.outputs.each { variantOutput ->
                // setPatchNewApkPath() has code that makes tinkerPatchXXX task dependent on assembleXXX task:
                // tinkerPatchBuildTask.dependsOn Compatibilities.getAssembleTask(mProject, variant)setPatchNewApkPath(configuration, variantOutput, variant, tinkerPatchBuildTask) setPatchOutputFolder(configuration, variantOutput, variant, tinkerPatchBuildTask) ... }}}}Copy the code

From the above source code, you can learn the following:

  • tinkerPatchXXXThe concrete realization of the task is inTinkerPatchSchemaTaskIn the class.
  • tinkerPatchXXXTask dependent onassembleXXXTask, so every time you patch, you rebuild it.

2, TinkerPatchSchemaTask

Let’s look at the tinkerPatchXXX task implementation class TinkerPatchSchemaTask:

public class TinkerPatchSchemaTask extends DefaultTask {
    @Internal
    TinkerPatchExtension configuration

    TinkerPatchSchemaTask(a) {... configuration = project.tinkerPatch }@TaskAction
    def tinkerPatch(a) {
        InputParam.Builder builder = new InputParam.Builder()
        ...
        for (def i = 0; i < newApks.size(); ++i) { ... builder.setOldApk(oldApk.getAbsolutePath()) .setNewApk(newApk.getAbsolutePath()) ... .setIsProtectedApp(configuration.buildConfig.isProtectedApp) InputParam inputParam = builder.create() Runner.gradleRun(inputParam) ... }}}Copy the code

In Gradle, regular tasks have methods that inherit from DefaultTask and are decorated with @TaskAction that are the execution logic of the Task. The method modified by @taskAction in TinkerPatchSchemaTask is tinkerPatch(), which is the concrete implementation of the tinkerPatchXXX task. In this method, we see isProtectedApp assigned to an InputParam instance and then passed to Runner. GradleRun (InputParam).

3, Runner

Following the Runner class, we can see that inputParam is eventually held by the Runner instance’s mConfig:

public class Runner {
    protected Configuration mConfig;

    public static void gradleRun(InputParam inputParam) {
        Runner m = new Runner(true);
        m.run(inputParam);
    }

    private void run(InputParam inputParam) {
        loadConfigFromGradle(inputParam);
        tinkerPatch();
    }

    private void loadConfigFromGradle(InputParam inputParam) {... mConfig =newConfiguration(inputParam); }}Copy the code

Runner. GradleRun (inputParam) will trigger the tinkerPatch() method by running () :

public class Runner {
	protectedConfiguration mConfig; .protected void tinkerPatch(a) {
        Logger.d("-----------------------Tinker patch begin-----------------------");

        Logger.d(mConfig.toString());
        try {
            //gen patch
            ApkDecoder decoder = new ApkDecoder(mConfig);
            decoder.onAllPatchesStart();
            decoder.patch(mConfig.mOldApkFile, mConfig.mNewApkFile);
            decoder.onAllPatchesEnd();

            //gen meta file and version file
            PatchInfo info = new PatchInfo(mConfig);
            info.gen();

            //build patch
            PatchBuilder builder = new PatchBuilder(mConfig);
            builder.buildPatch();

        } catch (Throwable e) {
            goToError(e, ERRNO_USAGE);
        }

        Logger.d("Tinker patch done, total time cost: %fs", diffTimeFromBegin());
        Logger.d("Tinker patch done, you can go to file to find the output %s", mConfig.mOutFolder);
        Logger.d("-----------------------Tinker patch end-------------------------"); }}Copy the code

TinkerPatch () is the core method for Tinker to generate patch packages, which is divided into three parts:

  • ApkDecoder: managementDecoderCollaborative generation of patch files (manifestDecoder,dexPatchDecoder,soPatchDecoder,resPatchDecoder)
  • PatchInfo.gen(): Generates meta files and version files
  • PatchBuilder.buildPatch(): Package the preceding patch files and information files into patch packages and signatures

And there are two places to use isProtectedApp, respectively in ApkDecoder, PatchInfo.

4. UniqueDexDiffDecoder & DexDiffDecoder

ApkDecoder manages various decoders, among which, dexPatchDecoder is UniqueDexDiffDecoder instance:

public class ApkDecoder extends BaseDecoder {
    private final UniqueDexDiffDecoder dexPatchDecoder;

    public ApkDecoder(Configuration config) throws IOException {
        dexPatchDecoder = newUniqueDexDiffDecoder(config, prePath + TypedValue.DEX_META_FILE, TypedValue.DEX_LOG_FILE); . }}public class UniqueDexDiffDecoder extends DexDiffDecoder {... }Copy the code

UniqueDexDiffDecoder inherits DexDiffDecoder, and the core logic generated by dex patch is in DexDiffDecoder:

public class DexDiffDecoder extends BaseDecoder {
    @Override
    public void onAllPatchesEnd(a) throws Exception {...if (config.mIsProtectedApp) {
            // For the hardened APP, write the changed classes and related information to the Patch dex
            generateChangedClassesDexFile();
        } else {
            // For non-hardened APPS, the Dexdiff algorithm is used to generate the patch dex, and the patch package is smallergeneratePatchInfoFile(); }... }}Copy the code

IsProtectedApp is used in DexDiffDecoder’s onAllPatchesEnd() method to generate dex patch files for both hardened and unhardened base apK. However, With generatePatchInfoFile generateChangedClassesDexFile () () the specific implementation details here, not interested to study, the difference between the two look at the above code comments. This is the specific code location corresponding to the official isProtectedApp explanation.

5. PatchInfo & PatchInfoGen

Finally, let’s look at another place where isProtectedApp is used. PatchInfo is the wrapper class for PatchInfoGen, and patchinfo.gen () is the final call to patchinfogen.gen () :

public class PatchInfo {

    private final PatchInfoGen infoGen;

    public PatchInfo(Configuration config) {
        infoGen = new PatchInfoGen(config);
    }

    /** * gen the meta file txt * such as rev, version ... * file version, hotpatch version class */
    public void gen(a) throws Exception { infoGen.gen(); }}public class PatchInfoGen {...public void gen(a) throws Exception { addTinkerID(); addProtectedAppFlag(); . }private void addProtectedAppFlag(a) {
        // If user happens to specify a value with this key, just override it for logic correctness.
        config.mPackageFields.put(TypedValue.PKGMETA_KEY_IS_PROTECTED_APP, config.mIsProtectedApp ? "1" : "0"); }}Copy the code

The addProtectedAppFlag() method is called in the patchinFogen.gen () method to convert Boolean isProtectedApp to the numbers 0 or 1, It will be saved to package_meta.txt.

Iii. Solutions

From the above source code analysis, we can know that isProtectedApp has two functions when generating patches:

  • forDexDiffDecoderDetermine the generation method of a DEX patch
  • forPatchInfoGendetermineis_protected_appThe value of, last recorded inpackage_meta.txtIn the file

In addition, in the above process, it can be found that the value of isProtectedApp used in each link ultimately comes from the same place, namely the Configuration attribute in TinkerPatchSchemaTask class. Configuration is a reference to project. TinkerPatch:

public class TinkerPatchSchemaTask extends DefaultTask {
    @Internal
    TinkerPatchExtension configuration

    TinkerPatchSchemaTask(a) {... configuration = project.tinkerPatch } }Copy the code

Don’t forget that TinkerPatchSchemaTask corresponds to the tinkerPatchXXX task. If we can perform the tinkerPatchXXX task before, Tamper with the value of isProtectedApp in project.tinkerPatch, then the tampered isProtectedApp will be used in the subsequent links. To do this, Gradle provides several task-related methods:

  • tasks.findByName(): Obtains the task based on the task name.
  • task.doFirst{}In:doFirst{}The code logic written in the closure is placed first in the execution phase of the Task.
  • afterEvaluate{}: Listening callback after the configuration phase is complete.

Combining the above apis, the final Gradle code looks like this:

apply from: 'configure/script/tinkerconfig.gradle' // Tinker gradle configuration, same as official Demo

// Note: the following code must be placed after the Tinker configuration, otherwise tasks.findByName cannot find the tinkerPatchXXX task
afterEvaluate {
    android.applicationVariants.all { variant ->
        // println "tinkerPatchTask ----> ${tasks.findByName("tinkerPatch${variant.name.capitalize()}")}"
        tasks.findByName("tinkerPatch${variant.name.capitalize()}").doFirst {

            / / print the original project. TinkerPatch. BuildConfig information
            println "original project.tinkerPatch --> ${project.tinkerPatch.buildConfig}"

            // Channel differentiation, xiaomi channel base APK will consolidate, other channels not
            def isProtectedApp
            if (variant.name.startsWith("xiaomi")) {
                isProtectedApp = true
            } else {
                isProtectedApp = false
            }

            println "change ${variant.name}'s `isProtectedApp` --> ${isProtectedApp}"
            project.tinkerPatch.buildConfig.isProtectedApp = isProtectedApp
        }
    }
}
Copy the code

Fourth, program optimization

The assignment of isProtectedApp is hard coded, and there is room for optimization. You can use functions and closure combinations to advance the assignment of isProtectedApp to productFlavors configuration.

// build.gradle
apply from: 'configure/script/basic.gradle'
android {
    productFlavors {
        xiaomi profileCommon(
            isProtectedApp: true
        ) >> {
            buildConfigField "String"."USER_ID".'"GitLqr"'
        }

        googlePlay profileCommon(
            isProtectedApp: false
        ) >> {
            buildConfigField "String"."USER_ID".'"CharyLin"'}}}Copy the code

Does that make it more intuitive? Here is the code in the basic.gradle script:

// basic.gradle
project.ext.variantIsProtectedAppMap = [:]
project.ext.profileCommon = { profiles = [:] ->
    return {
        def flavorName = delegate.name
        android.buildTypes.forEach {
            def variant = flavorName + it.name.capitalize() // xiaomiRelease
            project.ext.variantIsProtectedAppMap[variant] = profiles.getOrDefault('isProtectedApp'.false)
        }
    }
}

apply from: 'configure/script/tinkerconfig.gradle'
afterEvaluate {
    android.applicationVariants.all { variant ->
        // println "tinkerPatchTask ----> ${tasks.findByName("tinkerPatch${variant.name.capitalize()}")}"
        tasks.findByName("tinkerPatch${variant.name.capitalize()}").doFirst {
            println "original project.tinkerPatch --> ${project.tinkerPatch.buildConfig}"
            def isProtectedApp = variantIsProtectedAppMap[variant.name]
            println "change ${variant.name}'s `isProtectedApp` --> ${isProtectedApp}"
            project.tinkerPatch.buildConfig.isProtectedApp = isProtectedApp
        }
    }
}
Copy the code

At this point, Tinker’s multi-channel hardening configuration problem is solved. If you are not familiar with Gradle’s syntax, plugins, tasks, etc., read the following articles to learn about Gradle:

  • Gradle Features: juejin.cn/column/6987…

If this article is helpful to you, please click on my wechat official number: FSA Full Stack Action, which will be the biggest incentive for me. The public account not only has Android technology, but also iOS, Python and other articles, which may have some skills you want to know about oh ~