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:
- Tinker Dexdiff algorithm parsing
- Everything about wechat Tinker is here, including source code
- 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.
-
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
-
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
- You need a general understanding of the process from patch generation to repair
- 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
- 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
-
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
-
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:
- through
tinker-Annotation
Plug-in generatedGenerateTinkerApplication
- Project base class
BaseApplication
extendsGenerateTinkerApplication
MyApplication
extendsBaseApplication
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 reasonAmong themThe wiki article
Finally, the author still carried out application transformation and migration according to the official documents. - through
Custom extension
-
Patch Synthesis Process
- Check the validity of the patch file
- Wake up the patch synthesis process
- Patch composition ING
- Patch synthesis result callback
- Follow-up operations of patch synthesis
- Deleting patch Files
- 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?
- through
-Pparams=true
Dynamic 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
- Because the author did not use
gradle productFlavors
Multi – 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
-
Not using walle
./gradlew assembleXXX -PisNeedBackup=true --stacktrace Copy the code
-
The walle
./gradlew assembleReleaseChannels -PisNeedBackup=true --stacktrace Copy the code
The above is for reference only, different cases do different treatment
Generate the patch
-
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
-
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
- Be able to
oldApk
Backup? Ps: AndresGuard can be specifiedfinalApkBackupPath
Output APK path) oldApk
、applyMapping
,applyResourceMapping
Is the path specified correctly?- Ensures that newApk is executed
andresguardXXX
Was 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? performtinkerPatch
Task uses memory variables
The author confirmed the above suspicion by following two steps
- To view
tinker-patch-gradle-plugin
Program source code
- If applyResourceMapping is valid, Gradle log prints out the path
- The assignment operation is performed in
afterEvaluate
Action
- 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 in
Service
To achieve - Extract the hot fix management implementation code into a separate functional component
Finally, part of the source code posted for your reference