帶你不到80行代碼搞定Flutter熱更新

一、需要熱更新的背景

Flutter作為跨平台方案,相信最近很多小夥伴都已經開始接入了,我們的接入參考官方wiki,在成功接入之後,我們為了在CI構建中不依賴fluter環境,採用了調試模式使用源碼的方式,打包的時候使用aar的方式,這樣做的好處是,既能夠保留開發期間的可調試行,也能保障構建環境不依賴Flutter環境。為此,我們團隊雙端各寫了一個腳本,來切換接入模式,且自動將Flutter產物提提取並推送到原生工程以便打包。成功上線幾個業務之後,我們遇到flutter的線上問題,大家可能和我當時的感受一樣,沒有一個比較好的開源工具來對Flutter進行熱修復,在網上搜一下,如這篇,大多數表示只講解原理,看原理理論上是行得通的,但是遺憾的是並沒有具體實現過程,於是我們決定立足原理,來探索在Android上怎麼實現Flutter頁面的熱更新,以下是熱更新實現後的效果:

gif
熱修復了

我們的第三個tab是一個Flutter實現的頁面,可以看到這個頁面中,banner的 福利券兌好禮 文案被替換了,那麼我們是如何做到的呢?

二,實現熱更新之路的探索

要熱更新flutter頁面,我們首先要搞明白我們到底需要動態替換一些什麼?因此這裡需要對flutter構建的產物有一定的了解了,怕有些小夥伴不太明白,這裡也簡單的帶一下。

產物

如上圖所示,實際上,我們只需要copy一些aar文件,so文件到native工程lib目錄,就可以已aar的方式來跑Flutter的頁面了,這也是典型的已aar方式接入Flutter的模式。其中,libapp.so,注意在armeabi中沒有,如果你的gralde配置這麼寫的,abiFilters "armeabi" 那麼,copy armeabi-v7a下面的so到armeabi中,也是沒有任何問題的,關於構建產物提取到原生工程就介紹到這裡。

那麼,問題來了,如果我們的Flutter頁面出來bug,比如,某個文案寫錯了,而且是寫死的,某個控件在小屏機型上溢出了,沒動態計算寬高等等等,我們怎麼辦?

當然是進行熱修復了,那麼熱修復,我們要提供那些文件出來呢,都需要提供嗎?

答案是,並不需要都提供,如果是邏輯問題,而且這是我們大概率會遇到的問題,切發生的最多的線上問題,並不涉及到資源的話,我們只需要替換libapp.so即可實現熱更新。

說到這裡,有人說,tinker好像具備修復so的功能吧,可已不可以直接使用tinker呢?

答案是,並不能直接使用tinker,因為Flutter有自己的一套so加載流程,如下圖,換句話說,tinker使用熱修復後的so替換之前的so,Flutter不感知,因為它自己的環境會依然去讀哪個沒有修復的so

flutter自己的so加載過程

所以,我們該怎麼辦呢?

1、既然我們知道只要替換so既可以實現Flutter邏輯錯誤的修復,那麼我們自己寫一個管理端,下發需要替換的so即可呀,好像行得通,嗯,但是,這樣會涉及到補丁版本的管理,客戶端補丁下載管理,而且因為libapp.so會比較大,我們目前就一兩個頁面,就8M多了,因此我們也需要做差分,然後下發到客戶端之後在合併出功能修復的so,好像理論上完全是OK的,不過等下,還有更好的嗎的方案嗎?

2、我了解到tinker,貌似是可以修復so的,而且王者人生Android端目前原生就是使用tinker來做熱修復的,那麼,我們是否可以對tinker這個進行利用呢,我們在前面就知道,tinker雖然具備修復Android原生so的問題,但是不能直接用來修復flutter,但是,如果我們利用tinker的熱修復,將我們需要修復的libapp.so送達客戶端,然後,我們想辦法找到這個so,在想辦法hook 以上Flutter加載 libapp.so,換句話說,就是想辦法讓flutter加載tinker為我們準備好的熱修復後的so

很幸運,這個猜想最終被成功實施出來了。

三,具體實現的細節

實現起來非常簡單:

首先,我們在原生Android工程接入tinker,具體接入流程參考bugly文檔即可。

其次,我們怎麼去拿到tinker為我們生成的那個修復後的libapp.so,並且偷梁換柱呢,答案是:

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架構的,這是因為我們項目只需要這個架構,如果你的項目有多種,這裡需針對性修改一下,最後可以看一下

當tinker下發補丁成功之後,我們的應用data/data目錄會有這個生成這個libapp.so的補丁了。然後使用上面的代碼去偷梁換柱即可實現修復了。而且可以看一下,補丁的大小因為tinker做了查分包的緣故,會遠遠小於8M

bugly上tinker熱修復

那麼,對比下兩種方案

方案對比

自己開發

基於tinker

需要自己寫補丁下載邏輯

補丁大小

比較大

需要自己開發管理端

需要

不需要

是要自己寫補丁合成邏輯

需要

不需要

穩定性

需要驗證

很穩定

成本

很高

寫在最後,我們的Android端Flutter環境目前是

Flutter 1.9.1+hotfix.6 • channel stable •

如果想了解flutter打包腳本的小夥伴,可以翻看我之前的文章,或者留言。