At present, the hot repair technology basically includes AndFix of Ali, QZone scheme, thought scheme of Meituan and Tinker of Tencent.
Of these, AndFix is probably the simplest (similar to the Tinker command line), but compatibility is problematic; QZone has some impact on performance and memory disorder in Art mode (actually I was not aware of this problem before, mainly pointed out by Tinker in MDCC); The idea proposed by Meituan is mainly based on the principle of Instant Run, which is not open source yet. However, I like this plan quite well, mainly because it has good compatibility.
1. Access Tinker
1. Project----build.grandle // Integrate Tinker, hotfix classpath"com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"App --build.grandle defaultConfig {multiDexEnabled =true} dependencies {// Optional, provided(version number changed to the latest release)"com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}"// Compile (tinker's core library)"com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}")
compile 'com. Android. Support: multidex: 1.0.1'
}
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 {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = true
//for normal build
//old apk file to build patch apk
tinkerOldApkPath = "${bakPath}/app-debug-1207-18-46-41.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-debug-1018-17-32-47-R.txt"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}
def getOldApkPath() {
return hasProperty("OLD_APK")? OLD_APK : ext.tinkerOldApkPath } defgetApplyMappingPath() {
return hasProperty("APPLY_MAPPING")? APPLY_MAPPING : ext.tinkerApplyMappingPath } defgetApplyResourceMappingPath() {
return hasProperty("APPLY_RESOURCE")? APPLY_RESOURCE : ext.tinkerApplyResourcePath } defbuildWithTinker() {
return hasProperty("TINKER_ENABLE")? TINKER_ENABLE : ext.tinkerEnabled } defgetTinkerBuildFlavorDirectory() {
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'don't use applyResourceMapping to build */ ignoreWarning = true /** * 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
/**
* 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.txt file to reduce java changes */ applyResourceMapping = GetApplyResourceMappingPath () / * * * necessary, the default 'null' * because we don't want to check the base apk with md5 inthe 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 tinkerIdin your manifest automatic
*/
tinkerId = '1.0'
}
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"/** * optional, default'false'
* if usePreGeneratedPatchDex is true. tinker framework will generate auxiliary class * and insert auxiliary instruction when compiling base package using * assemble{Debug/Release} task to prevent class pre-verified issuein dvm.
* Besides, a real dex file contains necessary class will be generated and packed into
* patch package instead of any patch info files.
*
* Use this mode if you have to use any dex encryption solutions.
*
* Notice: If you change this value, please trigger clean task
* and regenerate base package.
*/
// usePreGeneratedPatchDex = false/** * 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 = ["com.tencent.tinker.loader.*", //warning, you must change it with your application "tinker.sample.android.app.SampleApplication", //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/armeabi/*.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 7 za 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
/**
* bak apk and mapping
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
def taskName = variant.name
def date = new Date().format("MMdd-HH-mm-ss")
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
from variant.outputs.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"
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
Note Change IgnoreWarning = true and tinkerId = ‘1.0’ // tinkerId: must be consistent with the project version number
The above Tinker configuration information has been integrated !!!!!
Two, Tinker use
Create SampleApplicationLike class DefaultApplicationLike to configure the generated MyApplication class. This class is used to resolve the same file across the dex file error generated MyApplication. Application alone, there will be multiple dex files loaded when classes are loaded in the project, and each dex file will have a signature message that you will have to do if you want to erase it ** @defaultLifecycle (Application =)"jasjax.com.chengpai.MyApplication", // Package name MyApplication Flags = ShareConstants.TINKER_ENABLE_ALL, loadVerifyFlag =false) public class SampleApplicationLike extends DefaultApplicationLike { public static Context context; public SampleApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) { super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent); } @Override public void onBaseContextAttached(Context base) { super.onBaseContextAttached(base); MultiDex.install(base); TinkerInstaller.install(this); // This method can be viewed as an onCreate method in MyApplication, such as using context=base; }}Copy the code
Here we simulate the completion of the repair with a click event.
findViewById(R.id.erro).setOnClickListener(new View.OnClickListener() {@override public void onClick(View View) {// Load the patch package. TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath()+"/test"); //test: Patch pack name (customizable)}});Copy the code
In Debug mode, a backApk package containing the bug APK is generated under the app build when the project in question is finished running. Next we need to set the Apk name to "old Apk file to Build patch Apk" in build.gradle so that we can fix the bug for the specified Apk
Gradle tinkerPatchDebug generates the X1 package (patch package). After compiling, it will generate the patch package marked by the red box. We need to copy it. Name modified into us in TinkerInstaller onReceiveUpgradePatch methods set in the file name (test)
Here I used local simulation, put the test file in the SD card directory, click the “fix” button, after the program is launched, enter again, you can see the previous bug has been modified
Tinker schematic diagram:
# #