微店的Flutter混合棧管理技術實踐

前景介紹

Flutter 是谷歌開發的一款能夠跨平臺開發的 UI框架,它的原理接近於遊戲引擎,目的在於統一Android/iOS 兩端開發,Flutter頁面有本身的棧,正常狀況下,若是一個App徹底由 Flutter構成,那麼只須要一個 FlutterView 便可。android

上述方案只適用於一些新構建的App,對於一些已有的App,是不可能用 flutter來重構的,成本太大,週期太長,因此這裏須要實現一套 Native 頁面棧和Flutter頁面棧的管理方案,即混合棧git

關於混合棧的管理,閒魚出過一篇文章,可是對於它的兼容性問題和截圖問題,沒有采用,不過做者對閒魚的混合棧源碼作了參考,這裏感謝閒魚的源碼分享。本文是在 android 的基礎上講解實現方式的,iOS 目前使用的仍是截圖方案,其他的原理差很少github

方案探索

方案一

不進行任何處理,直接使用 FlutterActivity 來打開頁面:此方法最接近原生,交替打開幾個頁面後會呈現出如下頁面結構 緩存

每一個 FlutterActivity 都有本身的 flutter 棧,此時若是用戶點擊了返回按鈕的時候頁面退出的呈現形式是正常的,可是若是App用了側滑返回的話工做就會不正常。框架

側滑結束 FlutterActivit2 會一會兒結束三個 flutter widget 頁面async

除了上述問題外,還存在一個嚴重的問題:FlutterView1FlutterView2 屬於兩個 isolate,二者至關於兩個 flutter engine 實例,在內存上隔離的,不共享函數

總結: 該方案有如下缺點佈局

  • 不兼容現有的側滑返回
  • 頁面的生命週期埋點須要在 dart 層從新實現一套
  • 不一樣 FlutterView 之間沒法共享內存(圖片緩存,全局單例都不可公用)
  • 資源佔用大:每次啓動一個 FlutterActivity 都會啓動一個新的 Flutter 實例
  • 界面切換體驗有差異:Native 頁面之間的切換動畫和 flutter 頁面之間的切換動畫有差異

方案二

全局共用一個 FlutterView,每一個 flutter 頁面都有一個對應的 native 頁面:此方案能夠解決方案一中的內存共享浪費問題post

此方案的大體原理以下: 測試

-w884

關鍵步驟是 2 這個操做,當要打開一個新的 flutter 頁面時,native 會啓動一個新的 FlutterActivity,而後把當前 FlutterActivity1 中的 FlutterView 移除,而且添加到 FlutterActivity2 中。

退出頁面的時候也同樣,先讓 FlutterView 從 FlutterActivity2 中 remove 移走,而後 add 到 FlutterActivity1 中。

你可能會想:「切換頁面的時候,FlutterView 從 FlutterActiviy 移除了,顯示不是會變成空白了嗎?

什麼都不作,的確存在上述問題,這裏想把此方案實現,還須要考慮兩點:

  • FlutterView 從 FlutterActivity1 移除的時候,顯示的內容不會被移除
  • FlutterView 從 FlutterActivity1 移除添加到 FlutterActivity2 的以前,必須保證新的 flutter page 已經 push 到 flutter 的棧中,不然 FlutterActivity2 顯示的仍是 FlutterActivity1 中顯示的界面

這裏要實現第一點的話只能使用截圖方案,在 FlutterView 移除前先保存一份當前頁面的截圖快照,而後移除,這樣就不會出現空白的問題

方案三

全局共用一個 FlutterNativeView,每一個 flutter 頁面都有一個對應的 native 頁面:此方案和方案二想接近,最大的區別就是複用的東西變成了 FlutterNativeView

此方案的結構圖以下:

屏幕快照 2019-01-14 下午8.18.37

和方案二不一樣的是,方案三中 FlutterViewFlutterActivity 綁定在一塊兒了,這樣能夠避免 FlutterView 單例化形成的 context 泄漏。

並且相比於方案二,要實現此方案只須要知足一條規則便可:

  • FlutterNativeView 從 FlutterActivity1 detach 而後 attach 到 FlutterActivity2 的以前,必須保證新的 flutter page 已經 push 到 flutter 的棧中,不然 FlutterActivity2 顯示的仍是 FlutterActivity1 中顯示的界面

你會發現,這裏不須要 FlutterNativeView 在 detach 的時候構造一份當前頁面的快照而後佔位顯示.

由於在頁面切換的時候 FlutterView 並無從 FlutterActivity 中移除,FlutterNativeViewFlutterView detach 的時候,FlutterView 顯示的內容就不會再更新了,至關於 Android 上的 onPreDraw 函數返回 false, 因此這裏不必截圖保存快照。

實現

通過上述方案的探索,決定在 android 上使用第三套方案

iOS 由於有側滑返回,沒法避免截圖,由於在側滑的時候,頁面不必定結束。因此我這裏拋棄了 android 上側滑返回(原本 android 的側滑返回就很奇怪,不支持合理)

實現關鍵點:

-w454

  • 整個佈局爲多 FlutterViewFlutterNativeView 實例
  • 每個 flutter 頁面對應一個 native 的 activity,並經過一個 id 關聯,作到棧同步
  • Flutter 和 Native 基於 url 的方式開管理頁面
  • 禁用 flutter 自帶的頁面切換動畫,使用 native 自帶的動畫來實現
  • 使用一個空白的 widget 做爲 flutter 頁面的棧底
  • 當打開新頁面或者退出頁面的時候,必須先讓 FlutterNativeViewFlutterView 脫離,才能夠在 flutter 棧操做頁面的進退

整個頁面啓動跳轉打大體流程圖以下:

Flutter混合開發

左側是 flutter 的執行流程,右側是 android native activity 的執行流程

頁面的傳參和數據返回

上述代碼的設計尚未考慮頁面之間的數據傳遞,原生 flutter 的頁面數據傳遞是這樣的:

void jumpToSettings(BuildContext context) async {
    String result = await Navitor.of(context).pushNamed("settings");
    print("page return: $result");
}
複製代碼

因此在設計頁面數據傳遞的時候向原生的看齊,以下所示:

final result = await VDRouter.instance().openUrlFromNative(
    context: context,
    routerOption: RouterOption(
        url: "native://example", args: {"message": "Open from Flutter"}));
複製代碼

當 native 端的 MethodCallHandler 被調用時,有個參數是 result,只有給這個 result 設置告終果 (result.success(xxx)),上面的 await 纔會有返回,順着這個思路去實現很簡單。

-w722

只要 FlutterActivity 把由當前 flutter 發起打開頁面請求的 result 對象保存起來,而後調用 startActivityForResult 來啓動頁面,等頁面結束後會回調到 onActivityResult 中,此時再經過保存的 result 對象,把結果返回給 flutter 端。

傳參直接使用 intent 傳參便可。

沉浸式同步的問題

每次啓動一個新的 FlutterActivity 都須要和 flutter 端同步下當前狀態欄的沉浸式狀態,這裏經過 native 主動調用 channel 來同步

// 請求更新主題色到 native 端,這裏使用了一個測試接口,之後要注意
var preTheme = SystemChrome.latestStyle;
if (preTheme != null) {
    SystemChannels.platform.invokeMethod("SystemChrome.setSystemUIOverlayStyle", _toMap(preTheme));
}
複製代碼

FlutterNativeView 的 detach 和 attach

FlutterNativeView 的 detach 和 attach 的時候,須要注意 FlutterActivity 的生命週期和 FlutterView 中 surface 的建立狀態,保證 FlutterActivityFlutterView 的生命週期同步到 FlutterNativeView

總結

總的來講,咱們微店基於上述理論實現了一個混合棧插件,沒有反射 flutter sdk,沒有內存泄漏,不須要截圖,支持頁面間的數據傳遞(源碼後續會開放),看似簡單實際實現過程當中仍是遇到過不少小問題的,好比頁面白屏,返回鍵無效之類的,這些都是 native 棧和 flutter 棧不一樣步致使的。

後續計劃

後續咱們微店混合棧的問題繼續跟進的問題以下:

  1. 首次打開白屏時間長
  2. 不支持 Hero 動畫
  3. iOS 沒法避免截圖方案
  4. 沒法和 Navigator.of(context).pop() 結合

其中1.的話目前沒有什麼好的思路,可是2.3.4.點 已經有了想法,待實現驗證,敬請期待。

做者簡介

qigengxin,@WeiDian,2016年加入微店,目前主要負責微店App的基礎支撐開發工做。

歡迎關注微店App技術團隊官方公衆號

微店App技術團隊
相關文章
相關標籤/搜索