多餘的前言
Flutter 2.0 釋出時,其中最受大家關注之一的內容就是 Add-to-App
相關的更新,因為除了熱更新之外,Flutter 最受大家詬病的就是混合開發體驗不好。
為什麼不好呢?因為 Flutter 的控制元件渲染直接脫離了原生平臺,也就是無論頁面堆疊和渲染樹都獨立於平臺執行,這固然給 Flutter 帶來了較好的跨平臺體驗,但是也造成了在和原生平臺混合時存在高成本的問題。
且不說在已有的原生專案中整合 Flutter ,就是現階段在 Flutter 中整合原生控制元件的 PlatformView 和 Hybrid Composition 體驗也是有待提升,當然“有支援”和“能用”就已經是很不錯的進展。
所以 Flutter 2.0 在千呼萬喚中釋出了 FlutterEngineGroup
用於支援官方的 Add Flutter to existing app
方案。
在此方案出現之前,類似的第三方支援有 flutter_boost
、 mix_stack
、 flutter_thrio
等等 ,它們是否好用這裡不討論,但是這些方案都要面對的問題是:
非官方的支援必然存在每個版本需要適配的問題,而按照 Flutter 目前的
issue closed
和pr merge
的速度,很可能每個季度的版本都存在較大的變動,所以如果開發者不維護或者維護不及時,那麼侵入性極強的這類框架很容易就成為專案的瓶頸。
而官方提供的 FlutterEngineGroup
方案有沒有缺陷?肯定有的,它目前看起來更像是被催生出來的狀態,各方面的問題還是有的,比如某些地方還存在不能 destroy
的問題。 (當然這個問題以及在 master
分支 merge 了)
但是官方提供的方案,就意味著這個設計得到了 Flutter 官方的保證,在未來的版本中會有相容的優勢。
FlutterEngineGroup
方案使用了多 Engine 混合模式,官方宣稱除了一個 Engine 物件之外,後續每個 Engine 物件在 Android 和 iOS 上僅佔用 180kB 。
以前的方案每多一個Engine ,可能就會多出 19MB Android 和 13MB iOS 的佔用。
從 Flutter 官方提供的例子上看,FlutterEngineGroup
的 API 十分簡單,多個 Engine 例項的內部都是獨立維護自己的內部導航堆疊,所以可以做到每個 Engine 對應一個獨立的模組。
所以使用 FlutterEngineGroup
之後,FlutterEngine
都將由 FlutterEngineGroup
去生成,生成的 FlutterEngine
可以獨立應用於 FlutterActivity
/FlutterViewController
,甚至是 FlutterFragment
:
所以就像例子上所示,你可以在一個
Activity
上顯示兩個獨立的 FlutterView 。
這其實得益於通過 FlutterEngineGroup
生成的 FlutterEngine
可以共享 GPU 上下文, font metrics 和 isolate group snapshot ,從而實現了更快的初始速度和更低的記憶體佔用。
下圖是使用官方例項開啟16個頁面之後的記憶體使用情況,並且每個頁面成功返回且沒有出現黑屏。
簡單的使用介紹
使用 FlutterEngineGroup
首先需要建立一個 FlutterEngineGroup
單例物件,之後每當需要建立 Engine 時,就通過它的 createAndRunEngine(activity, dartEntrypoint)
來建立對應的 FlutterEngine
。
val app = activity.applicationContext as App
// This has to be lazy to avoid creation before the FlutterEngineGroup.
val dartEntrypoint =
DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(), entrypoint
)
engine = app.engines.createAndRunEngine(activity, dartEntrypoint)
this.delegate = delegate
channel = MethodChannel(engine.dartExecutor.binaryMessenger, "multiple-flutters")
複製程式碼
以官方 Demo 的這段程式碼為例子:
1、首先通過 findAppBundlePath
和 entrypoint
建立出 DartEntrypoint
物件,這裡的 findAppBundlePath
主要就是預設的 flutter_assets
目錄;而 entrypoint
其實就是 dart 程式碼裡啟動方法的名稱;也就是繫結了在 dart 中 runApp
的方法。
///kotlin
app.engines.createAndRunEngine(pathToBundle, "topMain")
///dart
@pragma('vm:entry-point')
void topMain() => runApp(MyApp());
複製程式碼
2、通過上面建立的 dartEntrypoint
和 context
,使用 FlutterEngineGroup
就可以建立出對應的 FlutterEngine
,其實在內部就是通過FlutterJNI.nativeSpawn
和原有的引擎互動,得到新的 Long 地址 id。
在 C++ 層類似於原有的
RunBundleAndSnapshotFromLibrary
方法,但是它不能更改包路徑或者 asset ,所以只能載入同一份 AOT 檔案,這裡得到的指標地址就是一個新的AndroidShellHolder
。
3、最後利用生成的 FlutterEngine
的 binaryMessenger
來得到一個 MethodChannel
用於原生和 dart 之間的通訊。
通過上述流程得到的 Engine ,自然就可以直接用於渲染執行新的 Flutter UI,比如直接繼承 FlutterActivity
,然後 override provideFlutterEngine
方法返回得到的 Engine 。
class SingleFlutterActivity : FlutterActivity()
·······
override fun provideFlutterEngine(context: Context): FlutterEngine? {
return engine
}
}
複製程式碼
是不是很簡單?這麼簡單的接入後:
- 在 dart 層面可以通過
MethodChannel
開啟原始頁面; - 在原生層可以通過新建
FlutterEngine
開啟新的 Flutter 頁面; - 甚至你還可以在原生層開啟一個
FlutterView
的 Dialog;
當然,到這裡你可能已經注意到了,因為每個 Flutter 頁面都是一個獨立的 Engine ,由於 dart isolate 的設計理念,每個獨立 Engine 的 Flutter 頁面記憶體是無法共享的。
也就是說,當你需要共享資料時,只能在原生層持有資料,然後注入或者傳遞到每個 Flutter 頁面中,就像官方所說的,每個 Flutter 頁面更像是一個獨立 Flutter 模組。
當然這也造成了一些不必要的麻煩,比如:同一張圖片,在原生層、不同 Flutter Engine 會出現多次載入的問題,這種問題可能就需要你針對 Flutter 的圖片載入使用外界紋理,來實現在原生層統一的記憶體管理等。
另外目前我發現問題還有: Android 11 上的 ARM TBI 問題 ,不過通過這次嘗試,相信 FlutterEngineGroup
的進展將會越來越明朗,更早的被應用到生產環境中。