Tinker热修复 工具

345 阅读6分钟

现在热修复的技术基本上有阿里的AndFix、QZone的方案、美团提出的思想方案以及腾讯的Tinker等。

其中AndFix可能接入是最简单的一个(和Tinker命令行接入方式差不多),不过兼容性还是是有一定的问题的;QZone方案对性能会有一定的影响,且在Art模式下出现内存错乱的问题(其实这个问题我之前并不清楚,主要是tinker在MDCC上指出的);美团提出的思想方案主要是基于Instant Run的原理,目前尚未开源,不过这个方案我还是蛮喜欢的,主要是兼容性好。

一、接入Tinker

1. Project----build.grandle 
             //集成Tinker,热修复
                  classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"


2.app—---build.grandle
      defaultConfig {        
             multiDexEnabled = true   
            }

    dependencies {

    //可选,用于生成application类,(版本号换成最新的发布版)
    provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}")
    //tinker的核心库
    compile("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
    }

    def getApplyMappingPath() {
    return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
    }

    def getApplyResourceMappingPath() {
    return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
    }


    def buildWithTinker() {
    return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
    }

    def getTinkerBuildFlavorDirectory() {
    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 = 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,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 = '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 issue in 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 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
    /**
    * 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"
    }

    }
    }
    }
    }
    }

注意 修改IgnoreWarning = true 和 tinkerId = '1.0' //这里的 tinkerId :必须和项目版本号保持

以上部分Tinker配置信息就已经集成完了!!!!!

二、Tinker 使用

 创建 SampleApplicationLike 类 继承 DefaultApplicationLike 用于配置生成的MyApplication类
 这个类用解决相同的文件 跨dex文件导致错误异常
生成MyApplication 作用 :独立出来Application,在项目中加载类的时候会有多个dex文件加载,每一个dex文件下都有一个标志性信息,如果你想把他擦除掉就必须这样做


**
@DefaultLifeCycle(application = "jasjax.com.chengpai.MyApplication",  //包名.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);
    // 该方法可以看成MyApplication中的onCreate方法,比如上下文的使用
    context=base;
  }
}

通过这里我们用点击事件来模拟完成修复(官方:Service进行更新的操作)

    findViewById(R.id.erro).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            // 加载补丁包:指定补丁包的位置,读取补丁包信息。
            TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
                    Environment.getExternalStorageDirectory().getAbsolutePath()+"/test");// test:补丁包名称(可以定制)
        }
    });

在Debug模式下,当有问题的项目运行完,会在app --- build 下生成一个backApk包,里面存放着有bug的APK,接下来我们要把这个Apk名称设置给build.gradle中的 “old apk file to build patch apk”,这样我们就可以给指定的Apk 修复bug了


这里还没有完结,接下来我们需要使用命令行去生成 gradle tinkerPatchDebug 生成 X1包(补丁包),编译完成后会生成红色方框标记的的补丁包了,我们需要把他复制出来,名字修改成我们在TinkerInstaller.onReceiveUpgradePatch方法中设置的文件名(test)

这里我使用的是本地模拟,把test文件放置在sd卡目录下,点击修复按钮,程序推出后再次进入就可以看到之前的bug被修改

Tinker原理图:

##

  • alt
    alt