The advantages and disadvantages of Tinker hot repair, and why choose Tinker hot repair and Tinker implementation principle

Comparison of hot patch schemes

Tinker QZone AndFix (ali) Robust (Meituan)
Class to replace yes yes no no
So to replace yes no no no
Resources to replace yes yes no no
Full platform support yes yes yes yes
Effective immediately no no yes yes
Performance loss smaller larger smaller smaller
Patch pack size smaller larger general general
The development of transparent yes yes no no
The complexity of the The lower The lower complex complex
Gradle support yes no no no
Rom volume larger smaller smaller smaller
The success rate higher higher general The highest
  1. As a native solution, AndFix first faces stability and compatibility problems. More importantly, it cannot realize class replacement, which requires a lot of extra development costs. Dynamic replacement of Java layer in Native method, through the Native layer hook Java layer code.

  1. Robust(bytecode interpolation technology) has high compatibility and success rate, but it is similar to AndFix, which can only be used as bugFix scheme without adding variables and classes; For each function, a section of code is automatically inserted during the compilation and packaging phase, similar to a proxy that redirects the code executing the method to other methods.

  1. Qzone solution can release product functions, but its main problems are the performance problems of Dalvik caused by piling, and the rapid increase of patch packages in order to solve the memory address problem under Art. Qzone and Tinker work on a similar principleClassLoaderParent delegate mechanism (e.gjava.String(the system)com.String(custom) will loadjava.StringThis class, make custom over system so that only custom classes are loadedDex -> top of the pathList -> App)
  2. Tinker calculates the difference between dex in the specified Base Apk and dex in the modified Apk, and the content in the patch package is the difference between the two. Dex in the Base Apk and the patch package are combined during operation. Reboot and load the new combined dex file (Tinker must restart the App to take effect)

Especially since Android N, it has not been easy to fix the various solutions on the market due to the inline policy changes of mixed compilation. Tinker hotfix supports not only class, So, and resource replacement, but also full platform support for 2.x to 8.x (8.x supported above 1.9.0). Tinker can not only be used as a Bugfix, but also as a replacement for feature publishing.

Tinker restrictions

Due to principle and system limitations, Tinker has the following known problems:

  1. Tinker does not support modificationAndroidManifest.xml.TinkerFour new components are not supported (1.9.0 supports adding non-export activities).
  2. Due to Google Play’s developer terms, it is not recommended to dynamically update code in the GP channel;
  3. On Android N, the patch has a slight impact on app startup time;
  4. Some Samsung Android-21 models are not supported, and will be actively thrown when loading patches"TinkerRuntimeException: checkDexInstall failed";
  5. Resource replacement cannot be modifiedremoteView. For example,transitionAnimation,notification ``iconAs well asDesktop icon.
  6. Gradle is not compatible with the latest version of gradle because it is open source and free. Gradle must be under 4.0. Integration projects require a lot of time for validation and error correction, etc.

The use of the Tinker,

  1. Introducing plug-in dependencies
dependencies {
        classpath 'com. Android. Tools. Build: gradle: 3.5.3'
        classpath("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}") { changing = TINKER_VERSION? .endsWith("-SNAPSHOT")
            exclude group: 'com.android.tools.build'.module: 'gradle'
        }
    }
configurations.all {
        it.resolutionStrategy.cacheDynamicVersionsFor(5.'minutes')
        it.resolutionStrategy.cacheChangingModulesFor(0.'seconds')}Copy the code

Set tinker’s version and ID in gradle.properties (+1 order when generating dex package)

TINKER_VERSION=1.914.17.
TINKER_ID=1063Whether to enable tinker TINKER_ENABLE=trueIs it gradle3.The version of x GRADLE_3=true
Copy the code
  1. Introduce the Tinker core library
    api("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }

    // Maven local cannot handle transist dependencies.
    implementation("com.tencent.tinker:tinker-android-loader:${TINKER_VERSION}") { changing = true }

    annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    compileOnly("com.tencent.tinker:tinker-android-anno-support:${TINKER_VERSION}") { changing = true }
    // Multiple subcontracting dependencies
    implementation "Androidx. Multidex: multidex: 2.0.1."
Copy the code

DefaultConfig configuration

    defaultConfig {
        ......
        /** * You can use multiDex and install it in your ApplicationLifeCycle Implement */
        multiDexEnabled true
        // Handle tinker confusion
        multiDexKeepProguard file("tinker_multidexkeep.pro")
        /** * buildConfig can change during patch! * We can use the newly value when patch * configure patch information */
        buildConfigField "String"."MESSAGE"."\"I am the base apk\""
        /** * client version would update with patch * so we can get the newly git version easily! * Set TINKER_ID */
        buildConfigField "String"."TINKER_ID"."\"${TINKER_ID}\""
        /** * supports all platforms */
        buildConfigField "String"."PLATFORM"."\"all\""
    }
    / / how many open jumboMode
    dexOptions {
        jumboMode = true
    }
Copy the code
  1. Create the ApplicationLike proxy class
@DefaultLifeCycle(application = "tinker.sample.android.app.SampleApplication".// Automatically help generate the SampleApplication class, which is Application
                  flags = ShareConstants.TINKER_ENABLE_ALL,
                  loadVerifyFlag = false)
public class SampleApplicationLike extends DefaultApplicationLike {
    private static final String TAG = "Tinker.SampleApplicationLike";

    public SampleApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
                                 long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    }
    
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        //you must install multiDex whatever tinker is installed!
        MultiDex.install(base);

        SampleApplicationContext.application = getApplication();
        SampleApplicationContext.context = getApplication();
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public voidregisterActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) { getApplication().registerActivityLifecycleCallbacks(callback); }}Copy the code
  1. createTinkerManagement classTinkerManagerThis class is provided in Tinker’s Demohere
  2. inApplicationLikeIn theonBaseContextAttachedperformTinkerManager.installedTinker(this)
 		TinkerManager.setTinkerApplicationLike(this);

        TinkerManager.initFastCrashProtect();
        //should set before tinker is installed
        TinkerManager.setUpgradeRetryEnable(true);

        //optional set logIml, or you can use default debug log
        TinkerInstaller.setLogIml(new MyLogImp());

        //installTinker after load multiDex
        //or you can put com.tencent.tinker.** to main dex
        TinkerManager.installTinker(this);
        Tinker tinker = Tinker.with(getApplication());
Copy the code
  1. Configuring the Application and compiling the project results in an automatically generated Class of SampleApplication
public class SampleApplication extends TinkerApplication {

    public SampleApplication() {
        super(15."tinker.sample.android.app.SampleApplicationLike"."com.tencent.tinker.loader.TinkerLoader".false.false); }}Copy the code

Then set it to manifest

    <application
        android:name=".app.SampleApplication"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">...Copy the code
  1. Generate the base APK package

Tinker script configuration tinker.gradle, In general we only need to configure the ext tinkerEnabled, tinkerOldApkPath, tinkerApplyMappingPath, tinkerApplyResourcePath, tinkerBuildFlavorDirectory this several Options, other configurations generally do not change. Apply from: ‘tinker.gradle’

In normal development, tinkerEnabled is not required to be set to FALSE. If hot repair is needed, tinkerEnabled is required to be enabled to tag the base package (usually, the version of the APP must be prepared, even if the app is tagged after it goes online, the base package will be made according to the tag code in the future).

def bakPath = file("${buildDir}/bakApk/")

ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    tinkerEnabled = true
    // Baseline APK path
    tinkerOldApkPath = "${bakPath}/app-debug-0830-17-11-57.apk"
    // If obfuscation is not enabled, you do not need to enter this parameter
    tinkerApplyMappingPath = "${bakPath}/"
    // R file path in benchmark APk
    tinkerApplyResourcePath = "${bakPath}/app-debug-0830-17-11-57-R.txt"
    // If you fix the res file, specify your buggy version of the r.txt file
    tinkerBuildFlavorDirectory = "${bakPath}/app-debug-0830-17-11-57-R.txt"

}

// Get the old version path
def getOldApkPath() {
    return hasProperty("OLD_APK")? OLD_APK : ext.tinkerOldApkPath }// Obtain the mapping file path
def getApplyMappingPath() {
    return hasProperty("APPLY_MAPPING")? APPLY_MAPPING : ext.tinkerApplyMappingPath }// Obtain the resource mapping path
def getApplyResourceMappingPath() {
    return hasProperty("APPLY_RESOURCE")? APPLY_RESOURCE : ext.tinkerApplyResourcePath }// Whether Tinker is available or not
def buildWithTinker() {
    return hasProperty("TINKER_ENABLE")? TINKER_ENABLE : ext.tinkerEnabled }// Get the tinker subcontract directory
def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}



if (buildWithTinker()) {
    apply plugin: 'com.tencent.tinker.patch'

    tinkerPatch {
        /** * default null * associate old APK with new APK * add apk */ from build/bakApk
        oldApk = getOldApkPath()
        /** * Optional, default 'false' * In some cases we may receive some warning * if ignoreWarning is true, we just assert patching process * case 1: minSdkVersion is lower than 14, but you use dexMode with raw. * Case 2: add a new Android component in Androidmanifest.xml, * case 3: loader class in dex.loader {} does not remain in the main dex, * it must make Tinker not work. * case 4: The loader class in dex.loader {} is changed, * the loader class is loading the patch dex. It is useless to change them. * It won't crash, but these changes won't affect it. You can ignore it * Case 5: Resources.arsc has changed, but we don't use applyResourceMapping to build */
        ignoreWarning = true

        /** * Optional, default is "true" * whether to sign the patch file * If not, you must do it yourself. Otherwise, success cannot be checked during patch loading * we will use sign configuration with your build type */
        useSign = true

        /** * Optional, default is "true" * Whether to use tinker build */
        tinkerEnable = buildWithTinker()

        /** * Warning, applyMapping will affect normal Android builds! * /
        buildConfig {
            /** * Optional, default is 'null' * if we use tinkerPatch to build patch APK, you'd better apply the old * APK mapping file if minifyEnabled is enabled! * Warning: You must be careful, it will affect the normal assembly build! * /
            applyMapping = getApplyMappingPath()
            /** * Optional, defaults to 'null' * It is best to keep resource ids in the r.xt file to reduce Java changes */
            applyResourceMapping = getApplyResourceMappingPath()

            /** * required, default 'null' * because we don't want to check base APK versus MD5 at run time (it's slow) * tinkerId used to identify unique base APK when trying to apply patches. * We can use git rev, SVN rev or simply versionCode. * We will automatically generate tinkerId */ in your listing
            tinkerId = TINKER_ID.toInteger()

            /** * If keepDexApply is true, dex points to the old APK class. * Turn this on to reduce the dex Diff file size. * /
            keepDexApply = false

            /** * 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

            /** * optional, default 'false' * Whether tinker should support component hotplug (add new component dynamically). * If this attribute is true, the component added in new apk will be available after * patch is successfully loaded. Otherwise an error would be announced when generating patch * on compile-time. * * Notice that currently this feature is incubating and only support NON-EXPORTED Activity */
            supportHotplugComponent = false
        }

        dex {
            /** * Optional, default 'jar' * can only be 'raw' or 'jar'. For raw, we'll keep it in its original format * for JARS, we'll rewrap dexes in ZIP format. * If you want to support 14 below, you must use JAR * or you want to save ROM or check faster, you can also use raw mode */
            dexMode = "jar"

            /** * required, by default '[]' * dexes in APk should handle tinkerPatch * it supports * or? Mode. * /
            pattern = ["classes*.dex"."assets/secondary-dex-? .jar"]
            /** * required, default '[]' * warning, this is very very important, loading classes cannot change with patches. * Therefore, they will be removed from the patch. * You must put the following classes in the main dex. * in a nutshell, you should add your own application {@ code tinker. Sample. Android. SampleApplication} * own tinkerLoader, and the class you use * /
            loader = [
                    //use sample, let BaseBuildInfo unchangeable with tinker
                    "tinker.sample.android.app.BaseBuildInfo"
            ]
        }

        lib {
            /** * Optional, default '[]' * libraries in APK should handle tinkerPatch * it supports * or? Mode. * For the repository we just restore them in the patch directory * you can get them in TinkerLoadResult with Tinker */
            pattern = ["lib/*/*.so"]
        }

        res {
            /** * Optional, default '[]' * what resource in APK should handle tinkerPatch * it supports * or? Mode. * You must include all your resources here, * otherwise, they will not be repackaged in the new APK resources. * /
            pattern = ["res/*"."assets/*"."resources.arsc"."AndroidManifest.xml"]

            /** * Optional, defaults to '[]' * resource file exclusion mode, ignores adding, deleting or modifying resource changes ** It supports * or? Mode. * * warning, we can only use files without relative and resources.arsc */
            ignoreChange = ["assets/sample_meta.txt"]

            /** * Default 100KB ** For modifying resource, if it is larger than 'largeModSize' ** we want to use bsdiff algorithm to reduce patch file size */
            largeModSize = 100
        }

        packageConfig {
            /** * Optional, default 'TINKER_ID, TINKER_ID_VALUE', 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE' * package meta-file gen. Path is in the patch file assets/package_meta. TXT * you can use in your own PackageCheck approach securityCheck. GetPackageProperties () * Or TinkerLoadResult. GetPackageConfigByName * we will obtain TINKER_ID from old apk list automatically for you, * other configuration files (below patchMessage) is not required * /
            configField("patchMessage"."tinker is sample to use")
            /** * Just one example, you can use things like sdkVersion, brand, channel... * You can parse it in SamplePatchListener. * Then you can use patch conditions! * /
            configField("platform"."all")
            /** * Patch version via packageConfig */
            configField("patchVersion"."1.0.2")}// Or you can add external configuration files, or get meta values from the old APK
        //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
        //project.tinkerPatch.packageConfig.configField("test2", "sample")

        /** * If you don't use zipArtifact or path, we'll just use 7za to try */
        sevenZip {
            /** * Optional, default '7ZA' * 7zip artifact path, which will use the correct 7ZA with your platform */
            zipArtifact = "Com. Tencent. Mm: SevenZip: 1.1.10"
            /** * Optional, default '7za' * you can specify your own 7za path, which will override the zipArtifact value */
// path = "/usr/local/bin/7za"
        }
    }

    List<String> flavors = new ArrayList<>();
    project.android.productFlavors.each { flavor ->
        flavors.add(flavor.name)
    }
    boolean hasFlavors = flavors.size() > 0
    def date = new Date().format("MMdd-HH-mm-ss")

    /** * bak apk and mapping */
    android.applicationVariants.all { variant ->
        /** * task type, you want to bak */
        def taskName = variant.name

        tasks.all {
            if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {

                it.doLast {
                    copy {
                        def fileNamePrefix = "${project.name}-${variant.baseName}"
                        def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"

                        def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath

                        if (variant.metaClass.hasProperty(variant, 'packageApplicationProvider')) {
                            def packageAndroidArtifact = variant.packageApplicationProvider.get()
                            if(packageAndroidArtifact ! =null) {
                                try {
                                    from new File(packageAndroidArtifact.outputDirectory.getAsFile().get(), variant.outputs.first().apkData.outputFileName)
                                } catch (Exception e) {
                                    from new File(packageAndroidArtifact.outputDirectory, variant.outputs.first().apkData.outputFileName)
                                }
                            } else {
                                from variant.outputs.first().mainOutputFile.outputFile
                            }
                        } else {
                            from variant.outputs.first().outputFile
                        }

                        into destPath
                        rename { String fileName ->
                            fileName.replace("${fileNamePrefix}.apk"."${newFileNamePrefix}.apk")
                        }

                        from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("mapping.txt"."${newFileNamePrefix}-mapping.txt")
                        }

                        from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
                        from "${buildDir}/intermediates/symbol_list/${variant.dirName}/R.txt"
                        from "${buildDir}/intermediates/runtime_symbol_list/${variant.dirName}/R.txt"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("R.txt"."${newFileNamePrefix}-R.txt")
                        }
                    }
                }
            }
        }
    }
    project.afterEvaluate {
        //sample use for build all flavor for one time
        if (hasFlavors) {
            task(tinkerPatchAllFlavorRelease) {
                group = 'tinker'
                def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}ReleaseManifest")
                    preAssembleTask.doFirst {
                        String flavorName = preAssembleTask.name.substring(7.8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release.apk"
                        project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-mapping.txt"
                        project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-R.txt"

                    }

                }
            }

            task(tinkerPatchAllFlavorDebug) {
                group = 'tinker'
                def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Debug")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}DebugManifest")
                    preAssembleTask.doFirst {
                        String flavorName = preAssembleTask.name.substring(7.8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk"
                        project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-mapping.txt"
                        project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-R.txt"
                    }

                }
            }
        }
    }
}
Copy the code

Be sure to build the base package using Gradle commandsGenerate three files in bakApk:

  1. The APK file is the base package
  2. TXT is a confused mapping file after the obfuscation function is enabled
  3. R.tuck is the resource mapping file

  1. Gradle script to generate differential APK files

Modify tinker execution script

    tinkerOldApkPath = "${bakPath}/app-release-0906-17-18-35.apk"
    tinkerApplyMappingPath = "${bakPath}/app-release-0906-18-18-35-mapping.txt"
    tinkerApplyResourcePath = "${bakPath}/app-release-0906-18-18-35-R.txt"
Copy the code

performtinkerPatchRelease/tinkerPatchDebug One is to patch in the DEBUG environment and the other is to patch in the Release environmentThe following patch packages are generated: Generally usedpatch_signed_7zip.apk, upload the file to the server

The principle of the Tinker,

  • Server: Patch package management
  • Client: Performs hot repair
  • Development side: Generate patch package

Hot repair to solve the problem:

  1. What is a patch pack
  2. How do I generate patch packs?
  3. What happens when obfuscation is turned on?
  4. Manually generated patch packages versus automatically generated patch packages (Gradle automatically generated patch packages)
  5. When to perform hot fixes?
  6. How to perform hot fixes using patch packs?
  7. Android version compatibility issues

The mechanism of this

Parent delegation mechanism: when a class loader loads a class, it first delegates the loading task to the parent class loader, recursively. If the parent class loader can complete the class loading task, it returns successfully. Load only if the parent loader is unable to complete the load task or there is no parent loader.

  1. To avoid reloading, there is no need for the child ClassLoader to load the class again when the parent loader has already loaded the class
  2. Security considerations to prevent the API of the core library from being tampered with

The class diagram for a ClassLoader is as follows:

PathClassLoader: The class that loads the App application. DexClassLoader: the code that is not installed in app is generally used to load external classes and external dex files or APK files. (DexClassLoader will decompress the APK file if it finds that it is an APK file. After decompression, it will load all the dex files.)

For example, if the java.lang.string. class system class is loaded in the dex package, the first test is to check whether the parent class is loaded instead of checking whether the DexClassLoader is loaded:

DexClassLoader -> BaseDexClassLoader -> ClassLoader -> ClassLoader -> ClassLoader

If ClassLoader is loaded, return the result to ClassLoader -> BaseDexClassLoader -> DexClassLoader

Such as: If myclass. class is not loaded in memory, it is thrown to DexClassLoader, and then it checks whether BaseDexClassLoader is loaded or whether ClassLoader is loaded. If it is not thrown to BootClassLoader for loading, After loading, it is returned to the DexClassLoader in turn.

Hotfix is based on: pathList:DexPathList different versions of the source code have makeXXElements for reflection, the following dex loading process

Hot repair process

  1. Get the current applied PathClassLoader
  2. Reflection obtains the DexPathList property object pathList
  3. Reflection modifies the dexElements array of pathList
    1. The patch packagepatch.dexintoElement[] (patch)
    2. To obtainpathListthedexElementsProperties (old)
    3. patch + dexElementsMerge and reflect assignments topathListthedexElementsSo that thepatch.dexWill be indexElementsThe first one is going to load firstKey.class, if encountered the oldClass2.dexIn theKey.classNo more loading, and the new class replaces the old one.