Android 微信热修复 Tinker 接入过程及使用方法

来源:互联网 发布:sql合并两个字段 编辑:程序博客网 时间:2024/05/19 22:49

一、前言

学习热修复 Tinker 的使用,主要有两个原因:

  1. 业务需要:项目会出现一些细小的bug,需要等到发布新版才能修复,因此一直有关注热修复这块的技术。
  2. 技术驱动:这是一件需要一直保持的事情,不断的学习,与时俱进,才能提高自己。

二、Tinker 接入指南

热补丁:让应用能够在无需重新安装的情况实现更新,帮助应用快速建立动态修复能力。

心理准备:Tinker 的接入相对HotFix、AndFix 等要复杂一点,参考了几篇文章,前后分别建了 TinkerDemo、官方DEMO、TinkerFixDemo、TinkerTryDemo。到最后 TinkerTryDemo 才完全弄好。

这里写图片描述

2.1、gradle接入

gradle是推荐的接入方式,在gradle插件tinker-patch-gradle-plugin中我们帮你完成proguard、multiDex以及Manifest处理等工作。

添加gradle依赖:在项目的build.gradle中,添加tinker-patch-gradle-plugin的依赖

buildscript {    dependencies {        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.7')    }}

然后在app的gradle文件app/build.gradle,我们需要添加tinker的库依赖以及apply tinker的gradle插件.

dependencies {    //可选,用于生成application类     provided('com.tencent.tinker:tinker-android-anno:1.7.7')    //tinker的核心库    compile('com.tencent.tinker:tinker-android-lib:1.7.7') }......//apply tinker插件apply plugin: 'com.tencent.tinker.patch'

2.2、Tinker 参数 配置

 /**Tinker 相关配置----------------开始-----------------------------------*/        def bakPath = file("${buildDir}/bakApk/")        def gitSha() {        try {        String gitRev = '11.2.3.4'        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'")            }        }        ext {            //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?            //Tinker是否可用        tinkerEnabled = false        //for normal build        //old apk file to build patch apk 旧包名        tinkerOldApkPath = "${bakPath}/tinker_demo-debug-1121-17-43-24.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 旧包R文件        tinkerApplyResourcePath = "${bakPath}/tinker_demo-debug-1121-17-43-24-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 getTinkerIdValue() {            return hasProperty("TINKER_ID") ? TINKER_ID : gitSha()        }        def buildWithTinker() {            return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled        }        def getTinkerBuildFlavorDirectory() {            return ext.tinkerBuildFlavorDirectory        }        if (buildWithTinker()) {            apply plugin: 'com.tencent.tinker.patch'            tinkerPatch {                /**                 * 基准apk包路径,也就是旧包路径                 * */                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 = false                /**                 * 在运行过程中,我们需要验证基准apk包与补丁包的签名是否一致,我们是否需要为你签名                 * */                useSign = true        buildConfig {            /**             * 可选参数;在编译新的apk时候,我们希望通过保持旧apk的proguard混淆方式,从而减少补丁包的大小。这个只             * 是推荐的,但设置applyMapping会影响任何的assemble编译。             * */            applyMapping = getApplyMappingPath()            /**             * 可选参数;在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配,这样不仅可以减少补丁包的             * 大小,同时也避免由于ResId改变导致remote view异常。             * */            applyResourceMapping = getApplyResourceMappingPath()            /**             * 在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些             * 基准包上面,一般来说我们可以使用git版本号、versionName等等。             * */            tinkerId = getTinkerIdValue()        }        dex {            /**             * 只能是'raw'或者'jar'。             * 对于'raw'模式,我们将会保持输入dex的格式。             * 对于'jar'模式,我们将会把输入dex重新压缩封装到jar。如果你的minSdkVersion小于14,你必须选择‘jar’模式             * ,而且它更省存储空间,但是验证md5时比'raw'模式耗时()             * */            dexMode = "jar"            /**             * 是否提前生成dex,而非合成的方式。这套方案即回退成Qzone的方案,对于需要使用加固或者多flavor打包(建             * 议使用其他方式生成渠道包)的用户可使用。但是这套方案需要插桩,会造成Dalvik下性能损耗以及Art补丁包可             * 能过大的问题,务必谨慎使用。另外一方面,这种方案在Android N之后可能会产生问题,建议过滤N之后的用户。             */            usePreGeneratedPatchDex = false            /**             * 需要处理dex路径,支持*、?通配符,必须使用'/'分割。路径是相对安装包的,例如/assets/...             */            pattern = ["classes*.dex",                       "assets/secondary-dex-?.jar"]            /**             *     这一项非常重要,它定义了哪些类在加载补丁包的时候会用到。这些类是通过Tinker无法修改的类,也是一定要放在main dex的类。             这里需要定义的类有:             1. 你自己定义的Application类;             2. Tinker库中用于加载补丁包的部分类,即com.tencent.tinker.loader.*;             3. 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中;             4. 其他一些你不希望被更改的类,例如Sample中的BaseBuildInfo类。这里需要注意的是,这些类的直接引用类也             需要加入到loader中。或者你需要将这个类变成非preverify。             */            loader = ["com.tencent.tinker.loader.*",                      //warning, you must change it with your application                    //TODO 换成自己的Application                      "com.material.hbl.tinker_demo.MyApplication",            ]        }        lib {            /**             * 需要处理lib路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如/assets/...             */            pattern = ["lib/armeabi/*.so"]        }        res {            /**             * 需要处理res路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如/assets/...,             * 务必注意的是,只有满足pattern的资源才会放到合成后的资源包。             */            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]            /**             * 支持*、?通配符,必须使用'/'分割。若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改。             * 最极端的情况,ignoreChange与上面的pattern一致,即会完全忽略所有资源的修改。             */            ignoreChange = ["assets/sample_meta.txt"]            /**             * 对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。这可以降低补丁包的大小,但是会增加合成             * 时的复杂度。默认大小为100kb             */            largeModSize = 100        }        packageConfig {            /**             * configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动             * 写入configField。在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。但是建议直接通过修改代码来实现,例如BuildConfig。             */            configField("patchMessage", "tinker is sample to use")        }        sevenZip {            /**             * 例如"com.tencent.mm:SevenZip:1.1.10",将自动根据机器属性获得对应的7za运行文件,推荐使用             */            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"        }        /**         *  文件名                              描述         *  patch_unsigned.apk                  没有签名的补丁包         *  patch_signed.apk                  签名后的补丁包         *  patch_signed_7zip.apk              签名后并使用7zip压缩的补丁包,也是我们通常使用的补丁包。但正式发布的时候,最好不要以.apk结尾,防止被运营商挟持。         *  log.txt                              在编译补丁包过程的控制台日志         *  dex_log.txt                          在编译补丁包过程关于dex的日志         *  so_log.txt                          在编译补丁包过程关于lib的日志         *  tinker_result                      最终在补丁包的内容,包括diff的dex、lib以及assets下面的meta文件         *  resources_out.zip                  最终在手机上合成的全量资源apk,你可以在这里查看是否有文件遗漏         *  resources_out_7z.zip              根据7zip最终在手机上合成的全量资源apk         *  tempPatchedDexes                  在Dalvik与Art平台,最终在手机上合成的完整Dex,我们可以在这里查看dex合成的产物。         *         *         * */        /**         * 获得所有渠道集合,并判断数量         */        List<String> flavors = new ArrayList<>();        project.android.productFlavors.each {flavor ->            flavors.add(flavor.name)        }        boolean hasFlavors = flavors.size() > 0        /**         * bak apk and mapping         *  创建Task并执行文件操作         */        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"                        }                    }                }            }        }        }        }        /**Tinker 相关配置----------------结束-----------------------------------*/

2.3、Application 构造

创建一个SampleApplication类,继承DefaultApplicationLike:

@DefaultLifeCycle(application = "com.troy.tinkertrydemo.MyApplication",        flags = ShareConstants.TINKER_ENABLE_ALL,        loadVerifyFlag = false)public class SampleApplication extends DefaultApplicationLike{    public SampleApplication(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);    }    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)    @Override    public void onBaseContextAttached(Context base) {        super.onBaseContextAttached(base);        /************************Action*************************/        TinkerInstaller.install(this);        /************************Action*************************/    }    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)    public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {        getApplication().registerActivityLifecycleCallbacks(callback);    }}

其中DefaultLifeCycle中的Application为我们真正的Application,flags表示需要支持的修复的范围,然后将AndroidManifest.xml文件中的Application的name改为我们的Application。

<application        android:name=".MyApplication"        android:allowBackup="true"        android:icon="@mipmap/ic_launcher"        android:label="@string/app_name"        android:supportsRtl="true"        android:theme="@style/AppTheme">        <activity android:name=".MainActivity">            <intent-filter>                <action android:name="android.intent.action.MAIN" />                <category android:name="android.intent.category.LAUNCHER" />            </intent-filter>        </activity>    </application>

注意:“android:name=”.MyApplication””应该studio 会报红,不过正常编译能通过,可以忽略。

添加读写磁盘的权限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

三、具体操作

3.1、测试代码

在将Tinker成功接入以后,可以写一个简单的例子来检测一下是否可以使用。我在DEMO 中分别测试了,增加代码,增加方法,增加类,增加布局文件,增加图片,均能修复成功。

测试代码见DEMO。

3.2、打BaseApk(有Bug的 Apk)

将Gradle中ext中的 tinkerEnabled 置为true,然后点击studio顶部的Build中Build APk,打一个DeBug包,成功之后会在Module的build目录下生成一个文件夹,如图:

这里写图片描述

因为是Debug 包所以只会有两个文件,一个是APK,一个是对应的R 文件。如果有使用混淆的话,还会有一个mapping的混淆映射文件。
我们把 outputs/apk/ 中的apk文件拷出去,并安装到自己的手机上,运行。

这时相等于 有bug 的apk 已经在用户设备上运行了。

同时,将 Gradle 中ext 中的 tinkerOldApkPath 后的路径设置为上一步中bakApk 中的 apk 名称,同理把 tinkerApplyResourcePath 设置为 R 文件的名称。其他的不用管。

3.3、生成 patch

修改代码重新编译之后,点击Studio 右侧的Gradle 工具栏,展开之后找到自己的Module 找到tinker, 双击其中的 tinkerPathDeBug , 图示:

这里写图片描述

运行之后会在:

这里写图片描述

outputs下多出一个目录,都是一些编译打差异包过程的log和相关生成的apk,选择其中的patch_signed_7zip.apk,拷出来,放在手机根目录下。

重新启动app,点击load,tinker 会加载补丁,打印日志信息如下:

PatchFileUtil: safeDeleteFile, try to delete path: /data/data/com.troy.tinkertrydemo/tinker/patch-9de9cc81/res/resources.arscpatch I/Tinker.ResDiffPatchInternal: final new resource file:/data/data/com.troy.tinkertrydemo/tinker/patch-9de9cc81/res/resources.apk, entry count:342, size:474067674 25862-patch I/Tinker.ResDiffPatchInternal: recover resource result:true, cost:124674 25862-patch I/Tinker.DexDiffPatchInternal: dex count: 2, final wait time: 12684 25862-patch I/Tinker.DexDiffPatchInternal: check dex optimizer file classes.dex.dex, size 5882288684 25862-patch I/Tinker.DexDiffPatchInternal: check dex optimizer file test.dex.dex, size 12720684 25862-patch I/PatchInfo: rewritePatchInfoFile file path:/data/data/com.troy.tinkertrydemo/tinker/patch.info , oldVer:ac6e19ae8ee0e84b8e02c5683dba8424, newVer:9de9cc8103630d0dc0ed839fb0b70157684 25862-patch W/Tinker.UpgradePatch: UpgradePatch tryPatch: done, it is ok684 25862-patch I/Tinker.DefaultPatchReporter: patchReporter onPatchResult: patch all result path:/storage/emulated/0/patch_signed_7zip.apk, success:true, cost:3545 I/Tinker.DefaultTinkerResultService: DefaultTinkerResultService received a result:                                                                                           PatchResult:                                                                                            isSuccess:true                                                                                           rawPatchFilePath:/storage/emulated/0/patch_signed_7zip.apk                                                                                           costTime:3545                                                                                           patchVersion:9de9cc8103630d0dc0ed839fb0b70157 I/Process: Sending signal. PID: 25862 SIG: 9 W/Tinker.DefaultTinkerResultService: deleteRawPatchFile rawFile path: /storage/emulated/0/patch_signed_7zip.apk I/Tinker.PatchFileUtil: safeDeleteFile, try to delete path: /storage/emulated/0/patch_signed_7zip.apk

说明加载补丁成功,重新启动App,会显示修改的内容。

DEMO 下载

参考致谢:

(1)http://www.jianshu.com/p/f485af00672a
当时按照此写的,最后才成功。
(2)、http://blog.csdn.net/a750457103/article/details/52815096
按照此写的,一直报code -3,一直没有解决。

tinkerId is not equal, base is tinker_id_1.0, but patch is 1.0

这里写图片描述

(3)、http://blog.csdn.net/xiejc01/article/details/52735920
这个基本是官方的DEMO,按照此写,会报错:

Current version is 2.10. If using the gradle wrapper, try editing the distributionUrl in D:\workspaceghnice\tinker\tinker-sample-android\gradle\wrapper\gradle-wrapp

(4)、官方接入指南
https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97

文档结构不是很清晰,没有重点。

0 0
原创粉丝点击