如何用 Flutter 實現混合開發?閒魚公開源代碼實例

阿里妹導讀:具備必定規模的 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 側咱們只須要關心若是初始化容器,而後設置容器對應的頁面標誌便可。spa

主要概念插件

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主頁官方項目文檔。



本文做者: 福居

閱讀原文

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

相關文章
相關標籤/搜索