腾讯热修复Tinker的详解和使用

6,371 阅读10分钟

转载请标明出处:blog.csdn.net/qq_33408235…
本人github:github.com/niezhiyang

1. 热修复概述

热修复说白了就是”打补丁”,比如你们公司上线一个app,用户反应有重大bug,需要紧急修复。如果按照通
常做法,那就是程序猿加班搞定bug,然后测试,重新打包并发布。这样带来的问题就是成本高,效率低。于是,热
修复就应运而生.一般通过事先设定的接口从网上下载无Bug的代码来替换有Bug的代码。这样就省事多了,用
户体验也好。

2.Tinker的概述(本介绍是从官网上截取的)

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

它主要包括以下几个部分:

  1. gradle编译插件: tinker-patch-gradle-plugin
  2. 核心sdk库: tinker-android-lib
  3. 非gradle编译用户的命令行版本: tinker-patch-cli.jar

2.1 Tinker和阿里的AndFix、美团的Robust以及QZone的比较

Tinker QZone AndFix Robust
类替换 yes yes no
So替换 yes no no
资源替换 yes yes no
全平台支持 yes yes yes
即时生效 no no yes
性能损耗 较小 较大 较小
补丁包大小 较小 较大 一般
开发透明 yes yes no
复杂度 较低 较低 复杂
gradle支持 yes no no
Rom体积 较大 较小 较小
成功率 较高 较高 一般

总的来说:

  1. AndFix作为native解决方案,首先面临的是稳定性与兼容性问题,更重要的是它无法实现类替换,它是需要大量额外的开发成本的;
  2. Robust兼容性与成功率较高,但是它与AndFix一样,无法新增变量与类只能用做的bugFix方案;
  3. Qzone方案可以做到发布产品功能,但是它主要问题是插桩带来Dalvik的性能问题,以及为了解决Art下内存地址问题而导致补丁包急速增大的。

特别是在Android N之后,由于混合编译的inline策略修改,对于市面上的各种方案都不太容易解决。而Tinker热补丁方案不仅支持类、So以及资源的替换,它还是2.X-7.X的全平台支持。利用Tinker我们不仅可以用做bugfix,甚至可以替代功能的发布。Tinker已运行在微信的数亿Android设备上,那么为什么你不使用Tinker呢?

2.2 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以及桌面图标

3. Tinker 接入指南

其实官网说的已经差不多了,只是想实践一下,并且把坑给大伙填平,接入的方式有两种接入办法

  • gradle接入(官网推荐的,也是本篇介绍的方法)
  • 命令行接入(本篇不介绍)

3.1 添加依赖

  • 在根目录下的build.gradle中,添加tinker-patch-gradle-plugin的依赖
 dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.8.0')// tinker的pluggin
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
  • 在app的build.gradle中添加依赖
dependencies {
    //可选,用于生成application类 
    provided('com.tencent.tinker:tinker-android-anno:1.8.0')
    //tinker的核心库
    compile('com.tencent.tinker:tinker-android-lib:1.8.0') 
    //分包库  下面用得到
      compile 'com.android.support:multidex:1.0.1'
}
  • 还是在app的build.gradle的最上面添加插件依赖
//apply tinker插件
apply plugin: 'com.tencent.tinker.patch'
  • sync Now 之后会报错,错误是 tinkerId is not set!!!(别着急下面注重介绍这个)

3.2 app中的build.gradle中的具体配置参数与介绍

3.2.1 讲一讲参数的意思

我们将原apk包称为基准apk包,tinkerPatch直接使用基准apk包与新编译出来的apk包做差异,得到最终的补丁包。gradle配置的参数详细解释如下:

参数 默认值 描述
tinkerPatch 全局信息相关的配置项
tinkerEnable true 是否打开tinker的功能。
oldApk null 基准apk包的路径,必须输入,否则会报错。
newApk null 选填,用于编译补丁apk路径。如果路径合法,即不再编译新的安装包,使用oldApk与newApk直接编译。
outputFolder null 选填,设置编译输出路径。默认在build/outputs/tinkerPatch
ignoreWarning false 如果出现以下的情况,并且ignoreWarning为false,我们将中断编译。因为这些情况可能会导致编译出来的patch包带来风险:
1. minSdkVersion小于14,但是dexMode的值为”raw”;
2. 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver…);
3. 定义在dex.loader用于加载补丁的类不在main dex中;
4. 定义在dex.loader用于加载补丁的类出现修改;
5. resources.arsc改变,但没有使用applyResourceMapping编译。
useSign true 在运行过程中,我们需要验证基准apk包与补丁包的签名是否一致,我们是否需要为你签名。
buildConfig 编译相关的配置项
applyMapping null 可选参数;在编译新的apk时候,我们希望通过保持旧apk的proguard混淆方式,从而减少补丁包的大小。这个只是推荐设置,不设置applyMapping也不会影响任何的assemble编译
applyResourceMapping null 可选参数;在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配,这样不仅可以减少补丁包的大小,同时也避免由于ResId改变导致remote view异常。
tinkerId null 在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等。
keepDexApply false 如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。
isProtectedApp false 是否使用加固模式,仅仅将变更的类合成补丁。注意,这种模式仅仅可以用于加固应用中。
dex dex相关的配置项
dexMode jar 只能是’raw’或者’jar’。
对于’raw’模式,我们将会保持输入dex的格式。
对于’jar’模式,我们将会把输入dex重新压缩封装到jar。如果你的minSdkVersion小于14,你必须选择‘jar’模式,而且它更省存储空间,但是验证md5时比’raw’模式耗时。默认我们并不会去校验md5,一般情况下选择jar模式即可。
pattern [] 需要处理dex路径,支持*、?通配符,必须使用’/’分割。路径是相对安装包的,例如assets/…
loader [] 这一项非常重要,它定义了哪些类在加载补丁包的时候会用到。这些类是通过Tinker无法修改的类,也是一定要放在main dex的类。
这里需要定义的类有:
1. 你自己定义的Application类;
2. Tinker库中用于加载补丁包的部分类,即com.tencent.tinker.loader.*;
3. 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中;
4. 其他一些你不希望被更改的类,例如Sample中的BaseBuildInfo类。这里需要注意的是,这些类的直接引用类也需要加入到loader中。或者你需要将这个类变成非preverify。
5. 使用1.7.6版本之后版本,参数1、2会自动填写。
lib lib相关的配置项
pattern [] 需要处理lib路径,支持*、?通配符,必须使用’/’分割。与dex.pattern一致, 路径是相对安装包的,例如assets/…
res res相关的配置项
pattern [] 需要处理res路径,支持*、?通配符,必须使用’/’分割。与dex.pattern一致, 路径是相对安装包的,例如assets/…,务必注意的是,只有满足pattern的资源才会放到合成后的资源包。
ignoreChange [] 支持*、?通配符,必须使用’/’分割。若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改。 最极端的情况,ignoreChange与上面的pattern一致,即会完全忽略所有资源的修改。
largeModSize 100 对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。这可以降低补丁包的大小,但是会增加合成时的复杂度。默认大小为100kb
packageConfig 用于生成补丁包中的’package_meta.txt’文件
configField TINKER_ID, NEW_TINKER_ID configField(“key”, “value”), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动写入configField。在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。但是建议直接通过修改代码来实现,例如BuildConfig。
sevenZip 7zip路径配置项,执行前提是useSign为true
zipArtifact null 例如”com.tencent.mm:SevenZip:1.1.10”,将自动根据机器属性获得对应的7za运行文件,推荐使用。
path 7za 系统中的7za路径,例如”/usr/local/bin/7za”。path设置会覆盖zipArtifact,若都不设置,将直接使用7za去尝试。

3.2.2 根据我的 build.gradle 或者官网的 build.gradle 配置自己的build.gradle,其实我的跟官网的一样,就改了一下javaversion,其他的默认就可以

其实直接把 我的代码或者官网的代码,复制到自己 build.gradle中,自己build.gradle中已经有的东西就不要复制了

3.2.3 制作签名文件与build.gradle中的相对应

 signingConfigs {
    release {
        try {
            storeFile file("../demojks.jks") // .. 就是项目的根目录  demo.jks与app目录同级
            storePassword '123456'
            keyAlias 'demojks'
            keyPassword '123456'
        } catch (ex) {
            throw new InvalidUserDataException(ex.toString())
        }
    }

    debug {
        storeFile file("../demojks.jks")
        storePassword '123456'
        keyAlias 'demojks'
        keyPassword '123456'
    }
}

3.3 模拟线上apk制作(也就是发布到市场的apk)

写个类 MyApplicationLike 继承 ApplicationLike
ApplicationLike并不是一个application,但是得重新onCreate方法
代码如下: 注意点,此类中有个注解 @DefaultLifeCycle 里面的application是自定义的application,记住在清单文件中使用


    @DefaultLifeCycle(application = ".MyApplication",//而这个类是真正的自定义的application,别忘了在AndroidManifest.xml中使用
            flags = ShareConstants.TINKER_ENABLE_ALL,
            loadVerifyFlag =true)
    public class MyApplicationLike extends ApplicationLike {
        public MyApplicationLike(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);

        }
        @Override
            public void onCreate() {
                super.onCreate();
            }
    }

在MainActivity.java中故意写错代码,比如当点击这个button的时候肯定会崩溃

  @Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    // 加载补丁
    loadPatch();
}

private void loadPatch() {
    String path = Environment.getExternalStorageDirectory().getAbsolutePath()+"/patch.apk";
    File file = new File(path);
    if (file.exists()){
        // 加载补丁
        TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path);
    }else {
        Toast.makeText(this, "补丁不存在", Toast.LENGTH_SHORT).show();
    }
}

public void test(View view) {
   int i=1/0;  // 当点击这个button的时候肯定会崩溃
}

3,3.1 运行:

会报错看下面错误,意思就是说tinker不支持 Instant Run, 打开Studio的File–>settings–>搜索 instant run, 关闭即可
这里写图片描述

3.3.2 demo.apk和补丁apk都必须是release版本,把Build Variant改为release版本,如下图

这里写图片描述

3.3.3 运行完之后看下图位置,这个apk就是你打包完成,发布在线上的apk

这里写图片描述

3.4 补丁包apk 打包

  • 打包之前肯定需要把线上的bug修复掉呀, 把MainActivity点击button的时候改一下
    public void test(View view) {
    //        int i=1/0;
       Toast.makeText(this, "我被修好了,不崩溃了", Toast.LENGTH_SHORT).show();
        }
  • 把app的build.gradle中的tinkerOldApkPath , tinkerApplyMappingPath ,tinkerApplyResourcePath 都改成刚才线上apk的东西,也就是 3.3.3 的图中的东西,tinkerBuildFlavorDirectory 就不知道什么玩意了,我也改了一下
    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-release-1026-17-04-59.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/app-release-1026-17-04-59-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-release-1026-17-04-59-R.txt"

    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/app-release-1026-17-04-59"
}
  • 打开Gradle如下图,双击 tinkerPatchRelease就会编译出来咱们想要的补丁
    这里写图片描述
    补丁apk位置在如下图
    这里写图片描述
    “patch_singned_7zip.apk”就是补丁包, 更改名字改为patch.apk(与之前加载时对应),放到sd卡的根目录下,再次运行线上的apk,发现不崩溃了,bug修复好了,如下图
    这里写图片描述

源码位置github