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

什么是热修复

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

目前主流HotFix方案对比

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


注:

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

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张绍文博客截图

详情可参考微信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,生成基准包。

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下生成补丁。

这里写图片描述

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

这里写图片描述

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或采用相同代码不同配置方式实现;

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

未完待续~

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

猜你喜欢

转载自blog.csdn.net/qq_22393017/article/details/73498559