微信热更新Tinker 使用及爬坑(一)

什么是热修复

**定义 **: 热修复(HotFix)是以补丁的方式动态修复紧急Bug,不再需要重新发布App,不需要用户重新下载覆盖安装的方式来实现代码的替换修改。这里就不多啰嗦了,可以自行搜索网上的介绍。

目前主流HotFix方案对比

HotFix方案TinkerQZoneAndFixRobust
类替换yesyesnono
So替换yesnonono
资源替换yesyesnono
全平台支持yesyesnoyes
即时生效nonoyesyes
性能损耗较小较大较小较小
补丁包大小较小较大一般一般
开发透明yesyesnono
复杂度较低较低复杂复杂
Rom体积Dalvik较大较小较小较小
成功率较高(95%)较高一般最高(99.9%)

</br>
注:

  • Tinker的成功率数据,是从微信团队张绍文同学那儿打听得到的,该数据是微信APP自身的成功率,可信度高;
  • Robust的成功率数据,来自美团Robust开源项目官方文档。
  • QZone成功率和Tinker应该在同一水平(或稍低点)的样子。
  • AndFix 是公司以前就接入的,内部测试成功率只有80%左右(仅供参
    考),而且修复起来还有诸多限制。

Tinker的原理

《微信热更新Tinker 使用及爬坑(一)》 微信Tinker原理图
《微信热更新Tinker 使用及爬坑(一)》 Tinker流程图

Tinker的优势和特性

综合考虑来说,Tinker的补丁包以及功能全面性、稳定性是比较吸引人的,并且功能还能做到类替换 、资源替换以及So替换。这样一来它就不仅仅是热修复了,还能做到热更新。因此我们最后采用了Tinker (其实还是因为微信几亿设备也是用的Tinker这套方案,靠谱点)。

微信和阿里还提供了补丁后台托管,版本管理SDK ,不缺钱或者不想因为热修复对项目代码造成侵入性的话,也可以直接使用微信或阿里封装好的傻瓜式接入方案,微信 Tinker Patch 方案目前是补丁包日请求量1w以内免费;阿里云 Sophix 目前还在公测阶段,暂时不收费

微信 Tinker Patch 官方地址:Tinker Patch
阿里 SopHix 官方地址:Sophix

接入Tinker步骤

1.添加工程gradle plugin依赖

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

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

2.添加tinker库依赖及插件应用

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

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

3.gradle配置Tinker的一些参数

这步可参考Tinker 开源项目 sample中的app/build.gradle。

4.自定义Application代理类

程序启动时会加载默认的Application类,这导致我们补丁包是无法对它做修改了。如何规避?在这里我们并没有使用类似InstantRun hook Application的方式,而是通过代码框架的方式来避免,这也是为了尽量少的去反射,提升框架的兼容性。

这里我们要实现的是完全将原来的Application类隔离起来,即其他任何类都不能再引用我们自己的Application。将代码都放到代理类ApplicationLike中来,我们需要做的其实是以下几个工作:

  • 将我们项目原来的Application类以及它的Base类的所有代码拷贝到创建的ApplicationLike继承类中,例如SampleApplicationLike。你也可以直接将自己的Application改为继承ApplicationLike,然后做改动;
  • Application的attachBaseContext方法实现要单独移动到onBaseContextAttached中;
  • 对ApplicationLike中,引用application的地方改成getApplication();
  • 对其他引用Application或者它的静态对象方法的地方,改成引用ApplicationLike的静态对象与方法;

更详细的内容大家可以参考sample例子里SampleApplicationLike的做法。
GitHub地址: tinker/tinker-sample-android/app/build.gradle

对于为何放弃Instant Run 实现,而采用代理的方案,张绍文同学是这么解释的:

《微信热更新Tinker 使用及爬坑(一)》 Tinker张绍文博客截图

详情可参考微信Android团队技术分享博客,地址链接:WeMobileDev/article

5.Tinker SDK初始化以及调用

初始化

创建一个类继承自ApplicationLike ,并添加DefaultLifeCycle注解,指定需要自动生成的Application路径和名称,将AndroidManifest.xml里面的application名称设置为它 :

 <application
        android:name=".app.SampleApplication"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">

代理类SampleApplicationLike 代码:

@SuppressWarnings("unused")
@DefaultLifeCycle(application = "tinker.sample.android.app.SampleApplication",
                  flags = ShareConstants.TINKER_ENABLE_ALL,
                  loadVerifyFlag = false)
public class SampleApplicationLike extends ApplicationLike {
    private static final String TAG = "Tinker.SampleApplicationLike";

    public SampleApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
                                 long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    }

    /**
     * install multiDex before install tinker
     * so we don't need to put the tinker lib classes in the main dex
     *
     * @param base
     */
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        //you must install multiDex whatever tinker is installed!
        MultiDex.install(base);

        SampleApplicationContext.application = getApplication();
        SampleApplicationContext.context = getApplication();
        TinkerManager.setTinkerApplicationLike(this);

        TinkerManager.initFastCrashProtect();
        //should set before tinker is installed
        TinkerManager.setUpgradeRetryEnable(true);

        //optional set logIml, or you can use default debug log
        TinkerInstaller.setLogIml(new MyLogImp());

        //installTinker after load multiDex
        //or you can put com.tencent.tinker.** to main dex
        TinkerManager.installTinker(this);

        Tinker.with(getApplication());//初始化热更新SDK
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
        getApplication().registerActivityLifecycleCallbacks(callback);
    }

}

写好之后Sync一下,它会在编译时自动生成SampleApplication。如果不想通过注解自动生成,我们也可以手动写这个Application放到项目里,但构造方法需要设置好代理类的path:

package tinker.sample.android.app;

import com.tencent.tinker.loader.app.TinkerApplication;

public class SampleApplication extends TinkerApplication {

    public SampleApplication() {
        super(7, "tinker.sample.android.app.SampleApplicationLike", "com.tencent.tinker.loader.TinkerLoader", false);
    }

}

调用Tinker合并与清除补丁:

loadPatch :

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

loadLibrary :

    // #method 1, hack classloader library path
                TinkerLoadLibrary.installNavitveLibraryABI(getApplicationContext(), "armeabi");
                System.loadLibrary("stlport_shared");

                // #method 2, for lib/armeabi, just use TinkerInstaller.loadLibrary
//                TinkerLoadLibrary.loadArmLibrary(getApplicationContext(), "stlport_shared");

                // #method 3, load tinker patch library directly
//                TinkerInstaller.loadLibraryFromTinker(getApplicationContext(), "assets/x86", "stlport_shared");

cleanPatch:

Tinker.with(getApplicationContext()).cleanPatch();

6.补丁包生成与安装

6.1 打开右上侧Gradle,并双击assembleDebug,生成基准包。

《微信热更新Tinker 使用及爬坑(一)》 assembleDebug

6.2 安装基准包

app/build/bakApk 下,可以看到生成了基准包Apk以及R文件、mapping(mapping文件混淆下才会有),然后将该Apk安装到手机中。

平时开发测试时我们可通过AS 开发工具下方的Terminal 窗口 输入如下命令将APK Push到手机:

//APK已安装情况
adb install -r app/build/bakApk/app-debug-0620-14-12-54.apk 
//APK未安装
adb install app/build/bakApk/app-debug-0620-14-12-54.apk 

然后将app/build/bakApk 下生成的文件路径填入gradle 的ext 中

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-0620-14-12-54.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-0620-14-12-54-R.txt"
    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}

6.3 生成补丁包

oldApk路径填好之后,开始修改Bug,bug改完之后,双击tinkerPatchDebug,这个gradle命令会对当前代码和oldApk进行差异对比,在app/build/output/tinkerPatch下生成补丁。

《微信热更新Tinker 使用及爬坑(一)》 这里写图片描述

生成的补丁信息,我们需要的补丁包是patch_signed_7zip.apk:

《微信热更新Tinker 使用及爬坑(一)》 这里写图片描述

6.4 补丁包下载安装

补丁包生成之后,我们则可把它放到服务器后台,客户端通过接口去下载补丁包了,测试中我们一样是通过adb 将文件push到手机sd卡根目录:

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

补丁包push到手机之后,我们在基准包代码中已经写了如下代码,此时返回基准包触发该代码,则可把补丁包合并到基准包实现热更新:

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

爬坑及小技巧:

1.TinkerId 设置问题。

git项目中会有TinkerId,如果是通过非Clone方式拉取的代码,则需要push一次同步到Git中才会有,如果为了测试方便,也可以直接在 gtadle.properties文件指定tinkerId,如:TINKER_ID = 1

2.Java1.8 兼容问题

在gradle中设置 JavaVersion 为1.8,导致Application代理失败造成一启动就崩溃问题,有两种办法:

  • 去除gradle tinker-android-anno 依赖库,不通过DefaultLifeCycle注解自动生成Application的办法,采用直接手动创建Application,并在构造方法中(第二个参数),设置代理类。
  • anno 注解不支持 jackOptions 因此需要通过添加 lambda插件来兼容Java1.8
    //添加插件
    apply plugin: ‘me.tatarka.retrolambda’

3.补丁包push到sd卡:

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

4.安装apk:

adb install app/build/bakApk/app-debug-0620-14-12-54.apk

adb install -r app/build/bakApk/app-debug-0620-14-12-54.apk

5.多渠道打包:

通过flavor 生成渠道包的情况下,会因为BuildInfo不同而导致Apk的Dex文件不同,从而导致每个渠道的补丁包都需要一对一,那么假如有几十个渠道,则同样需要几十个渠道的补丁包,这是非常不合理的。那么怎么办呢?

解决方案:
1.将渠道信息写在AndroidManifest.xml或文件中,例如channel.ini;
2.将渠道信息写在apk文件的zip comment中,这样一来,所有渠道包的Dex文件都是相同的,我们就可以通过assembleRelease 生成的基准包,来打补丁包。所有渠道都可以共用这个补丁包。至于这种渠道打包方式的工具,可以使用GitHub上开源的 packer-ng-plugin 或者可使用美团点评使用了V2 Scheme签名的 walle
3.若不同渠道存在功能上的差异,建议将差异部分放于单独的dex或采用相同代码不同配置方式实现;

强烈建议采取第二种方式!!!

未完待续~

欢迎交流讨论,有问题也非常欢迎指出不足之处~

    原文作者:一枕黄粱终成梦
    原文地址: https://www.jianshu.com/p/566aa853c0ce
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞