Flutter 混合棧路由實踐與優化

導語 | 在 Flutter 和原生混合開發的場景裏,路由是繞不開的一個話題。但業內的方案中仍存在內存異常,對官方底層的修改也需要不斷踩坑。我們在項目實踐中,抽離出了一套混合棧路由框架。對內存進行了進一步優化,清晰了對底層代碼的修改,同時更易於 Flutter SDK 升級。文章作者:李鵬飛,騰訊 IEG 前端研發工程師。

一、背景及綜述

Flutter 在目前跨平臺方案中有更好的平臺一致性以及更優的體驗。但對於本身已有成熟的業務代碼的項目來說,更多的是採用混合棧的方式,在不變更原有 App 業務的基礎上,將 Flutter 能力擴展爲子模塊進行接入和開發。這樣並不影響原有的業務和原生能力,又可以結合業務需求進行技術選擇。

混合棧涉及到 Flutter 頁面與原生頁面的跳轉。而官方的路由方案,在多引擎下有着通信隔離,資源不共享,極大的內存損耗等缺陷。

業內採用較廣泛是單引擎複用方案,但這仍有不少痛點,體現在兩個方面:

爲了解決這些問題,心悅抽離出了一套混合棧路由框架 TRouter。

本文的目標是闡述 Flutter 實踐混合棧路由中遇到的痛點,以及 TRouter 是如何去解決的。最後會對目前的方案進行橫向對比,講述下一步的計劃。

二、混合集成面臨的問題

項目最終明確選用了單引擎複用的方案,業內未解決而我們面臨的痛點有兩個:

  1. iOS 側的內存增長異常;

  2. Android 側 底層修改不透明給項目帶來風險。

在介紹 TRouter 之前,本節會討論問題的成因,以及爲什麼說業內方案存在缺陷。

官方並沒有很好解決混合棧路由所遇到的問題

Flutter 的技術鏈路是建立在 C++ 編寫的 Engine 和 Dart 編寫的 Framework 層組成。主要構成如下圖所示:

可以明確的是:

在混合棧路由上,雖然 Dart 層本身有提供 navigator 等路由方式,但當我們把 Flutter 集成爲原生的模塊或能力時,一定會出現 Native -> Flutter -> Native -> Flutter… 這種混合頁面跳轉情況。

這樣存在問題是:如何保存 Flutter 頁面的狀態,並且在頁面回退或跳轉時,在正確的時機恢復或切換 Flutter 的渲染內容

1. 多引擎方案

Google 官方提供的是 keep it simple 的方案,即間隔的 Flutter 頁面單獨使用一個新的 Engine 來單獨維持一份視圖渲染,跳轉時就無需考慮 Dart 層頁面切換。

這種方案弊端很多,首先是 Engine 的線性增多,帶來內存的極大損耗。如下圖所示,Android 端多引擎下打開 5 個頁面內存增量對比:

其次由於 isolate 隔離,Dart 側圖片緩存等資源也無法共享,所有通信都需要經過原生,使通信有極高的複雜度

所以多引擎不能滿足項目的性能要求。

2. 單引擎瀏覽器方案

由於多引擎的缺陷,業內的做法一般是對 isolute 或 Engine 進行復用來解決。影響力較大的是以 FlutterBoost 和 Thrio 爲代表的單引擎瀏覽器方案。

即把 Activity/ViewController 作爲承載 Dart 頁面的瀏覽器,在頁面切換時對單引擎進行 detach/attach,同時通知 Dart 層頁面切換,來實現 Engine 的複用。

Thrio 與 Boost 區別在於:在 Flutter 頁面連續跳轉時,只使用同一個 Activity/ViewController 承載。

可以看出 Android 側跳轉 Flutter 頁面的內存消耗已降低到接近原生。

痛點一:iOS 側內存增長異常

在 iOS 側,我們發現了打開新的承載 Flutter 頁面的 ViewController 仍會有 10M 左右的內存增量

對此,Boost 的建議是同一時間下,人爲控制 Flutter 頁面在 5 個以內,來避免內存過大的問題。哈囉單車的 Thrio 就是在 Boost 基礎上提出的優化方案,即在 Flutter->Flutter 的情景下,避免創建 ViewController,而是在 Dart 層進行路由切換。但可以看出,該方案在增加雙端路由複雜度的同時,並沒有解決 Native->Flutter 的內存大幅增長。

這兩個方案都沒有真正解決內存的異常問題。

痛點二:Android 側,底層不可見的修改給項目帶來風險

此外,在 Android 側,單引擎實現依賴於修改官方的 io.flutter 包。但我們並不清楚外部方案具體做了哪些底層修改,這給項目帶來風險

在預研單引擎路由方案的時候,我們發現大多是直接拉取官方 io.flutter 包來進行底層改造。這對於使用者就像一個黑盒子,並不知道什麼地方做了什麼修改,對出現的 bug 更無法排查。並且這種耦合依賴 io.flutter 包的方式,也會對 Flutter SDK 升級帶來困難。

事實上,Github 上 Boost 目前仍還有 160+ 的 issue 未解決,支持 Flutter SDK 版本的更新速度也不盡人意。所以我們打算自己踩一遍坑,尋求對官方代碼最小的修改,並使修改可見,來保證路由的穩定性,問題可排查性。

三、實現方式及痛點解決

在明確業內方案和麪臨的痛點之後。我們聚焦於痛點的解決,推出了一套更優的混合棧路由方案 TRouter。

1. 整體框架

整體框架上,仍採用單引擎瀏覽器方案。用 Activity/ViewController 承載 Dart 頁面的方式,把路由收歸原生,維持唯一的單引擎實例。

在頁面生命週期變更時對單 Engine 進行 attach/detach,同時傳遞 url、params 通知 Dart 層進行頁面切換。

值得注意的是,Dart 和 Native 層是職責分離的。

Dart 層只負責接收原生端生命週期信息,並得到頁面的 url 與 params,來進行 Flutter 的頁面渲染。

而 Native 層統一接管了頁面的跳轉和 url 解析,在跳轉 Flutter 頁面時,感知上仍是打開一個 Activity/ViewController。

這樣,混合棧路由與原生路由的體驗並無區別,可以輕鬆接入原有項目的路由邏輯。

2. 內存優化

iOS 端即使實現了單引擎複用,但仍會在創建 Flutter ViewContoller 時有 10M 的內存異常增長。這就需要我們從底層來理解 Flutter 的渲染過程。

Flutter 渲染是由 Vsync 信號觸發 UI 刷新,再在 Dart 層進行 Widget 佈局、繪製生成 LayerTree。然後渲染線程進行柵格化及合成,最終把渲染的結果設置到 layer.contents 裏進行屏幕顯示。

定位到最後一步,由於渲染出的結果是位圖,內存佔用比較大。當每次新建一個 FlutterViewController 時會有一個渲染後的位圖與之對應,會導致每次新增一個頁面時會有一個較大的內存增長

由此,可以確定內存的優化思路。即在頁面完全退出(viewDidDisappear)後,將 FlutterView.layer.contents 對象****設置爲 nil,回收當前頁面的位圖對象,在頁面即將展示(viewWillAppear)時重新渲染出新頁面

這樣,在保證路由體驗的同時,避免了 iOS 側的內存異常。優化效果如下:

在連續打開 Flutter 頁面裏,內存也能平穩保持在正常水平。

3. 底層改造

Android 端 io.flutter 包的代碼,並沒有支持 Engine 的複用,所以會涉及到官方代碼的修改。

從項目風險考慮,我們在方案設計時有三個核心的訴求:

在理解底層代碼和不斷踩坑後,我們明確了 Engine 可以在外部初始化,並且對引擎切換的代碼修改是有限的,這是實現訴求的前提。最終我們把底層改造邏輯分離,集合到 FlutterFixPlugin 插件裏。

使用操縱字節碼 Hook 的方式,把每一個問題點的修改封裝爲一個策略,一個策略包含多個代碼改動片段,從而達到改動可見,與 SDK 版本適配的目的

FlutterFixPlugin 插件對代碼的改造是非侵入式的,僅需要在 .gradle 文件中進行依賴。

1apply plugin: 'com.tencent.fixflutter'
2

插件支持根據不同 Flutter 版本進行策略的增減與變更,工程結構如下

方案優勢體現在如下兩方面:

(1)修改可見和問題覆蓋

可以清晰明確底層代碼的修改內容,並細分到了每條執行語句。到目前爲止,除開對 Engine 複用的必要修改外,插件已經對跳轉時頁面跳屏,頁面白屏,跳轉時動畫不延續的等問題以及一些官方 issue 進行了適配修改。

(2)多版本的支持

得益於對 io.flutter 包非侵入式修改,我們驗證了 Flutter SDK v1.17、v1.20、v1.22,v2.0 等版本上,都可以良好運行。

4. 方案對比

最後,對方案進行一次對比總結:

總結來看,TRouter 混合棧的路由優勢在於:

四、下一步做的事情

Flutter v2.0 升級與 View 級別的支持

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 版本的更新也不斷的在調整。最後應該會趨近於同一套被廣泛認可的方式。

從這一角度上講,所有技術都是不斷演進的,最終導向的是更高的性能表現,與最佳的項目實踐。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/lTrbLHMHn3qXFd-3IUoU8Q