控制載入順序,最佳化效能與記憶體

本頁面描述了展示一個 Flutter UI 的分解步驟。知道了這一點,您可以就何時對 Flutter 引擎進行預熱,在哪個階段可以進行哪些操作,以及這些操作的潛在問題和記憶體成本做出更好、更明智的決策。

載入 Flutter

在展示 Flutter UI 時, Android 與 iOS 應用(用於整合到現有應用的兩個受支援的平台),純 Flutter 應用,以及 add-to-app 的模式,在概念上的載入步驟順序相似。

查詢 Flutter 資源

Flutter 的引擎執行時和應用已編譯的 Dart 程式碼都被打包為 Android 和 iOS 上的共享庫。載入 Flutter 的第一步是在 .apk、.ipa 或 .app 中查詢這些資源(以及其他 Flutter 資源,例如影象和字型,假如適用的話還有 JIT 程式碼)。

當您首次在 AndroidiOS 上呼叫 API 建構 FlutterEngine 時,就會發生這種情況。

載入 Flutter 庫

找到後,引擎的共享庫將在每個處理序中載入一次記憶體。

Android 上,當建構 FlutterEngine 時也會發生這種情況,因為 JNI 聯結器需要參考 Flutter C++ 庫。在 iOS 上,這是在首次執行 FlutterEngine 時發生的,例如執行 runWithEntrypoint:

啟動 Dart VM

Dart 執行時負責管理 Dart 程式碼的 Dart 記憶體與非同步。在 JIT 模式下,它還負責在執行時將 Dart 原始碼編譯為機器碼。

在 Android 和 iOS 上,每個應用程式會話都存在一個 Dart 執行時。

Android 上首次建構 FlutterEngine,以及在 iOS 上首次 執行 Dart 入口 時,將完成一次 Dart VM 啟動。

此時,您的 Dart 程式碼的 snapshot 也將從應用程式的檔案載入到記憶體中。

即使您直接使用 Dart SDK而沒 Flutter 引擎,也會這樣執行,這是一個通用的過程。

Dart VM 啟動後永遠不會關閉。

建立並執行一個 Dart Isolate

在初始化 Dart 執行時之後,下一步就是 Flutter 引擎對 Dart 執行時的使用。

這是透過在 Dart 執行時中啟動 Dart Isolate 來完成的。 isolate 是 Dart 的記憶體和執行緒容器。此時在宿主平臺上還建立了許多 輔助執行緒 來支援 isolate,例如用於解除 GPU 處理的執行緒和用於影象解碼的執行緒。

每個 FlutterEngine 例項都存在一個 isolate,並且同一個 Dart VM 可以承載多個 isolate。

Android 上,當您在 FlutterEngine 例項上呼叫 DartExecutor.executeDartEntrypoint() 時,就會發生這種情況。

iOS 上,當您對 FlutterEngine 例項呼叫 runWithEntrypoint:時會發生這種情況。

此時,Dart 程式碼會執行預設的入口點方法 (預設是 main.dart 檔案的 main() 方法) ,如果你在 main() 方法中呼叫 Flutter 的 runApp() 方法,則你的 Flutter 應用或庫的 widget 樹將會建立並建構。如果你需要阻止某些功能在 Flutter 程式碼中執行,則需要使用列舉值 AppLifecycleState.detached 表示其不繫結在任何 UI 元件上。

將 UI 掛載到 Flutter 引擎

啟動後不久,一個標準的完整的 Flutter 應用程式便會達到此狀態。

在 add-to-app 的場景中,例如透過在 Android 上使用 FlutterActivity.withCachedEngine() 方法建構的 Intent,呼叫 startActivity() 時,或者,在 iOS 上呼叫 initWithEngine: nibName: bundle:,展示例項化的 FlutterViewController,都會將 FlutterEngine 掛載到 UI 元件。

如果在沒有啟動 Flutter UI 元件的情況下也是如此, 例如在 Android 上使用 FlutterActivity.createDefaultIntent() 或在 iOS 上使用 FlutterViewController initWithProject: nibName: bundle: 預熱一個 FlutterEngine。在這些情況下,將建立一個隱含的 FlutterEngine

在後台,這兩個平台的UI元件都為 FlutterEngine 提供了渲染層,例如 Android 上的 SurfaceiOS 上的 CAEAGLLayerCAMetalLayer

此時,您的 Flutter 程式產生的 Layer 樹將轉換為 OpenGL(或 Vulkan 或 Metal)GPU 指令。

記憶體和延遲

顯示 Flutter UI 會耗費不少時間。提前啟動 Flutter 引擎可以降低時間開銷。

對於 add-to-app 的場景,預熱相應的選擇是,讓您決定什麼時候預載入 FlutterEngine (即載入 Flutter 庫,啟動 Dart VM 並在 isolate 中執行入口點),以及確定記憶體的佔用與時間開銷。您還需要知道,在將 UI 元件隨後掛載到該 FlutterEngine 時,預熱會如何影響 Flutter 渲染首幀的記憶體和時間開銷。

用 Flutter v1.10.3 版本,在 2015 年的低端裝置上測試,release AOT 模式下,預熱 FlutterEngine 的開銷:

  • Android 平台需要 42 MB 記憶體,耗費 1530 毫秒。主執行緒阻塞了 330 毫秒;

  • iOS 平台需要 22 MB 記憶體,耗費 860 毫秒。主執行緒阻塞了 260 毫秒。

Flutter 使用者介面可以在預熱期間被載入。所需時間與渲染出首幀的時間有關。

在記憶體方面的開銷(具體根據使用情況而定)可能是:

  • 約 4 MB 系統記憶體用於建立 pthread;

  • 約 10 MB 是 GPU 驅動記憶體;

  • 約 1 MB 是用於 Dart 執行時管理的記憶體;

  • 約 5 MB 用於 Dart 載入的字型對映。

在時間方面的開銷(具體根據使用情況而定)可能是:

  • 約 20 毫秒用於從應用套件中收集 Flutter 資源;

  • 約 15 毫秒用於 dlopen 載入 Flutter 引擎的函式庫;

  • 約 200 毫秒用於建立 Dart VM 並載入 AOT snapshot;

  • 約 200 毫秒用於載入 Flutter 依賴的字型和資源;

  • 約 400 毫秒來執行應用入口,建立第一個 widget 樹,並編譯所需的 GPU 著色器程式。

應該對 FlutterEngine 進行預熱,不應過於提早,以延遲記憶體佔用,但又要避免 Flutter 引擎初始化的時機與顯示 Flutter 的首幀的時機趕在一起。

確切的時間取決於應用的結構與不斷試探的結果。一個範例是在 Flutter 繪製螢幕之前將 Flutter 引擎載入到螢幕中。

引擎預熱後,載入 UI 首幀的成本為:

  • Android 上為 320 毫秒,另外需要 12 MB 的記憶體(在很大程度上取決於螢幕的物理畫素大小);

  • iOS 上為 200 毫秒,另外需要 16 MB 的記憶體(在很大程度上取決於螢幕的物理畫素大小)。

在記憶體方面,開銷主要用於渲染的圖形記憶體緩衝區,並且取決於螢幕大小。

在時間方面,開銷主要是在等待系統回呼(Callback),為 Flutter 提供渲染層,並編譯其餘無法預先預測的著色器程式。這是一次性的開銷。

釋放 Flutter UI 元件後,將釋放與 UI 相關的記憶體。這不會影響 Flutter 狀態(除非也釋放了 FlutterEngine),狀態位於 FlutterEngine 中。

關於建立多個 FlutterEngine 對效能影響的詳細情況,請參考文件: 多個 Flutter 例項