已開源|碼上用它開始Flutter混合開發——FlutterBoost

閒魚技術發表於2019-03-07
開源地址:

為什麼要混合方案

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

閒魚在實踐中沉澱出一套自己的混合技術方案。在此過程中,我們跟Google Flutter團隊進行著密切的溝通,聽取了官方的一些建議,同時也針對我們業務具體情況進行方案的選型以及具體的實現。

官方提出的混合方案

1  基本原理

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。

2   官方建議

引擎深度共享

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

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

多引擎模式

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

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

Flutter Page1 -> Flutter Page2 -> Native Page1 -> Flutter Page3

我們只需要在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之間的通訊,多引擎例項也讓這件事情更加複雜。

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

現狀及思考

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

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

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

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

已開源|碼上用它開始Flutter混合開發——FlutterBoost

具體細節,大家可以參考老方案開源專案地址:

stackmanager

新一代混合技術方案 FlutterBoost

1   重構計劃

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

  • 可複用通用型混合方案

  • 支援更加複雜的混合模式,比如支援主頁Tab這種情況

  • 無侵入性方案:不再依賴修改Flutter的方案

  • 支援通用頁面生命週期

  • 統一明確的設計概念

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

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

2    主要概念

已開源|碼上用它開始Flutter混合開發——FlutterBoost

3   Native層概念

  • Container:Native容器,平臺Controller,Activity,ViewController

  • Container Manager:容器的管理者

  • Adaptor:Flutter是適配層

  • Messaging:基於Channel的訊息通訊

4   Dart層概念

  • Container:Flutter用來容納Widget的容器,具體實現為Navigator的派生類-

  • Container Manager:Flutter容器的管理,提供show,remove等Api

  • Coordinator: 協調器,接受Messaging訊息,負責呼叫Container Manager的狀態管理。

  • Messaging:基於Channel的訊息通訊

5    關於頁面的理解

在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至關重要。

6   與老方案主要差別

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

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

7   多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容器事件去進行驅動。

總結

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

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

擴充套件補充


1  效能相關

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

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

2   Release1.0的支援

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

3  接入

只要是整合了Flutter的專案都可以用官方依賴的方式非常方便的以外掛形式引入FlutterBoost,只需要對工程進行少量程式碼接入即可完成接入。 詳細接入文件,請參閱GitHub主頁官方專案文件。
4  現已開源

目前,新一代混合棧已經在閒魚全面應用。我們非常樂意將沉澱的技術回饋給社群。歡迎大家一起貢獻,一起交流,攜手共建Flutter社群。

專案開源地址:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69900359/viewspace-2637821/,如需轉載,請註明出處,否則將追究法律責任。

相關文章