Preface: when you go online project, there is a small code bug, or when you meet the holiday needs to have holiday activities, increase the user experience. In addition to repackaging for release, hot update, hot fix technology emerged at this time. Although Google is strictly prohibited including Apple. On Android, however, it’s gaining momentum.

There are many articles written on the Internet that are very good, but older. You will find that your project will never produce a patch APK. That’s why I decided to write this article after I had gone through a lot of holes. So this article will not introduce very detailed, directly on the dry goods. Hotfix — Integration and use of Tinker


Tinker framework dependencies

For hot updates, hot fix technology. Sounds very lofty, and look at some of the text is also very high-end, let a person touch. This blog takes you straight to use and runs through your projects. (If you have never been familiar with it, you can learn about the general principles, such as class loading, dex, Elemnt array insertion, etc.)

Add project build.gradle

dependencies {
        ...
        classpath "Com. Tencent. Tinker: tinker - patch - gradle - plugin: 1.9.14"
    }
Copy the code


Tinker needs multidex. Add dependencies step by step:

. api("Com. Tencent. Tinker: tinker - android - lib: 1.9.14") { changing = true }
    // Maven local cannot handle transist dependencies.
    implementation("Com. Tencent. Tinker: tinker - android - loader: 1.9.14") { changing = true }
    annotationProcessor("Com. Tencent. Tinker: tinker - android - anno: 1.9.14") { changing = true }
    compileOnly("Com. Tencent. Tinker: tinker - android - anno: 1.9.14") { changing = true }
    implementation "Com. Android. Support: multidex: 1.0.1." "
Copy the code


Add it to the Android TAB

android {
    ...
    dexOptions {
        // Support large projects
        jumboMode = true}}Copy the code


Add to the defaultConfig TAB

defaultConfig {
        ...
        multiDexEnabled true
    }
Copy the code


After you’ve done this, copy the following big strings to the bottom of your app’s build.gradle. Trust your eyes

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

/** * you can use assembleRelease to build you base apk * use tinkerPatchRelease -POLD_APK= -PAPPLY_MAPPING= -PAPPLY_RESOURCE= to build patch * add apk from the build/bakApk */
ext {
    // Whether to use Tinker(false when your project is in development)
    tinkerEnabled = true
    // Base package file path (old-app.apk) Used to compare old and new apps to generate patches, whether debug or release compilation)
    tinkerOldApkPath = "${bakPath}/old-app.apk"
    // Mapping. TXT file path of base package (used to assist generation of obfuscation patch package, generally used in generation of release APP obfuscation, so this mapping. TXT file is generally only used for generation of release installation package patch)
    tinkerApplyMappingPath = "${bakPath}/old-app-mapping.txt"
    // The r.file path of the base package (if there are any changes to the resource file in your installation package, you need to use this r.file to assist in the generation of the patch package)
    tinkerApplyResourcePath = "${bakPath}/old-app-R.txt"
    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/flavor"
}


def getOldApkPath(a) {
    return hasProperty("OLD_APK")? OLD_APK : ext.tinkerOldApkPath }def getApplyMappingPath(a) {
    return hasProperty("APPLY_MAPPING")? APPLY_MAPPING : ext.tinkerApplyMappingPath }def getApplyResourceMappingPath(a) {
    return hasProperty("APPLY_RESOURCE")? APPLY_RESOURCE : ext.tinkerApplyResourcePath }def getTinkerIdValue(a) {
    return hasProperty("TINKER_ID")? TINKER_ID : android.defaultConfig.versionName }def buildWithTinker(a) {
    return hasProperty("TINKER_ENABLE")? Boolean.parseBoolean(TINKER_ENABLE) : ext.tinkerEnabled }def getTinkerBuildFlavorDirectory(a) {
    return ext.tinkerBuildFlavorDirectory
}

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

    tinkerPatch {
        / * * * necessary, default 'null' * the old apk path, use to diff with the new apk to build * add apk from the build/bakApk */
        oldApk = getOldApkPath()
        / * * * optional, default 'false' * there are some cases we may get some warnings * if ignoreWarning is true, we would just assert the patch process * case 1: minSdkVersion is below 14, but you are using dexMode with raw. * it must be crash when load. * case 2: newly added Android Component in AndroidManifest.xml, * it must be crash when load. * case 3: loader classes in dex.loader{} are not keep in the main dex, * it must be let tinker not work. * case 4: loader classes in dex.loader{} changes, * loader classes is ues to load patch dex. it is useless to change them. * it won't crash, but these changes can't effect. you may ignore it * case 5: resources.arsc has changed, but we don't use applyResourceMapping to build */
        ignoreWarning = false

        / * * * optional, default 'true' * whether sign the patch file * if not, you must do yourself. otherwise it can't check success during the patch loading * we will use the sign config with your build type */
        useSign = true

        /** * optional, default 'true' * whether use tinker to build */
        tinkerEnable = buildWithTinker()

        /** * Warning, applyMapping will affect the normal android build! * /
        buildConfig {
            / * * * optional, default 'null' * if we use tinkerPatch to build the patch apk, you'd better to apply the old * apk mapping file if minifyEnabled is enable! * Warning: * you must be careful that it will affect the normal assemble build! * /
            applyMapping = getApplyMappingPath()
            /** * optional, default 'null' * It is nice to keep the resource ID from R.xt file to reduce Java changes */
            applyResourceMapping = getApplyResourceMappingPath()

            / * * * necessary, default 'null' * because we don't want to check the base apk with md5 in the runtime(it is slow) * tinkerId is use to identify the unique base apk when the patch is tried to apply. * we can use git rev, svn rev or simply versionCode. * we will gen the tinkerId in your manifest automatic */
            tinkerId = getTinkerIdValue()

            /** * if keepDexApply is true, class in which dex refer to the old apk. * open this can 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' * only can be 'raw' or 'jar'. for raw, we would keep its original format * for jar, we would repack dexes with zip format. * if you want to support below 14, you must use jar * or you want to save rom or check quicker, you can use raw mode also */
            dexMode = "jar"

            /** * necessary, default '[]' * what dexes in APK are expected to deal with tinkerPatch * it support * or? pattern. */
            pattern = ["classes*.dex"."assets/secondary-dex-? .jar"]
            / * * * necessary, default '[]' * Warning, it is very very important, loader classes can't change with patch. * thus, they will be removed from patch dexes. * you must put the following class into main dex. * Simply, you should add your own application {@code tinker.sample.android.SampleApplication}
             * own tinkerLoader, and the classes you use in them
             *
             */
// loader = [
// //use sample, let BaseBuildInfo unchangeable with tinker
// "tinker.sample.android.app.BaseBuildInfo"
/ /]
        }

        lib {
            /** * optional, default '[]' * What library in APK are expected to deal with tinkerPatch * it support * or? pattern. * for library in assets, we would just recover them in the patch directory * you can get them in TinkerLoadResult with Tinker */
            pattern = ["lib/*/*.so"]
        }

        res {
            /** * optional, default '[]' * What resource in APK are expected to deal with tinkerPatch * it support * or? pattern. * you must include all your resources in apk here, * otherwise, they won't repack in the new apk resources. */
            pattern = ["res/*"."assets/*"."resources.arsc"."AndroidManifest.xml"]

            / * * * optional, default '[]' * the resource file exclude patterns, ignore add, delete or modify resource change * it support * or ? pattern. * Warning, we can only use for files no relative with resources.arsc */
            ignoreChange = ["assets/sample_meta.txt"]

            /** * default 100kb * for modify resource, if it is larger than 'largeModSize' * we would like 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 assets/package_meta.txt in patch file * you can use securityCheck.getPackageProperties() in your ownPackageCheck method * or TinkerLoadResult.getPackageConfigByName * we will get the TINKER_ID from the old apk manifest for you automatic, * other config files (such as patchMessage below)is not necessary */
            configField("patchMessage"."tinker is sample to use")
            /** * just a sample case, you can use such as sdkVersion, brand, channel... * you can parse it in the SamplePatchListener. * Then you can use patch conditional! * /
            configField("platform"."all")
            /** * patch version via packageConfig */
            configField("patchVersion"."1.0")}//or you can add config filed outside, or get meta value from 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 just use 7za to try */
        sevenZip {
            /** * optional, default '7za' * The 7zip Artifact path, it will use the right 7za with your platform */
            zipArtifact = "Com. Tencent. Mm: SevenZip: 1.1.10"
            /** * optional, default '7za' * you can specify the 7za path yourself, it will overwrite 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(a), variant.outputs.first(a).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"
                    }

                }
            }
        }
    }
}



task sortPublicTxt(a) {
    doLast {
        File originalFile = project.file("public.txt")
        File sortedFile = project.file("public_sort.txt")
        List<String> sortedLines = new ArrayList<>()
        originalFile.eachLine {
            sortedLines.add(it)
        }
        Collections.sort(sortedLines)
        sortedFile.delete()
        sortedLines.each {
            sortedFile.append("${it}\n")}}}Copy the code


Initialize Tinker

First, open my project and copy the Tinker package into your project. The Tinker pack is as follows:

Register Tinker with Application

This TinkerApplicationLike is only used inside the Tinker framework, His annotation way will help us to generate @ DefaultLifeCycle (application = “com. Lihang. Tinkerstu. MyApplication” application. Here is the package name dot the class name. After generation, be sure to add it to the name of the manifest file.

@SuppressWarnings("unused")
@DefaultLifeCycle(application = "com.lihang.tinkerstu.MyApplication".// Application class name. You can only use strings. The MyApplication file doesn't exist, but you can use (name) on the application tag in androidmanifest.xml.
        flags = ShareConstants.TINKER_ENABLE_ALL,// tinkerFlags
        loaderClass = "com.tencent.tinker.loader.TinkerLoader".//loaderClassName = loaderClassName (Do not write)
        loadVerifyFlag = false)//tinkerLoadVerifyFlag
public class TinkerApplicationLike extends DefaultApplicationLike {
    private static final String TAG = "Tinker.SampleApplicationLike";

    public TinkerApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
                                 long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    }

    /**
     * install multiDex before install tinker
     * so we don't need to put the tinker lib classes in the main dex
     *
     * @param base
     */
    @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();
        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());
        // We can move the operation that was done by the custom onCreate() method in the Application to here...
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) { getApplication().registerActivityLifecycleCallbacks(callback); }}Copy the code



Inside the manifest file application, let’s say our application. MyApplication is generated by annotations, because it is generated by annotations, you will get red if you do not build, remember build

  <application
        ...
        android:name="com.lihang.tinkerstu.MyApplication"
        >
        ...
    </application>
Copy the code


Register the service in the manifest file

In fact, this service can not, it is just a callback, tell you success and failure. I did not register the page in the manifest file

 <service
            android:name=".tinker.service.SampleResultService"
            android:permission="android.permission.BIND_JOB_SERVICE"
            android:exported="false"/>
Copy the code


Three, the test, exciting time.

Let’s simulate an online APK. Let’s pack it up. A “I am Jiro” pops up (this requires storage and read permissions, because it is essentially packaged into a patch APK, stored in your phone’s memory, and Tinker reads it. The patch APK knows the class to be modified, and then inserts it into the EmLent array. Bug classes are still in APK.

First, the code in the Activity:

public class MainActivity extends AppCompatActivity {
    private RxPermissions rxPermissions;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        // patch_signed_7zip.apk
        String path = "/sdcard/Tinker/";
        File dir = new File(path);
        if(! dir.exists()) { dir.mkdirs(); } File file =new File(path, "patch_signed_7zip.apk");
        if (file.exists()) {
            if (file.length() > 0) {
                TinkerInstaller.onReceiveUpgradePatch(MainActivity.this, file.getAbsolutePath());
            }
        }
        
        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this."I'm Jiro.", Toast.LENGTH_SHORT).show(); }}); }}Copy the code


3.1. Simulate the BUggy APK on the generation line.

Press as, and click assembleDebug

After waiting for some time, click on your project and go to the app\ Build \bakApk file as follows; Change the name of app-debug-0911-18-32-21.apk to old-app.apk. The old – app. Apk. That’s our original app. Install it on your phone and click the button to pop up “I am Kojiro.”

3.2. Revise our toast content by changing “I am Kojiro” to “I am Hot repair technology!!” . Then click tinkerPatchDebug as shown

Wait for some time to come to our app\build\outputs\ APk \tinkerPatch\debug. There are three APKs:

  • Patch_signed. apk Signature patch package
  • Patch_signed_7zip. apk Patch package signed and compressed in 7Z
  • Patch_unsigned. Apk Unsigned patch package

Here we use patch_signed_7zp.apk

4. Simulate the placement of patch packs

How does Tinker install and uninstall patches

Requesting patch installation

TinkerInstaller. OnReceiveUpgradePatch (context, patches of local path);Copy the code

Uninstall the patch

Tinker.with(getApplicationContext()).cleanPatch();// Uninstall all patchesTinker.with(getApplicationContext()).cleanPatchByVersion(version number)// Uninstall the patch of the specified version
Copy the code


Back to the subject! Remember our code in main:

                    // This is the folder I created for the patch pack
                    String path = "/sdcard/Tinker/";
                    File dir = new File(path);
                    if(! dir.exists()) { dir.mkdirs(); }//patch_signed_7zip.apk is the patch package we want to make
                    File file = new File(path, "patch_signed_7zip.apk");
                    if (file.exists()) {
                        if (file.length() > 0) {
                            Log.e("I just want to see the path.", file.getAbsolutePath());
                            TinkerInstaller.onReceiveUpgradePatch(MainActivity.this, file.getAbsolutePath()); }}Copy the code

Then copy our patch_signed_7zzip. apk to /sdcard/Tinker/ via mobile assistant or other means, for example:

One step. Congratulations. You’re done. ! Because Tinker is not real-time. So you need to exit and close the APK and re-enter. Click the button again and it will pop up “I’m Hotfix Tech!!”

5. Bugly makes thermal repair so easy

Let me briefly explain my understanding here. This patch pack can be downloaded from your backend. But do do some versioning. It’s impossible to get a 1.0 patch for version 2.0. However, Tinker officially has a Bugly background. After inheriting it, it is equivalent to this step. Wechat officially does it for you. !!!!!!!!!

Wechat hotfix Tinker demo. Less than 30 minutes to get you on your way. Making the address