微信热修复tinker初探

前言

Tinker简介

Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。

Tinker已知问题

1) Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件; 2) 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码 3) 在Android N上,补丁对应用启动时间有轻微的影响; 4) 不支持部分三星android-21机型,加载补丁时会主动抛出”TinkerRuntimeException:checkDexInstall failed”; 5) 对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。

2. Tinker接入指南

2.1. Gradle接入

gradle是推荐的接入方式,在gradle插件tinker-patch-gradle-plugin中官方帮助完成proguard、multiDex以及Manifest处理等工作。 在项目的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'

tinker参数配置

同样是在项目的app的gradle文件app/build.gradle文件里,在文件的末尾加上如下相关配置:

/**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 = true
    //for normal build
    //old apk file to build patch apk 旧包名
    tinkerOldApkPath = "${bakPath}/app-debug-0711-15-50-35.apk"
    //proguard mapping file to build patch apk  旧包混淆文件目录
    tinkerApplyMappingPath = "${bakPath}/app-debug-0711-15-50-35-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed 旧包R文件
    tinkerApplyResourcePath = "${bakPath}/app-debug-0711-15-50-35-R.txt"
    //only use for build all flavor, if not, just ignore this field  多渠道
    tinkerBuildFlavorDirectory = "${bakPath}/app-0711-15-50-35"
}
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.troy.tinkertrydemo.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 相关配置----------------结束-----------------------------------*/

配置参数参考GitHub: 官方github

自定义Application类

为了使真正的Application实现可以在补丁包中修改,需要把Appliction类的所有逻辑移动到ApplicationLike代理类中。 即创建一个SampleApplication类,继承DefaultApplicationLike

@DefaultLifeCycle(application = "com.example.qiongyu_jiang.tinkerdemo.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。 DefaultApplicationLike不是继承自Application,需要用注解来设置项目中真正的Application,Tinker插件会自动生成真正的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"/>

加载补丁

加载补丁的代码是这个:

TinkerInstaller.onReceiveUpgradePatch(this.getApplication(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");

补丁文件在被加载之后会被删除。

具体操作与测试

具体操作

编译打补丁前的APK

在项目右侧的gradle操作栏中选择assembleDebug

当然也可以采用命令行的方式

./gradlew assembleRelease

在app/build/outputs/apk下生成app-debug.apk,在bakApk文件夹下生成相关备份

如果选择的生成方式是release,同时也会生成一个mapping.txt文件,debug是没有的。 随后在终端进入apk所在目录下,将apk安装到手机上:

adb install -r app-debug-0711-11-24-18.apk

这样就成功的把打补丁前的app安装到手机上。下面是demo的样子:

生成patchAPK

在build.gradle里修改部分文件,使其与刚刚生成的文件名一致,这里说明旧的apk是什么。

ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    //Tinker是否可用
    tinkerEnabled = true
    //for normal build
    //old apk file to build patch apk 旧包名
    tinkerOldApkPath = "${bakPath}/app-debug-0711-11-24-18.apk"
    //proguard mapping file to build patch apk  旧包混淆文件目录
    tinkerApplyMappingPath = "${bakPath}/app-debug-0711-11-24-18-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed 旧包R文件
    tinkerApplyResourcePath = "${bakPath}/app-debug-0711-11-24-18-R.txt"
    //only use for build all flavor, if not, just ignore this field  多渠道
    tinkerBuildFlavorDirectory = "${bakPath}/app-0711-11-24-18"
}

随后修改原来的项目文件,比如加个按钮。 然后在项目右侧的gradle的tinker目录下选择tinkerPatchDebug,生成补丁包文件。生成后放在outputs目录下。

将此文件推送到手机的SD卡中

adb push ./app/build/outputs/tinkerPatch/debug/patch_signed_7zip.apk /storage/sdcard0/

重启APP,补丁推送成功。

测试

经测试,四大组件的修改,增加代码,增加方法,增加类,增加布局文件,增加图片,均能修复成功。

测试项

修改代码

增加类

增加布局文件

增加资源文件(图片)

补丁包大小

4kb

4kb

32kb

22kb(图片大小9.42kb)

启动时长(前)

106ms

117ms

144ms

120ms

启动时长(后)

117ms

144ms

120ms

121ms

内存占用(前)

8.43/17.17 mb

7.27/18.33 mb

7.26/18.34 mb

5.09/20.51 mb

内存占用(后)

7.27/18.33 mb

7.26/18.34 mb

5.09/20.51 mb

6.31/19.29 mb

原来项目APK大小:1402KB 加入classpath (‘com.tencent.tinker:tinker-patch-gradle-plugin:1.7.7’)后:1402KB 加入compile(‘com.tencent.tinker:tinker-android-lib:1.7.7’)后:1550KB 加入provided(‘com.tencent.tinker:tinker-android-anno:1.7.7’)后:1550KB

一些坑

1) tinkerId is not set tinker采用的是git的版本号来设置tinkerId,项目需要设置git并且设置完了记得找段代码commit一下。 2)ignoreWarning 在studio终端中输入gradlew tinkerPatchDebug来生成pach时可能会报以下异常: Warning:ignoreWarning is false, but resources.arsc is changed, you should use applyResourceMapping mode to build the new apk, otherwise, it may be crash at some times com.tencent.tinker.build.util.TinkerPatchException: ignoreWarning is false, but resources.arsc is changed, you should use applyResourceMapping mode to build the new apk, otherwise, it may be crash at some times 将build.gradle中的ignoreWarning = false改为true即可

官方常见问题

参考博客

http://blog.csdn.net/qiyei2009/article/details/57079973 http://blog.csdn.net/happy_horse/article/details/55224356

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏我和未来有约会

Kit 3D 更新

Kit3D is a 3D graphics engine written for Microsoft Silverlight. Kit3D was inita...

2546
来自专栏转载gongluck的CSDN博客

cocos2dx 打灰机

#include "GamePlane.h" #include "PlaneSprite.h" #include "BulletNode.h" #include...

5486
来自专栏魂祭心

原 canvas绘制clock

4104
来自专栏张善友的专栏

LINQ via C# 系列文章

LINQ via C# Recently I am giving a series of talk on LINQ. the name “LINQ via C...

2645
来自专栏张善友的专栏

Miguel de Icaza 细说 Mix 07大会上的Silverlight和DLR

Mono之父Miguel de Icaza 详细报道微软Mix 07大会上的Silverlight和DLR ,上面还谈到了Mono and Silverligh...

2717
来自专栏pangguoming

Spring Boot集成JasperReports生成PDF文档

由于工作需要,要实现后端根据模板动态填充数据生成PDF文档,通过技术选型,使用Ireport5.6来设计模板,结合JasperReports5.6工具库来调用渲...

1.2K7
来自专栏一个会写诗的程序员的博客

Spring Reactor 项目核心库Reactor Core

Non-Blocking Reactive Streams Foundation for the JVM both implementing a Reactiv...

2182
来自专栏一个爱瞎折腾的程序猿

sqlserver使用存储过程跟踪SQL

USE [master] GO /****** Object: StoredProcedure [dbo].[sp_perfworkload_trace_s...

2070
来自专栏我和未来有约会

Silverlight第三方控件专题

这里我收集整理了目前网上silverlight第三方控件的专题,若果有所遗漏请告知我一下。 名称 简介 截图 telerik 商 RadC...

4035
来自专栏张善友的专栏

Silverlight + Model-View-ViewModel (MVVM)

     早在2005年,John Gossman写了一篇关于Model-View-ViewModel模式的博文,这种模式被他所在的微软的项目组用来创建Expr...

2978

扫码关注云+社区