阿里妹導讀:具有一定規模的 App 通常有一套成熟通用的基礎庫,尤其是阿里系 App,一般需要依賴很多體系內的基礎庫。那麼使用 Flutter 重新從頭開發 App 的成本和風險都較高。所以在 Native App 進行漸進式遷移是 Flutter 技術在現有 Native App 進行應用的穩健型方式。

今天我們來看看,閑魚團隊如何在這個實踐過程中沉澱出一套獨具特色的混合技術方案。

現狀及思考

閑魚目前採用的混合方案是共享同一個引擎的方案。這個方案基於這樣一個事實:任何時候我們最多隻能看到一個頁面,當然有些特定的場景你可以看到多個 ViewController ,但是這些特殊場景我們這裡不討論。

我們可以這樣簡單去理解這個方案:我們把共享的 Flutter View 當成一個畫布,然後用一個 Native 的容器作為邏輯的頁面。每次在打開一個容器的時候我們通過通信機制通知 Flutter View 繪製成當前的邏輯頁面,然後將 Flutter View 放到當前容器裡面。

這個方案無法支持同時存在多個平級邏輯頁面的情況,因為你在頁面切換的時候必須從棧頂去操作,無法再保持狀態的同時進行平級切換。舉個例子:有兩個頁面A,B,當前B在棧頂。切換到A需要把B從棧頂 Pop 出去,此時B的狀態丟失,如果想切回B,我們只能重新打開B之前頁面的狀態無法維持住。

如在 pop 的過程當中,可能會把 Flutter 官方的 Dialog 進行誤殺。而且基於棧的操作我們依賴對 Flutter 框架的一個屬性修改,這讓這個方案具有了侵入性的特點。

新一代混合技術方案 FlutterBoost

重構計劃

在閑魚推進 Flutter 化過程當中,更加複雜的頁面場景逐漸暴露了老方案的侷限性和一些問題。所以我們啟動了代號 FlutterBoost(向C++ Boost庫致敬)的新混合技術方案。這次新的混合方案我們的主要目標有:

  • 可復用通用型混合方案
  • 支持更加複雜的混合模式,比如支持主頁Tab這種情況
  • 無侵入性方案:不再依賴修改 Flutter 的方案
  • 支持通用頁面生命週期
  • 統一明確的設計概念

跟老方案類似,新的方案還是採用共享引擎的模式實現。主要思路是由 Native 容器 Container 通過消息驅動 Flutter 頁面容器 Container,從而達到 Native Container與 Flutter Container 的同步目的。我們希望做到 Flutter 渲染的內容是由 Naitve 容器去驅動的。

簡單的理解,我們想做到把 Flutter 容器做成瀏覽器的感覺。填寫一個頁面地址,然後由容器去管理頁面的繪製。在 Native 側我們只需要關心如果初始化容器,然後設置容器對應的頁面標誌即可。

主要概念

Native 層概念
  • Container:Native 容器,平臺 Controller,Activity,ViewController
  • Container Manager:容器的管理者
  • Adaptor:Flutter 是適配層
  • Messaging:基於 Channel 的消息通信

Dart 層概念

  • Container:Flutter 用來容納 Widget 的容器,具體實現為 Navigator 的派生類
  • Container Manager:Flutter 容器的管理,提供 show,remove 等 Api
  • Coordinator: 協調器,接受 Messaging 消息,負責調用 Container Manager 的狀態管理。
  • Messaging:基於 Channel 的消息通信

關於頁面的理解

在 Native 和 Flutter 表示頁面的對象和概念是不一致的。在 Native,我們對於頁面的概念一般是 ViewController,Activity 。而對於 Flutter 我們對於頁面的概念是 Widget 。我們希望可統一頁面的概念,或者說弱化抽象掉 Flutter 本身的 Widget 對應的頁面概念。換句話說,當一個 Native 的頁面容器存在的時候, FlutteBoost 保證一定會有一個 Widget 作為容器的內容。所以我們在理解和進行路由操作的時候都應該以 Native 的容器為準, Flutter Widget 依賴於 Native 頁面容器的狀態。

那麼在 FlutterBoost 的概念裏說到頁面的時候,我們指的是 Native 容器和它所附屬的 Widget 。所有頁面路由操作,打開或者關閉頁面,實際上都是對 Native 頁面容器的直接操作。無論路由請求來自何方,最終都會轉發給 Native 去實現路由操作。這也是接入 FlutterBoost 的時候需要實現 Platform 協議的原因。

另一方面,我們無法控制業務代碼通過 Flutter 本身的 Navigator 去 push 新的 Widget 。對於業務不通過 FlutterBoost 而直接使用 Navigator 操作 Widget 的情況,包括 Dialog 這種非全屏 Widget,我們建議是業務自己負責管理其狀態。這種類型 Widget 不屬於 FlutterBoost 所定義的頁面概念。

理解這裡的頁面概念,對於理解和使用 FlutterBoost 至關重要。

與老方案主要差別

前面我們提到老方案在 Dart 層維護單個 Navigator 棧結構用於 Widget 的切換。而新的方案則是在 Dart 側引入了 Container 的概念,不再用棧的結構去維護現有的頁面,而是通過扁平化 key-value 映射的形式去維護當前所有的頁面,每個頁面擁有一個唯一的 id 。這種結構很自然的支持了頁面的查找和切換,不再受制於棧頂操作的問題,之前的一些由於 pop 導致的問題迎刃而解。也不需要依賴修改 Flutter 源碼的形式去進行頁面棧操作,去掉了實現的侵入性。

實際上我們引入的 Container 就是 Navigator 的,也就是說一個 Native 的容器對應了一個 Navigator 。那這是如何做到的呢?

多 Navigator 的實現

Flutter 在底層提供了讓你自定義 Navigator 的介面,我們自己實現了一個管理多個 Navigator 的對象。當前最多隻會有一個可見的 Flutter Navigator ,這個Navigator 所包含的頁面也就是我們當前可見容器所對應的頁面。

Native 容器與 Flutter 容器( Navigator )是一一對應的,生命週期也是同步的。當一個 Native 容器被創建的時候, Flutter 的一個容器也被創建,它們通過相同的 id 關聯起來。當 Native 的容器被銷毀的時候, Flutter 的容器也被銷毀。 Flutter 容器的狀態是跟隨 Native 容器,這也就是我們說的 Native 驅動。由 Manager 統一管理切換當前在屏幕上展示的容器。

我們用一個簡單的例子描述一個新頁面創建的過程:

  1. 創建 Native 容器( iOS ViewController,Android Activity or Fragment )。
  2. Native 容器通過消息機制通知 Flutter Coordinator 新的容器被創建。
  3. Flutter Container Manager 進而得到通知,負責創建出對應的 Flutter 容器,並且在其中裝載對應的 Widget 頁面。
  4. 當 Native 容器展示到屏幕上時,容器發消息給 Flutter Coordinator 通知要展示頁面的 id 。
  5. Flutter Container Manager 找到對應 id 的 Flutter Container 並將其設置為前臺可見容器。

這就是一個新頁面創建的主要邏輯,銷毀和進入後臺等操作也類似有 Native 容器事件去進行驅動。

官方提出的混合方案

基本原理

Flutter 技術鏈主要由 C++實現的 Flutter Engine 和 Dart 實現的 Framework 組成(其配套的編譯和構建工具我們這裡不參與討論)。Flutter Engine 負責線程管理,Dart VM 狀態管理和 Dart 代碼載入等工作。而 Dart 代碼所實現的 Framework 則是業務接觸到的主要 API,諸如 Widget 等概念就是在 Dart 層面 Framework 內容。

一個進程裡面最多隻會初始化一個 Dart VM。然而一個進程可以有多個 Flutter Engine,多個 Engine 實例共享同一個 Dart VM。

我們來看具體實現,在 iOS 上面每初始化一個 FlutterViewController 就會有一個引擎隨之初始化,也就意味著會有新的線程(理論上線程可以復用)去跑 Dart 代碼。Android 類似的 Activity 也會有類似的效果。如果你啟動多個引擎實例,注意此時Dart VM 依然是共享的,只是不同 Engine 實例載入的代碼跑在各自獨立的 Isolate。

官方建議

引擎深度共享

在混合方案方面,我們跟 Google 討論了可能的一些方案。Flutter 官方給出的建議是從長期來看,我們應該支持在同一個引擎支持多窗口繪製的能力,至少在邏輯上做到 FlutterViewController 是共享同一個引擎的資源的。換句話說,我們希望所有繪製窗口共享同一個主 Isolate。

但官方給出的長期建議目前來說沒有很好的支持。

多引擎模式

我們在混合方案中解決的主要問題是如何去處理交替出現的 Flutter 和 Native 頁面。Google 工程師給出了一個 Keep It Simple 的方案:對於連續的 Flutter 頁面(Widget)只需要在當前 FlutterViewController 打開即可,對於間隔的 Flutter 頁面我們初始化新的引擎。

例如,我們進行下面一組導航操作:

我們只需要在 Flutter Page1 和 Flutter Page3 創建不同的 Flutter 實例即可。

這個方案的好處就是簡單易懂,邏輯清晰,但是也有潛在的問題。如果一個 Native頁面一個 Flutter 頁面一直交替進行的話,Flutter Engine 的數量會線性增加,而Flutter Engine 本身是一個比較重的對象。

多引擎模式的問題

  • 冗餘的資源問題:多引擎模式下每個引擎之間的 Isolate 是相互獨立的。在邏輯上這並沒有什麼壞處,但是引擎底層其實是維護了圖片緩存等比較消耗內存的對象。想像一下,每個引擎都維護自己一份圖片緩存,內存壓力將會非常大。
  • 插件註冊的問題:插件依賴 Messenger 去傳遞消息,而目前 Messenger 是由 FlutterViewController(Activity) 去實現的。如果你有多個 FlutterViewController ,插件的註冊和通信將會變得混亂難以維護,消息的傳遞的源頭和目標也變得不可控。
  • Flutter Widget 和 Native 的頁面差異化問題: Flutter 的頁面是 Widget, Native 的頁面是 VC 。邏輯上來說我們希望消除 Flutter 頁面與 Naitve 頁面的差異,否則在進行頁面埋點和其它一些統一操作的時候都會遇到額外的複雜度。
  • 增加頁面之間通信的複雜度:如果所有 Dart 代碼都運行在同一個引擎實例,它們共享一個 Isolate ,可以用統一的編程框架進行 Widget 之間的通信,多引擎實例也讓這件事情更加複雜。

因此,綜合多方面考慮,我們沒有採用多引擎混合方案。

總結

目前 FlutterBoost 已經在生產環境支撐著在閑魚客戶端中所有的基於 Flutter 開發業務,為更加負複雜的混合場景提供了支持,穩定為億級用戶提供服務。

我們在項目啟動之初就希望 FlutterBoost 能夠解決 Native App 混合模式接入 Flutter 這個通用問題。所以我們把它做成了一個可復用的 Flutter 插件,希望吸引更多感興趣的朋友參與到 Flutter 社區的建設。在有限篇幅中,我們分享了閑魚在 Flutter 混合技術方案中積累的經驗和代碼。歡迎興趣的同學能夠積極與我們一起交流學習。

擴展補充

  • 性能相關

在兩個 Flutter 頁面進行切換的時候,因為我們只有一個 Flutter View 所以需要對上一個頁面進行截圖保存,如果 Flutter 頁面多截圖會佔用大量內存。這裡我們採用文件內存二級緩存策略,在內存中最多隻保存2-3個截圖,其餘的寫入文件按需載入。這樣我們可以在保證用戶體驗的同時在內存方面也保持一個較為穩定的水平。

頁面渲染性能方面, Flutter 的 AOT 優勢展露無遺。在頁面快速切換的時候, Flutter 能夠很靈敏的相應頁面的切換,在邏輯上創造出一種 Flutter 多個頁面的感覺。

  • Release1.0的支持

項目開始的時候我們基於閑魚目前使用的 Flutter 版本進行開發,而後進行了 Release 1.0 兼容升級測試目前沒有發現問題。

  • 接入

只要是集成了 Flutter 的項目都可以用官方依賴的方式非常方便的以插件形式引入 FlutterBoost ,只需要對工程進行少量代碼接入即可完成接入。 詳細接入文檔,請參閱GitHub主頁官方項目文檔。

本文作者: 福居

原文鏈接

更多技術乾貨敬請關注云棲社區知乎機構號:阿里云云棲社區 - 知乎

本文來自雲棲社區合作夥伴「阿里技術」,如需轉載請聯繫原作者。


推薦閱讀:
相關文章