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


我們的第三個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。

所以,我們該怎麼辦呢?
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

那麼,對比下兩種方案
方案對比 |
自己開發 |
基於tinker |
---|---|---|
需要自己寫修補程式下載邏輯 |
是 |
否 |
修補程式大小 |
比較大 |
小 |
需要自己開發管理端 |
需要 |
不需要 |
是要自己寫修補程式合成邏輯 |
需要 |
不需要 |
穩定性 |
需要驗證 |
很穩定 |
成本 |
很高 |
低 |
寫在最後,我們的Android端Flutter環境目前是
Flutter 1.9.1+hotfix.6 • channel stable •
如果想了解flutter打包腳本的小夥伴,可以翻看我之前的文章,或者留言。