微信热修复Tinker的实践

Tinker热修复简介

Tinker是微信开源出的Android热修复框架,基于MultiDex.

#接入Tinker相关依赖
1.项目的根目录build.gradle:
``bash

dependencies {
classpath ‘com.tencent.tinker:tinker-patch-gradle-plugin:1.7.1’
}

2.项目下的APP目录下的build.gradle:bash
compile(‘com.tencent.tinker:tinker-android-anno:1.7.1’)
//tinker 核心 Android lib
compile(‘com.tencent.tinker:tinker-android-lib:1.7.1’)

3.添加gradle脚本,主要是生成patch包,Tinker是采用插件的形式,你可以参照Tinker-sample-android的做法:bash

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
    //you should bak the following files
    //old apk file to build patch apk
    tinkerOldApkPath = “${bakPath}/Meiya-debug-tt.apk”
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = “${bakPath}”
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = “${bakPath}”
    }

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 getTinkerIdValue() {
return hasProperty(“TINKER_ID”) ? TINKER_ID : gitSha()
}

def buildWithTinker() {
return hasProperty(“TINKER_ENABLE”) ? TINKER_ENABLE : ext.tinkerEnabled
}
def gitSha() {
try {
String gitRev = ‘git rev-parse –short HEAD’.execute().text.trim()
if (gitRev == null) {
throw new GradleException(“can’t get git rev, you should add git to system path or just input test value, such as ‘testTinkerId’”)
}
return gitRev
} catch (Exception e) {
throw new GradleException(“can’t get git rev, you should add git to system path or just input test value, such as ‘testTinkerId’”)
}
}
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()
    /**
     如果出现以下的情况,并且ignoreWarning为false,我们将中断编译。因为这些情况可能会导致编译出来的patch包带来风险:
     1. minSdkVersion小于14,但是dexMode的值为"raw";
     2. 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);
     3. 定义在dex.loader用于加载补丁的类不在main dex中;
     4. 定义在dex.loader用于加载补丁的类出现修改;
     5. resources.arsc改变,但没有使用applyResourceMapping编译
     */
    ignoreWarning = true
    /**
     在运行过程中,我们需要验证基准apk包与补丁包的签名是否一致,我们是否需要为你签名
     */
    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()
        /**
         可选参数;在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配,这样不仅可以减少
         补丁包的大小,同时也避免由于ResId改变导致remote view异常
         */
        applyResourceMapping = getApplyResourceMappingPath()

        /**
         * necessary,default 'null'
         在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个
         是决定补丁包能
         运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等
         */
        tinkerId = getTinkerIdValue()
    }

    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 = ["com.tencent.tinker.loader.*",
                  "com.meiyaapp.meiya.APP",
                  //use sample, let BaseBuildInfo unchangeable with tinker
                  "tinker.com.sen.mytinkerdemo.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")

    }
    //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”
}
}

}

/**

  • task type, you want to bak
    /
    def taskName = “debug”
    /*
  • bak apk and mapping
    */

if (buildWithTinker()) {//在Tinker模式下复制bakApk(by shouwang)
tasks.getByName(“assemble${taskName.capitalize()}”) {
it.doLast {
copy {

            def date = new Date().format("MMdd-HH-mm-ss")
            from "${buildDir}/outputs/apk/${project.getName()}-${taskName}.apk"
            into bakPath
            rename { String fileName ->
                fileName.replace("${project.getName()}-${taskName}.apk", "${project.getName()}-${taskName}-${date}.apk")
            }

            from "${buildDir}/outputs/mapping/${taskName}/mapping.txt"
            into bakPath
            rename { String fileName ->
                fileName.replace("mapping.txt", "${project.getName()}-${taskName}-${date}-mapping.txt")
            }

            from "${buildDir}/intermediates/symbols/${taskName}/R.txt"
            into bakPath
            rename { String fileName ->
                fileName.replace("R.txt", "${project.getName()}-${taskName}-${date}-R.txt")
            }

        }
    }
}

}

``

如何生成补丁,这边有几个步骤

1.修改buildPatch.gradle里面tinkerOldApkPath的值(对应上个apk名称,就是要修复的APK)(运行项目会生成在APP/build/bakApk/ 目录下)
2.要填写TinkerId的值,在gradle.properties里,项目里采用App-VersionName(默认是git版本号)
3.修改buildPatch.gradle里tinkerEnabled的值为true(注:发版本的时候也要用true打包)
4.运行task tinkerPatchDebug.补丁包为patch_signed_7zip.apk,生成目录在App/build/output/tinkerPatch/ 下
5.可以push到手机测试

这里注意一点:Tinker支持对同一基准版本做多次补丁修复,在生成补丁时,oldApk依然是已经发布出去的那个版本。即补丁版本二的oldApk不能是补丁版本一,它应该依然是用户手机上已经安装的基准版本。
bash adb push ./Meiya/build/outputs/tinkerPatch/debug/patch_signed_7zip.apk /storage/sdcard0/

关于打包

我们需要保证tinkerId一定是要唯一性的,这里我们一定要注意,升级可客户端版本,需要更新tinkerId!

补丁加载

关于补丁加载,其实主要是项目Applicaion类的改造,改造的话主要是代理:

``bash
public class APP extends TinkerApplication {
private static APP sInstance;
public APP() {

super(
        //tinker标志, 要支持那种类型 默认为ALL
        //dex only, library only, all support
        ShareConstants.TINKER_ENABLE_ALL,
        // 这边最好用完整包名,参数:AppApplication的托管类,Tinker的核心类,是否验证
        // have a binary dependency on your ApplicationLifeCycle class.
        "com.meiyaapp.meiya.MeiyaApplicationLike","com.tencent.tinker.loader.TinkerLoader",false);
sInstance=this;

}

public static APP getInstance() {
return sInstance;
}

``
注:Tinker前提是项目的Application类不能引入其他类,否者会导致他们无法被补丁修改。
改造的方法采用委托:这里将全部的实现移到了单独的类里面SampleApplicationLike.java(具体参照项目源码)