一、背景及綜述
Flutter 在目前跨平台方案中有更好的平台一緻性以及更優的體驗。但對于本身已有成熟的業務代碼的項目來說,更多的是采用混合棧的方式,在不變更原有 App 業務的基礎上,将 Flutter 能力擴充為子子產品進行接入和開發。這樣并不影響原有的業務和原生能力,又可以結合業務需求進行技術選擇。

混合棧涉及到 Flutter 頁面與原生頁面的跳轉。而官方的路由方案,在多引擎下有着通信隔離,資源不共享,極大的記憶體損耗等缺陷。
業内采用較廣泛是單引擎複用方案,但這仍有不少痛點,展現在兩個方面:
混合棧路由在使用時,仍有記憶體異常;
底層代碼的修改,需要不斷踩坑。
為了解決這些問題,心悅抽離出了一套混合棧路由架構 TRouter。
單引擎下記憶體進一步優化,解決了打開多個 Flutter 頁面時記憶體異常增長(Boost 等方案下仍有記憶體異常);
規避底層代碼修改不可見導緻的項目風險,解決過度耦合 io.flutter 包導緻的 sdk 更新困難。
本文的目标是闡述 Flutter 實踐混合棧路由中遇到的痛點,以及 TRouter 是如何去解決的。最後會對目前的方案進行橫向對比,講述下一步的計劃。
二、混合內建面臨的問題
項目最終明确選用了單引擎複用的方案,業内未解決而我們面臨的痛點有兩個:1. iOS側的記憶體增長異常;2. Android側 底層修改不透明給項目帶來風險。在介紹TRouter之前,本節會讨論問題的成因,以及為什麼說業内方案存在缺陷。
官方并沒有很好解決混合棧路由所遇到的問題。
Flutter 的技術鍊路是建立在 C++ 編寫的 Engine 和 Dart 編寫的 Framework 層組成。主要構成如下圖所示:
可以明确的是:
Engine 管理着 Flutter 所使用的四個線程,本身是一個較重的一個對象。
isolate 管理着 Dart 層記憶體和單線程控制的運作實體。isolate 本身意思是“隔離”,每個 isolate 之間的記憶體和邏輯是隔離的,是以對應的 Engine 也是資源不共享的。
Engine 依賴于原生的某個視圖元件提供渲染的能力,比如純 Flutter 應用就隻在單獨一個 Activity/ViewController 上建立了 Engine 以提供 Flutter 的視圖渲染。
在混合棧路由上,雖然 Dart 層本身有提供 navigator 等路由方式,但當我們把 Flutter 內建為原生的子產品或能力時,一定會出現 Native -> Flutter -> Native -> Flutter… 這種混合頁面跳轉情況。
這樣存在問題是:如何儲存 Flutter 頁面的狀态,并且在頁面回退或跳轉時,在正确的時機恢複或切換 Flutter 的渲染内容。
Google 官方提供的是 keep it simple 的方案,即間隔的 Flutter 頁面單獨使用一個新的 Engine 來單獨維持一份視圖渲染,跳轉時就無需考慮 Dart 層頁面切換。
這種方案弊端很多,首先是 Engine 的線性增多,帶來記憶體的極大損耗。如下圖所示,Android 端多引擎下打開 5 個頁面記憶體增量對比:
其次由于 isolate 隔離,Dart 側圖檔緩存等資源也無法共享,所有通信都需要經過原生,使通信有極高的複雜度。
是以多引擎不能滿足項目的性能要求。
由于多引擎的缺陷,業内的做法一般是對 isolute 或 Engine 進行複用來解決。影響力較大的是以 FlutterBoost 和 Thrio 為代表的單引擎浏覽器方案。
即把 Activity/ViewController 作為承載 Dart 頁面的浏覽器,在頁面切換時對單引擎進行 detach/attach,同時通知 Dart 層頁面切換,來實作 Engine 的複用。
Thrio與Boost差別在于:在Flutter頁面連續跳轉時,隻使用同一個 Activity/ViewController 承載。
由于隻持有了一個 Engine 單例,僅建立一份 isolate,Dart 層是通信和資源共享的,記憶體損耗也得以有顯著的降低。下圖所示是 Android 側單引擎下打開 5 個頁面記憶體增量對比:
可以看出 Android 側跳轉 Flutter 頁面的記憶體消耗已降低到接近原生。
但在 iOS 側,我們發現了打開新的承載 Flutter 頁面的 ViewController 仍會有 10M 左右的記憶體增量。
對此,Boost 的建議是同一時間下,人為控制 Flutter 頁面在 5 個以内,來避免記憶體過大的問題。哈啰單車的 Thrio 就是在 Boost 基礎上提出的優化方案,即在 Flutter->Flutter 的情景下,避免建立 ViewController,而是在Dart 層進行路由切換。但可以看出,該方案在增加雙端路由複雜度的同時,并沒有解決 Native->Flutter 的記憶體大幅增長。
這兩個方案都沒有真正解決記憶體的異常問題。
此外,在 Android 側,單引擎實作依賴于修改官方的 io.flutter 包。但我們并不清楚外部方案具體做了哪些底層修改,這給項目帶來風險。
在預研單引擎路由方案的時候,我們發現大多是直接拉取官方 io.flutter 包來進行底層改造。這對于使用者就像一個黑盒子,并不知道什麼地方做了什麼修改,對出現的 bug 更無法排查。并且這種耦合依賴 io.flutter 包的方式,也會對 Flutter SDK 更新帶來困難。
事實上,Github上 Boost 目前仍還有 160+ 的 issue 未解決,支援 Flutter SDK 版本的更新速度也不盡人意。是以我們打算自己踩一遍坑,尋求對官方代碼最小的修改,并使修改可見,來保證路由的穩定性,問題可排查性。
三、實作方式及痛點解決
在明确業内方案和面臨的痛點之後。我們聚焦于痛點的解決,推出了一套更優的混合棧路由方案 TRouter。
整體架構上,仍采用單引擎浏覽器方案。用 Activity/ViewController 承載 Dart 頁面的方式,把路由收歸原生,維持唯一的單引擎執行個體。
在頁面生命周期變更時對單 Engine 進行 attach/detach,同時傳遞 url、params 通知 Dart 層進行頁面切換。
值得注意的是,Dart 和 Native 層是職責分離的。
Dart 層隻負責接收原生端生命周期資訊,并得到頁面的 url 與 params,來進行 Flutter 的頁面渲染。
而 Native 層統一接管了頁面的跳轉和 url 解析,在跳轉 Flutter 頁面時,感覺上仍是打開一個 Activity/ViewController。
這樣,混合棧路由與原生路由的體驗并無差別,可以輕松接入原有項目的路由邏輯。
iOS 端即使實作了單引擎複用,但仍會在建立 Flutter ViewContoller 時有 10M 的記憶體異常增長。這就需要我們從底層來了解 Flutter 的渲染過程。
Flutter 渲染是由 Vsync 信号觸發 UI 重新整理,再在 Dart 層進行 Widget 布局、繪制生成 LayerTree。然後渲染線程進行栅格化及合成,最終把渲染的結果設定到 layer.contents 裡進行螢幕顯示。
定位到最後一步,由于渲染出的結果是位圖,記憶體占用比較大。當每次建立一個 FlutterViewController 時會有一個渲染後的位圖與之對應,會導緻每次新增一個頁面時會有一個較大的記憶體增長。
由此,可以确定記憶體的優化思路。即在頁面完全退出(viewDidDisappear)後,将 FlutterView.layer.contents 對象設定為 nil,回收目前頁面的位圖對象,在頁面即将展示(viewWillAppear)時重新渲染出新頁面。
這樣,在保證路由體驗的同時,避免了 iOS 側的記憶體異常。優化效果如下:
在連續打開 Flutter 頁面裡,記憶體也能平穩保持在正常水準。
Android 端 io.flutter 包的代碼,并沒有支援 Engine 的複用,是以會涉及到官方代碼的修改。
從項目風險考慮,我們在方案設計時有三個核心的訴求:
對官方代碼做最小的修改,避免有引入額外 bug 的風險;
對代碼的變更是明确清晰的,在遇到線上問題時,可以第一時間進行分析和排查;
可複用的訴求,易于 Flutter SDK 的疊代更新。
在了解底層代碼和不斷踩坑後,我們明确了 Engine 可以在外部初始化,并且對引擎切換的代碼修改是有限的,這是實作訴求的前提。最終我們把底層改造邏輯分離,集合到 FlutterFixPlugin 插件裡。
使用操縱位元組碼 Hook 的方式,把每一個問題點的修改封裝為一個政策,一個政策包含多個代碼改動片段,進而達到改動可見,與 SDK 版本适配的目的。
FlutterFixPlugin 插件對代碼的改造是非侵入式的,僅需要在 .gradle 檔案中進行依賴。
插件支援根據不同 Flutter 版本進行政策的增減與變更,工程結構如下:
方案優勢展現在如下兩方面:
(1)修改可見和問題覆寫
可以清晰明确底層代碼的修改内容,并細分到了每條執行語句。到目前為止,除開對 Engine 複用的必要修改外,插件已經對跳轉時頁面跳屏,頁面白屏,跳轉時動畫不延續的等問題以及一些官方 issue 進行了适配修改。
(2)多版本的支援
得益于對 io.flutter 包非侵入式修改,我們驗證了 Flutter SDK v1.17、v1.20、v1.22,v2.0 等版本上,都可以良好運作。
最後,對方案進行一次對比總結:
總結來看,TRouter 混合棧的路由優勢在于:
路由方式簡單,Dart 層資源共享,有更優的記憶體性能表現;
項目風險可控,底層代碼修改是可見的,Flutter SDK 版本适配更易行。
四、下一步做的事情
3月4日,Google 釋出 Flutter v2.0 穩定版,除了對 Web 更高品質的支援與引入空安全外。其中一個重要更新就是提供了多引擎下使用 FlutterEngineGroup 來建立新的 Engine,官方宣稱記憶體損耗僅占 180K。
其本質是使 Engine 可以共享 GPU 上下文、font metrics 和 isolate group snapshot,進而實作了更快的初始速度和更低的記憶體占用。
雖然目前看起來仍未穩定,也有比較多的問題尚未解決,比如 Dart 層還是是資源隔離的,一套圖檔資源可能被加載多次。但這讓我們看到了混合棧路由回歸官方方案的可能。
下一步我們将繼續探究 v2.0 的特性,用 v2.0 對多引擎的加持來實作 View 級别的支援。
結語
TRouter 是心悅項目解決 Flutter 路由痛點後的産物。在最開始的接入時,我們想法是能引入穩定可靠的方案,但官方對混合棧的支援偏向薄弱。
而從流傳的文章來看,業内的方案跟随 Flutter 版本的更新也不斷的在調整。最後應該會趨近于同一套被廣泛認可的方式。
從這一角度上講,所有技術都是不斷演進的,最終導向的是更高的性能表現,與最佳的項目實踐。