前景介紹
Flutter 是谷歌開發的一款可以跨平臺開發的 UI框架,它的原理接近於遊戲引擎,目的在於統一Android/iOS 兩端開發,Flutter頁面有自己的棧,正常情況下,如果一個App完全由 Flutter構成,那麼只需要一個 FlutterView
即可。
上述方案只適用於一些新構建的App,對於一些已有的App,是不可能用 flutter來重構的,成本太大,週期太長,所以這裡需要實現一套 Native 頁面棧和Flutter頁面棧的管理方案,即混合棧
。
關於混合棧的管理,閒魚出過一篇文章,但是對於它的相容性問題和截圖問題,沒有采用,不過作者對閒魚的混合棧原始碼做了參考,這裡感謝閒魚的原始碼分享。本文是在 android 的基礎上講解實現方式的,iOS 目前使用的還是截圖方案,其餘的原理差不多
方案探索
方案一
不進行任何處理,直接使用 FlutterActivity
來開啟頁面:此方法最接近原生,交替開啟幾個頁面後會呈現出以下頁面結構
每個 FlutterActivity
都有自己的 flutter 棧,此時如果使用者點選了返回按鈕的時候頁面退出的呈現形式是正常的,但是如果App用了側滑返回的話工作就會不正常。
側滑結束
FlutterActivit2
會一下子結束三個 flutter widget 頁面
除了上述問題外,還存在一個嚴重的問題:FlutterView1
和 FlutterView2
屬於兩個 isolate,兩者相當於兩個 flutter engine 例項,在記憶體上隔離的,不共享
總結: 該方案有以下缺點
- 不相容現有的側滑返回
- 頁面的生命週期埋點需要在 dart 層重新實現一套
- 不同
FlutterView
之間無法共享記憶體(圖片快取,全域性單例都不可公用) - 資源佔用大:每次啟動一個
FlutterActivity
都會啟動一個新的 Flutter 例項 - 介面切換體驗有差別:Native 頁面之間的切換動畫和 flutter 頁面之間的切換動畫有差別
方案二
全域性共用一個 FlutterView
,每個 flutter 頁面都有一個對應的 native 頁面:此方案可以解決方案一中的記憶體共享浪費問題
此方案的大致原理如下:
關鍵步驟是 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
此方案的結構圖如下:
和方案二不同的是,方案三中 FlutterView
和 FlutterActivity
繫結在一起了,這樣可以避免 FlutterView
單例化造成的 context
洩漏。
而且相比於方案二,要實現此方案只需要滿足一條規則即可:
FlutterNativeView
從 FlutterActivity1 detach 然後 attach 到 FlutterActivity2 的之前,必須保證新的 flutter page 已經 push 到 flutter 的棧中,否則 FlutterActivity2 顯示的還是 FlutterActivity1 中顯示的介面
你會發現,這裡不需要 FlutterNativeView
在 detach 的時候構造一份當前頁面的快照然後佔位顯示.
因為在頁面切換的時候 FlutterView
並沒有從 FlutterActivity
中移除,當 FlutterNativeView
從 FlutterView
detach 的時候,FlutterView
顯示的內容就不會再更新了,相當於 Android 上的 onPreDraw
函式返回 false
, 所以這裡沒必要截圖儲存快照。
實現
經過上述方案的探索,決定在 android 上使用第三套方案
iOS 因為有側滑返回,無法避免截圖,因為在側滑的時候,頁面不一定結束。所以我這裡拋棄了 android 上側滑返回(本來 android 的側滑返回就很奇怪,不支援合理)
實現關鍵點:
- 整個佈局為多
FlutterView
單FlutterNativeView
例項 - 每一個 flutter 頁面對應一個 native 的 activity,並通過一個 id 關聯,做到棧同步
- Flutter 和 Native 基於 url 的方式開管理頁面
- 禁用 flutter 自帶的頁面切換動畫,使用 native 自帶的動畫來實現
- 使用一個空白的 widget 作為 flutter 頁面的棧底
- 當開啟新頁面或者退出頁面的時候,必須先讓
FlutterNativeView
和FlutterView
脫離,才可以在 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 才會有返回,順著這個思路去實現很簡單。
只要 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 的建立狀態,保證 FlutterActivity
和 FlutterView
的生命週期同步到 FlutterNativeView
總結
總的來說,我們微店基於上述理論實現了一個混合棧外掛,沒有反射 flutter sdk,沒有記憶體洩漏,不需要截圖,支援頁面間的資料傳遞(原始碼後續會開放),看似簡單實際實現過程中還是遇到過很多小問題的,比如頁面白屏,返回鍵無效之類的,這些都是 native 棧和 flutter 棧不同步導致的。
後續計劃
後續我們微店混合棧的問題繼續跟進的問題如下:
- 首次開啟白屏時間長
- 不支援 Hero 動畫
- iOS 無法避免截圖方案
- 無法和
Navigator.of(context).pop()
結合
其中1.
的話目前沒有什麼好的思路,但是2.
、3.
、4.
點 已經有了想法,待實現驗證,敬請期待。
作者簡介
qigengxin,@WeiDian,2016年加入微店,目前主要負責微店App的基礎支撐開發工作。