This paper mainly describes the problems encountered by the author in accessing Tinker and personal solutions for reference only

  • Introduction of the gun
  • How do you learn to use this gun
  • How to make a bullet
  • How to mix with other equipment
  • How to manage bullets effectively
  • How do YOU make a bullet load itself

Introduction of the gun

Tinker who

The author cites tinker’s official documentation to explain

Tinker is wechat’s official Hot patch solution for Android. It supports dynamic delivery of code, So library and resources, So that apps can be updated without the need for reinstallation

Tinker’s power can be seen in this brief explanation. The underlying implementation principle of Tinker is very complex. Due to my limited ability, I only read some superficial content and then tasted it. For more on Tinker, check out the following articles:

  1. Tinker Dexdiff algorithm parsing
  2. Everything about wechat Tinker is here, including source code
  3. Tinker: Initial intention and persistence of technology

Excerpted from official Tinker team

So called contrast

No comparison, no harm, I will still release this map for you to enjoy

Of course Ali’s Sophix does some things better (non-invasive, just-in-time), and it might be a faster solution if the team wanted to quickly implement a thermal fix; In addition, Tinker-patch-SDK one-stop solution launched by Tencent team Bugly based on Tinker is widely adopted in the industry, perhaps because it is free. Both of the above solutions automatically manage the cruD of patches, auto-loading, compositing, etc. Why bother building wheels?

Why Tinker

Hot repair has been developing rapidly in recent years, and this technology is becoming more and more mature, and corresponding solutions are set one after another. It is very difficult to realize it in a real sense and open source. Thanks to Mr. Daniu of DACHang for opening source their solutions. I first started tinker this year, pretending to do some research.

  1. Github Quality Project Evaluation Criteria (PS: Personal opinion)

    • Star, fork
    • Continuous maintenance and update
    • Issues continued to be resolved
    • Big factory or technology big cow produce that is even better
  2. How did Tinker break through and win the title?

    • The official explanation
    • Personal view
      • The code is open source and a free trial
      • Good combination with AndresGuard, reinforcement, etc
      • Can solve most problems

How do you learn to use this gun

preparation

  1. You need a general understanding of the process from patch generation to repair
  2. About implementation Principles
    • You can simply understand the loading process of Android ClassLoader and refer to the author’s preliminary knowledge of hot repair
    • Tinker is based on Android native ClassLoader, developed its own ClassLoader, and then loaded the bytecode of patch file
    • Based on android native AAPT (Android Asset Packaging Tool), we developed our own AAPT to load patch file resources
    • Based on the dex file format, the wechat team developed its own DexDiff algorithm to compare the differences between the two APKs and generate patch files
  3. Documents are one of the best teachers

    The official documentationDo read carefully do read carefully do read carefully!!

Access to the process

The official document has made a very detailed description of the use of instructions, the author does not want to repeat, mainly describes some problems encountered in the access process, do a simple log record

Basic Implementation

  1. Gradle access

    Why is it so easy to use build keywords like build.gradle

    android{
        signingConfigs{}
        defaultConfig{}
        dexOptions{}
        lintOptions{}
        compileOptions{}
        flavorDimensions 
        ...
    }
    Copy the code

    Where did they come from in the above script? In fact, all of which are from the official custom Android plugin source of com. Android. View the build: gradle: X.X.X happened to tinker, through the custom gradle – plugin (plug-in) to build the patches, We can add build parameters in build.gradle. For example,

    tinkerPatch{
         oldApk = getOldApkPath()
         tinkerId = versionName
         ignoreChange = ["assets/sample_meta.txt"]}Copy the code
    parameter meaning
    tinkerId For example, the tinkerID of the base package must be 2.5.6. The tinkerID of the patch package must also be 2.5.6. Otherwise, an exception will be thrown when the patch package is assembled
    ignoreChaned Specify unaffected resource paths, ignore resource changes, and ignore additions, deletions, and modifications to the file at compile time

    In addition, I strongly recommend separating tinker gradle configuration into a separate script, app build.gradle

    apply from: './tinker.gradle'
    Copy the code
  2. Code transformation

    As stated in the official documentation, you need to migrate the Application class and inheritance logic to your own ApplicationLike inheritance class. In the actual development scenario, the project may be very large and complex, and its impact is difficult to assess. In view of this, the author considers not migrating the code and substituting the code as follows:

    1. throughtinker-AnnotationPlug-in generatedGenerateTinkerApplication
    2. Project base classBaseApplication extends GenerateTinkerApplication
    3. MyApplication extends BaseApplication

    If you do this, you’ll find that the code is pretty much unchanged. Then, after generating a patch pack, loading the patch pack, and restarting a bunch of operations, the program crashes?

    The author’s mobile phone is Android7.1.1. The error log found that the baseApplication.get () startup page got a null instance of the global contextproblem

    The author has drilled a lot of corners to solve this problem, and finally put forward this problem on issues. The author tinker replied and gave the reason

    Among themThe wiki article

    Finally, the author still carried out application transformation and migration according to the official documents.

Custom extension

  • Patch Synthesis Process

    1. Check the validity of the patch file
    2. Wake up the patch synthesis process
    3. Patch composition ING
    4. Patch synthesis result callback
    5. Follow-up operations of patch synthesis
    6. Deleting patch Files
    7. Restart to load the patch and display the effect
  • You can customize some listener classes to achieve the following function points

    • Patch synthesis and loading logs are reported
    • Customize some actions
      • After the patch composition is complete, subsequent actions at LLDB restart
      • Synthesize success Caches the current synthesize success record
      • .

The author customized DefaultPatchReporter to do the following two things

@Override
    public void onPatchResult(File patchFile, boolean success, long cost) {
        super.onPatchResult(patchFile, success, cost);
        if (success) {
            DLog.w("Synthesis time:" + cost);
            DLog.w("@@@@ L42"."CustomerPatchReporter:onPatchResult() -> " + "Patch composition successful:" + patchFile.getAbsolutePath());
            String fileMd5 = FileMd5Util.getFileMd5(patchFile);
            if(! TextUtils.isEmpty(fileMd5)) { PatchLogReporter.updatePatchCompositeCnt(fileMd5);// Save the current patch md5 file to the local spSharedPreferences sp = context.getSharedPreferences(TinkerManager.SP_FILE_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit().putString(TinkerManager.SP_KEY_PATCH, fileMd5); SharedPreferencesCompat.EditorCompat.getInstance().apply(editor); }}else {
            // Failed to compose
            DLog.w("@@@@ L42"."CustomerPatchReporter:onPatchResult() -> " + "Patch composition failed"); }}Copy the code

Record the MD5 value of the synthesized patch file to the cache file to prevent repeated synthesis

@Override
    public void onPatchPackageCheckFail(File patchFile, int errorCode) {
        // If the patch file is not deleted, this method is called every time tinker is called to load the patch
        String errorInfo = TranslateErrorCode.onLoadPackageCheckFail(errorCode);
        DLog.w("@@@@ L54"."CustomerPatchReporter:onPatchPackageCheckFail() -> " + TranslateErrorCode.onLoadPackageCheckFail(errorCode));
        // Patch synthesis failure Report the cause of patch synthesis failure
        String fileMd5 = FileMd5Util.getFileMd5(patchFile);
        if(! TextUtils.isEmpty(fileMd5)) {// Error logs are uploaded to the server
            PatchLogReporter.reportPatchCompositeErrorInfo(fileMd5, errorCode, errorInfo);
        }
        super.onPatchPackageCheckFail(patchFile, errorCode);
        if(errorCode == ERROR_PACKAGE_CHECK_TINKER_ID_NOT_EQUAL) { Tinker.with(context).cleanPatchByVersion(patchFile); }}Copy the code

If an exception occurs during composition, upload the error log to the server. Note that some callback methods may run in different processes. For more customizations, see The Tinker Custom Extension

How to make a bullet

Generate a base pack

This process is relatively simple, just basic packaging command, two additional points need to be noted

  • Do YOU need to back up the base pack in any case?
  • How to manage multi-channel packages?
  1. through-Pparams=trueDynamic parameter transfer for judgment
android.applicationVariants.all { variant ->
    def buildTypeName = variant.buildType.name
    tasks.all {
         it.doLast {
             def isNeedBackup = project.hasProperty("isNeedBackup")?project.getProperties().get("isNeedBackup") : "false"
             // Determine whether remarks are required based on this variable
             / /... Base file copy logic}}}Copy the code
  1. Because the author did not usegradle productFlavorsMulti – channel packaging, mainly for two reasons
  • Compilation speed
  • Different channel benchmark APKS cannot use the same patch pack: Reason

The author uses walle, a solution provided by Meituan

Braided command

  1. Not using walle

    ./gradlew assembleXXX -PisNeedBackup=true --stacktrace
    Copy the code
  2. The walle

     ./gradlew assembleReleaseChannels -PisNeedBackup=true --stacktrace
    Copy the code

The above is for reference only, different cases do different treatment

Generate the patch

  1. The path specified

    def bakPath = file("${rootDir}/tinkerBackup/${versionNamePrefix}")
    ext {
        // Baseline APK path
        tinkerOldApkPath = "${bakPath} / app - debug - 2.6.6. Apk." "
        // Benchmark apk mapping file
        tinkerApplyMappingPath = "${bakPath}/app-debug-2017-12-13-mapping.txt"
        // Base apk R file -> excute assembleRelease will generate R file in bak directory
        tinkerApplyResourcePath = "${bakPath}/app-debug-2017-12-13-R.txt"
    }
    Copy the code
  2. Task execution

    ./gradlew tinkerPatchXXX -PisNeedBackup=false --stacktrace
    Copy the code

How to mix with other equipment

Compatible with andreGuard resource compression tool

Of course, you can generate old APK and new APK in advance, and then configure oldApk and newApk respectively in tinkerPatch, and execute tinkerPatchXXX to generate patch files without any problem. But can we do this with gradle scripts? Still can! Think about it

How are patch files generated?

Nothing more than oldApk, newApk generated by comparison, so the core is inseparable from these two files, so the following functions can be implemented

  1. Be able tooldApkBackup? Ps: AndresGuard can be specifiedfinalApkBackupPathOutput APK path)
  2. oldApkapplyMapping,applyResourceMappingIs the path specified correctly?
  3. Ensures that newApk is executedandresguardXXXWas it after the mission?

From the first point, backing up oldApk is nothing more than a copy logic (no code here), which programmers do best. The author maintains a prefix-. TXT file to specify the prefix of the backup file and specify oldApk, applyMapping, and applyResourceMapping paths when generating patch packages.

File intoFileDir = file(bakPath.absolutePath) // bakPath Backup path
if (intoFileDir.exists()) {
    // If there is a backup delete
    println("============================= will delete history baseapk...")
    delete(intoFileDir)
}
println("=================================: start copy file to destination")
def newPrefix = "${project.name}-${variant.baseName}-${versionName}-${date}"
// Write the newPrefix file contents to a temporary file for later generation of patch packs
File prefixVersionFile = new File("${bakPath.absolutePath}/prefix.txt")
if(! prefixVersionFile.parentFile.exists()) { prefixVersionFile.parentFile.mkdirs() }if(! prefixVersionFile.exists()) { prefixVersionFile.createNewFile() } prefixVersionFile.write(newPrefix)
Copy the code

On the second point:

// The fixed version prefix appears as v1.2.2
def versionNamePrefix = "v${getVersionName()}"
// Define the backup file location
def bakPath = file("${rootDir}/tinkerBackup/${versionNamePrefix}")
ext {
    // Whether to enable tinker
    tinkerEnable = enableTinker.toBoolean()

    def prefix = readPrefixContent(bakPath.absolutePath)
    println('------------prefix = ' + prefix)
    // Baseline APK path
    tinkerOldApkPath = "${bakPath}/${prefix}.apk"
    // Benchmark apk mapping file
    tinkerApplyMappingPath = "${bakPath}/${prefix}-mapping.txt"
    // Base apk R file -> excute assembleRelease will generate R file in bak directory
    tinkerApplyResourcePath = "${bakPath}/${prefix}-R.txt"
    // Specify the multi-channel package path to generate the corresponding channel patch file
    tinkerBuildFlavorDirectory = "${bakPath}/"
}
Copy the code

The readPrefixContent method reads the backup logic and writes the file prefix to the content of the prefix-.txt file. In this way, if you want to change the name of the backup file, you only need to modify prefix-txt

As for the third point, you need to know some apis of Gradle for Android, such as doFirst doLast… What does it mean to make resguardXXX task perform first with tinkerPatchXXX?

if ("tinkerPatch${buildTypeName}".equalsIgnoreCase(it.name)) {
    // Temporarily store the current IT task (tinkerPatchRelease) in tempPointer
    def tempPointer = it
    def resguardTask
    tasks.all {
        if (it.name.equalsIgnoreCase("resguard${taskName.capitalize()}")) {
            resguardTask = it
            tempPointer.doFirst({
                // Specify the tinkerPatch newApk path to generate the patch package
                it.buildApkPath = "${buildDir}/outputs/apk/${andResOutputPrefix}/${ouputApkNamePrefix}_${andResSuffix}.apk"
                    // ..
        })
        tempPointer.dependsOn tinkerPatchPrepare
        tempPointer.dependsOn resguardTask
        }
    }
}

Copy the code

From the code, you can see: find tinkerPatchXXX tasks and resugardXXX task, through setting execution order, dependsOn careful friends will find I also used a dependsOn tinkerPatchPrepare rely on for another task. What does it do? Go on to….

My initial thought was if I could not

ext {
    appName = "dlandroidzdd"
    // Whether to enable tinker
    tinkerEnable = enableTinker.toBoolean()
    // Baseline APK path
    tinkerOldApkPath = ""
    // Baseline APK ProGuard Mapping file
    tinkerApplyMappingPath = ""
    // Base apk R file -> excute assembleRelease will generate R file in bak directory
    tinkerApplyResourcePath = ""

    // Specify the multi-channel package path to generate the corresponding channel patch file
    tinkerBuildFlavorDirectory = ""
}

tinkerPatch{
    oldApk = getOldApkPath()
    ....
    
    buildConfig {
        applyMapping = getApplyMappingPath()
    }
}
Copy the code

What about these code blocks that do assignments? DependsOn: Can you pass project.tinkerpatch.xx = before executing tinkerPatchXXX to create a patch? How about specifying assignment in this form? Without saying anything else, the author rolled up his sleeves and started to do it

task tinkerPatchPrepare << {
    File intoFileDir = file(bakPath.absolutePath)
    if (intoFileDir.exists()) {
        def prefix = null
        File prefiFile = new File("${bakPath.absolutePath}/prefix.txt")
        if (new File("${bakPath.absolutePath}/prefix.txt").exists()) {
            prefix = prefiFile.getText()}if (null! = prefix) {// If there is a backup assignment to the global variable
            project.tinkerPatch.oldApk = "${bakPath}/${prefix}.apk"
            project.tinkerPatch.buildConfig.applyMapping = "${bakPath}/${prefix}-mapping.txt"
            project.tinkerPatch.buildConfig.applyResourceMapping = "${bakPath}/${prefix}-resource_mapping.txt"}}}Copy the code

Seems to work, right? The first time, modify the base Java code, deliberately not in ext variable specified base file, execute the patch generation task, fix the bug perfect ~ the second time, change the layout, still deliberately not in ext variable specified base file, execute the patch generation task, actually throw the following error

The following two codes are not in effect?

. project.tinkerPatch.buildConfig.applyMapping = "${bakPath}/${prefix}-mapping.txt" project.tinkerParch.buildConfig.applyResourceMapping=${bakPath}/${prefix}-resource_mapping.txt ...Copy the code

In order to verify that this suspicion is correct, the author shielded the above code, manually assigned global variables such as mapping path in Ext, and executed the task of generating patch package named tinkerPatchXXX. As expected, the patch package was typed normally, which proves that these two lines of code did not play an actual role. Why does this happen? Again, I wonder:

Is it possible that the corresponding base file path was read out and cached in memory variables before the above two pieces of code were called? performtinkerPatchTask uses memory variables

The author confirmed the above suspicion by following two steps

  1. To viewtinker-patch-gradle-pluginProgram source code

  • If applyResourceMapping is valid, Gradle log prints out the path
  • The assignment operation is performed inafterEvaluateAction
  1. hands-on

If the tinkerPatchPrepare task is still required, run the patch generation command

tinkerPatchPrepare

AfterEvaluate = afterEvaluate = afterEvaluate = afterEvaluate = afterEvaluate = afterEvaluate = afterEvaluate = afterEvaluate = afterEvaluate = afterEvaluate = afterEvaluate

Adds an action to execute immediately after this project is evaluated.

Generally means: parsing is complete (configuration, syntax, etc.) all the configuration and task dependencies have been generated, the task execution, which means it executed prior to the task, tinker has read and stored in the memory other variables, subsequent change is invalid, anyway because he won’t read again. Get the timing before setting the timing

Compatible with Walle multi-channel packaging solutions

To be clear: With productFlavors, you can change the source code BuildConfig and make the classes.dex difference. Therefore, I use Walle multi-channel package solution. Oldapk, NewAPk, Mapping, and other files are compatible with AndresGuard, so it’s easy to support Walle. I will not repeat it here

AndResGuard is compatible with Walle2018.04.08

project.afterEvaluate {
    // Do not import com.android.builder.task
	Task walleTask = project.tasks.findByPath('assembleReleaseChannels')
	Task resguardReleaseTask = project.tasks.findByPath('resguardRelease')
	if (null! = walleTask) {project.logger.error('----------------walleTask ! = null----------------')}if (null! = resguardReleaseTask) {project.logger.error('----------------resguardReleaseTask ! = null----------------')}if (null! = walleTask &&null! = resguardReleaseTask) { resguardReleaseTask.doLast{
			walleTask.packaging()
		}
	}
}
Copy the code

Mutual compatibility?

How to implement AndresGuard first via dependsOn on how to implement AndresGuard and Walle simultaneously? It seems that only the source code can be modified. I tried to make an issue in Walle and found that Drakeet had already made this issue

Then wait for it to be fixed

conclusion

When I was dealing with this, I didn’t know much about Gradle for Android. Most of my work was based on my own guess and argument. I haven’t found a good textbook on the Internet, so I still recommend the official document. I will study this in depth when I have time later, and then I will share another one

How to manage bullets effectively

Nodejs + mysql build back-end API interface

Implement the following functions

  • Basic account system, APP CRUD, Appversion CRUd
  • Patch CRUD operations, providing external (Web and APP) interfaces
  • Error log upload

The author uses the following library to complete the basic function development

  • express
  • body-parser
  • cookie-parser
  • mysql
  • uuid
  • multer
  • .

Database tables: Users, applications, reference versions, patches, error logs Each route basically needs to implement basic CRUD functionality, which is nothing more than the use of some basic SQL and related library apis

How to obtain the latest patch file of the specified version?

Patch_code Records the current patch index and the incremental value of the int type. The patch_code file with the largest value is the latest patch file

Update the number of downloaded patch files, number of successfully synthesized patch files, and related logs

Considering the APP code, each patch table maintains a patch_MD5, through which the above data are maintained and updated

Other problems

When the database connection has been inactive for a certain period of time, it will automatically close the connection. For this problem, most of the online methods are mysql.createpool (config) to create a connection pool. Here the author also uses this kind of practice

Patch management web platform

Bootstrap front-end framework, the interface is just so-so

How do YOU make a bullet load itself

Patch Loading Process

  • Put the implementation of this inServiceTo achieve

  • Extract the hot fix management implementation code into a separate functional component

Finally, part of the source code posted for your reference