Flutter 2.0 下混合開發淺析

戀貓de小郭發表於2021-03-18

多餘的前言

Flutter 2.0 釋出時,其中最受大家關注之一的內容就是 Add-to-App 相關的更新,因為除了熱更新之外,Flutter 最受大家詬病的就是混合開發體驗不好。

為什麼不好呢?因為 Flutter 的控制元件渲染直接脫離了原生平臺,也就是無論頁面堆疊和渲染樹都獨立於平臺執行,這固然給 Flutter 帶來了較好的跨平臺體驗,但是也造成了在和原生平臺混合時存在高成本的問題。

且不說在已有的原生專案中整合 Flutter ,就是現階段在 Flutter 中整合原生控制元件的 PlatformView 和 Hybrid Composition 體驗也是有待提升,當然“有支援”和“能用”就已經是很不錯的進展。

所以 Flutter 2.0 在千呼萬喚中釋出了 FlutterEngineGroup 用於支援官方的 Add Flutter to existing app 方案。

在此方案出現之前,類似的第三方支援有 flutter_boostmix_stackflutter_thrio 等等 ,它們是否好用這裡不討論,但是這些方案都要面對的問題是:

非官方的支援必然存在每個版本需要適配的問題,而按照 Flutter 目前的 issue closedpr merge 的速度,很可能每個季度的版本都存在較大的變動,所以如果開發者不維護或者維護不及時,那麼侵入性極強的這類框架很容易就成為專案的瓶頸

而官方提供的 FlutterEngineGroup 方案有沒有缺陷?肯定有的,它目前看起來更像是被催生出來的狀態,各方面的問題還是有的,比如某些地方還存在不能 destroy 的問題。 (當然這個問題以及在 master 分支 merge 了)

image.png

但是官方提供的方案,就意味著這個設計得到了 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、首先通過 findAppBundlePathentrypoint 建立出 DartEntrypoint 物件,這裡的 findAppBundlePath 主要就是預設的 flutter_assets 目錄;而 entrypoint 其實就是 dart 程式碼裡啟動方法的名稱;也就是繫結了在 dart 中 runApp 的方法。


///kotlin
app.engines.createAndRunEngine(pathToBundle, "topMain")


///dart
@pragma('vm:entry-point')
void topMain() => runApp(MyApp());
複製程式碼

2、通過上面建立的 dartEntrypointcontext ,使用 FlutterEngineGroup 就可以建立出對應的 FlutterEngine ,其實在內部就是通過FlutterJNI.nativeSpawn 和原有的引擎互動,得到新的 Long 地址 id。

在 C++ 層類似於原有的 RunBundleAndSnapshotFromLibrary 方法,但是它不能更改包路徑或者 asset ,所以只能載入同一份 AOT 檔案,這裡得到的指標地址就是一個新的 AndroidShellHolder

3、最後利用生成的 FlutterEnginebinaryMessenger 來得到一個 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 的進展將會越來越明朗,更早的被應用到生產環境中。

相關文章