Android 腾讯热修复 Tinker + Flutter

简介: Android 腾讯热修复 Tinker + Flutter

热修复概述


热修复说白了就是”打补丁”,比如你们公司上线一个 App,用户反应有重大 Bug,需要紧急修复。

如果按照通常做法,那就是程序猿加班搞定 Bug,然后测试,重新打包并发布。这样带来的问题就是成本高,效率低。于是,热修复就应运而生。

一般通过事先设定的接口从网上下载无 Bug 的代码来替换有 Bug 的代码。这样就省事多了,用户体验也好。

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


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

  • Gradle 编译插件: tinker-patch-gradle-plugin
  • 核心 SDK 库: tinker-android-lib
  • 非 Gradle 编译用户的命令行版本: tinker-patch-cli.jar

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体积 较大 较小 较小
成功率 较高 较高 一般

总的来说:

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

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


Tinker 的已知问题


由于原理与系统限制,Tinker有以下已知问题:

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


Tinker 参数


详见官方文档:Tinker 接入指南

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

我们将原 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的类。 这里需要定义的类有:
  6. 你自己定义的Application类;
  7. Tinker库中用于加载补丁包的部分类,即com.tencent.tinker.loader.*;
  8. 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中;
  9. 其他一些你不希望被更改的类,例如Sample中的BaseBuildInfo类。这里需要注意的是,这些类的直接引用类也需要加入到loader中。或者你需要将这个类变成非preverify。
  10. 使用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去尝试。 |

制作签名文件与 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'
    }
}

Demo 地址:github.com/niezhiyang/…


Flutter 加载热更文件


先要搞明白我们到底需要动态替换一些什么?因此这里需要对 flutter 构建的产物有一定的了解了,怕有些小伙伴不太明白,这里也简单的带一下。


image.png


实际上,我们只需要 copy 一些 aar 文件,so 文件到 native 工程 lib 目录,就可以以 aar 的方式来跑 Flutter 的页面了,这也是典型的已 aar 方式接入 Flutter 的模式。其中,libapp.so,注意在 armeabi 中没有,如果你的 gralde 配置这么写的,abiFilters "armeabi" 那么,copy armeabi-v7a 下面的 so 到 armeabi 中,也是没有任何问题的,关于构建产物提取到原生工程就介绍到这里。线上问题,并不涉及到资源的话,我们只需要替换libapp.so即可实现热更新。

Tinker 好像具备修复 so 的功能吧,可已不可以直接使用 Tinker 呢?

答案是,并不能直接使用 Tinker,因为 Flutter 有自己的一套 so 加载流程,如下图,换句话说,Tinker 使用热修复后的 so 替换之前的 so,Flutter 不感知,因为它自己的环境会依然去读哪个没有修复的 so。



image.png


这里提供两种解决方案:

  • 自己写一个管理端,下发需要替换的 so 即可。但是,这样会涉及到补丁版本的管理,客户端补丁下载管理,而且因为 libapp.so 会比较大,我们目前来看一两个页面,就 8M 多了,因此我们也需要做 differ 拆分,然后下发到客户端之后在合并出功能修复的 so,如果团队实力雄厚有较多的定制型需求可以考虑这种方案。
  • Tinker 虽然具备修复 Android 原生 so 的问题,但是不能直接用来修复 flutter,但是,如果我们利用 Tinker 的热修复,将我们需要修复的 libapp.so 送达客户端,然后,我们想办法找到这个 so,在想办法 hook 以上Flutter 加载 libapp.so 就可以在继续使用 Tinker 热更新的生态的同时,达到 Flutter 热更新的效果。


代码实现


package com.xxx.xxx.common.utils;
import android.content.Context;
import android.util.Log;
import com.xxx.xxx.MyApplication;
import com.tencent.tinker.lib.tinker.Tinker;
import com.tencent.tinker.lib.tinker.TinkerLoadResult;
import com.tencent.tinker.lib.util.TinkerLog;
import com.tencent.tinker.loader.shareutil.ShareConstants;
import com.tencent.tinker.loader.shareutil.SharePatchFileUtil;
import java.io.File;
import java.lang.reflect.Field;
import io.flutter.view.FlutterMain;
/**
 * flutter 热更新
 * create by brzhang
 * date 2019.10.24
 */
public class FlutterPatch {
    private static final String TAG = "FlutterPatch";
    private FlutterPatch() {
    }
    public static void flutterPatchInit() {
        try {
            String libPath = findLibraryFromTinker(MyApplication.getIGameApplicationContext(), "lib/armeabi", "libapp.so");
            Log.e("FlutterPatch", "flutterPatchInit() called   " + libPath);
            Field field = FlutterMain.class.getDeclaredField("sAotSharedLibraryName");
            field.setAccessible(true);
            field.set(null, libPath);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static String findLibraryFromTinker(Context context, String relativePath, String libName) throws UnsatisfiedLinkError {
        final Tinker tinker = Tinker.with(context);
        libName = libName.startsWith("lib") ? libName : "lib" + libName;
        libName = libName.endsWith(".so") ? libName : libName + ".so";
        String relativeLibPath = relativePath + "/" + libName;
        if (tinker.isEnabledForNativeLib() && tinker.isTinkerLoaded()) {
            TinkerLoadResult loadResult = tinker.getTinkerLoadResultIfPresent();
            if (loadResult.libs == null) {
                return libName;
            }
            for (String name : loadResult.libs.keySet()) {
                if (!name.equals(relativeLibPath)) {
                    continue;
                }
                String patchLibraryPath = loadResult.libraryDirectory + "/" + name;
                File library = new File(patchLibraryPath);
                if (!library.exists()) {
                    continue;
                }
                //whether we check md5 when load
                boolean verifyMd5 = tinker.isTinkerLoadVerify();
                if (verifyMd5 && !SharePatchFileUtil.verifyFileMd5(library, loadResult.libs.get(name))) {
                    tinker.getLoadReporter().onLoadFileMd5Mismatch(library, ShareConstants.TYPE_LIBRARY);
                } else {
//                    System.load(patchLibraryPath);
                    TinkerLog.i(TAG, "findLibraryFromTinker success:" + patchLibraryPath);
                    return patchLibraryPath;
                }
            }
        }
        return libName;
    }
}
//然后在你的application 的 onCreate中调用
Flutter.startInitialization(this);
FlutterPatch.flutterPatchInit();

嗯,不到 80 行代码就搞定了 Flutter 热更新了,当然我这里只写了 armeabi 架构的,这是因为我们项目只需要这个架构,如果你的项目有多种,这里需针对性修改一下,最后可以看一下。


image.png


当 Tinker 下发补丁成功之后,我们的应用 data/data 目录会有这个生成这个 libapp.so 的补丁。然后使用上面的代码去偷梁换柱即可实现修复了。而且可以看一下,补丁的大小因为 Tinker 做了拆分包的缘故,会远远小于8M。


image.png


来对比下两种方案:


image.png


Android 端 Flutter 环境目前是:

  • Flutter 1.9.1+hotfix.6 • channel stable

如果 Flutter 升级到 Channel stable, v1.12.13+hotfix.5,热更新模块需要稍微修改一下:

public static void flutterPatchInit() {
    try {
        FlutterLoader flutterLoader = FlutterLoader.getInstance();
        String libPath = findLibraryFromTinker(IGameApplication.getIGameApplicationContext(), "lib/armeabi", "libapp.so");
        Log.e("FlutterPatch", "flutterPatchInit() called   " + libPath);
        Field field = FlutterLoader.class.getDeclaredField("aotSharedLibraryName");
        field.setAccessible(true);
        field.set(flutterLoader, libPath);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

对比之前,sAotSharedLibraryName 由静态私有变量变为了私有变量,而且不再放在 FlutterMain 当中了,放到了 FlutterLoader 当中。


参考



目录
相关文章
|
8天前
|
开发框架 Dart 开发工具
|
10天前
|
Android开发 开发者 UED
未来已来:Flutter 引领的安卓与跨平台开发奇幻之旅
【6月更文挑战第8天】Flutter,一款引领安卓与跨平台开发革新的框架,以其高效一致的开发体验、精美UI设计和跨平台能力脱颖而出。开发者可使用相同代码库在多平台开发,降低复杂性。其活跃社区和丰富生态系统进一步增强功能。示例代码展示了一个简单的计数器应用,体现Flutter的易用性。随着技术发展,Flutter有望塑造移动应用的未来,开启奇幻之旅。
|
1月前
|
数据库 Android开发
Android数据库框架-GreenDao入门,2024年最新flutter 页面跳转动画
Android数据库框架-GreenDao入门,2024年最新flutter 页面跳转动画
Android数据库框架-GreenDao入门,2024年最新flutter 页面跳转动画
|
1月前
|
XML 存储 Android开发
Android技能树 — Fragment总体小结,2024年最新腾讯面试gm
Android技能树 — Fragment总体小结,2024年最新腾讯面试gm
|
1月前
|
Android开发
Android Jetpack架构开发组件化应用实战,字节跳动+阿里+华为+腾讯等大厂Android面试题
Android Jetpack架构开发组件化应用实战,字节跳动+阿里+华为+腾讯等大厂Android面试题
|
1月前
|
算法 架构师 网络协议
对标腾讯T9架构师的 Android 面试题新鲜出炉,算法真的太重要了
对标腾讯T9架构师的 Android 面试题新鲜出炉,算法真的太重要了
|
1月前
|
Android开发
Flutter完整开发实战详解(六、 深入Widget原理),2024百度Android岗面试真题收录解析
Flutter完整开发实战详解(六、 深入Widget原理),2024百度Android岗面试真题收录解析
|
1月前
|
XML Dart Java
Flutter插件开发之APK自动安装,字节跳动Android岗面试题
Flutter插件开发之APK自动安装,字节跳动Android岗面试题
|
1月前
|
Android开发
Android热补丁动态修复实践,腾讯&字节&网易&华为Android面试题分享
Android热补丁动态修复实践,腾讯&字节&网易&华为Android面试题分享
|
1月前
|
Java Android开发 设计模式
flutter音视频开发,Android开发需要学什么
flutter音视频开发,Android开发需要学什么