加载顺序、性能和内存

此页面描述了显示 Flutter UI 所涉及步骤的细分。了解这一点后,您可以对何时预热 Flutter 引擎、哪些操作在哪个阶段可以进行以及这些操作的延迟和内存成本做出更明智的决策。

加载 Flutter

Android 和 iOS 应用程序(集成到现有应用程序的两个支持平台)、完整的 Flutter 应用程序和添加到应用程序模式在显示 Flutter UI 时具有类似的加载步骤概念序列。

查找 Flutter 资源

Flutter 的引擎运行时和应用程序的编译 Dart 代码都作为共享库捆绑在 Android 和 iOS 上。加载 Flutter 的第一步是在您的 .apk/.ipa/.app 中找到这些资源(以及其他 Flutter 资源,例如图像、字体和 JIT 代码,如果适用)。

当您在 **Android** 和 **iOS** API 上首次构建 FlutterEngine 时,就会发生这种情况。

加载 Flutter 库

找到后,引擎的共享库将在每个进程中加载到内存一次。

在 **Android** 上,当构建 FlutterEngine 时,也会发生这种情况,因为 JNI 连接器需要引用 Flutter C++ 库。在 **iOS** 上,当 FlutterEngine 首次运行时,例如通过运行 runWithEntrypoint:,就会发生这种情况。

启动 Dart VM

Dart 运行时负责管理 Dart 代码的内存和并发。在 JIT 模式下,它还负责在运行时将 Dart 源代码编译为机器代码。

在 Android 和 iOS 上,每个应用程序会话只有一个 Dart 运行时。

在 **Android** 上首次构建 FlutterEngine 时,以及在 **iOS** 上首次 运行 Dart 入口点 时,会进行一次性的 Dart VM 启动。

此时,您的 Dart 代码的 快照 也将从您的应用程序文件加载到内存中。

这是一个通用过程,即使您直接使用 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 代码的选定入口点(默认情况下,您的 Dart 库的 main.dart 文件的 main() 函数)将被执行。如果您在 main() 函数中调用了 Flutter 函数 runApp(),那么您的 Flutter 应用或库的 widget 树也会被创建和构建。如果您需要阻止某些功能在您的 Flutter 代码中执行,那么 AppLifecycleState.detached 枚举值表示 FlutterEngine 未附加到任何 UI 组件,例如 iOS 上的 FlutterViewController 或 Android 上的 FlutterActivity

将 UI 附加到 Flutter 引擎

一个标准的完整 Flutter 应用会在应用启动后立即进入此状态。

在添加到应用的场景中,当您将 FlutterEngine 附加到 UI 组件时,就会发生这种情况,例如通过在 **Android** 上使用 startActivity() 调用带有使用 FlutterActivity.withCachedEngine() 构建的 Intent。或者,通过在 **iOS** 上使用 FlutterViewController 初始化并使用 initWithEngine: nibName: bundle: 展示。

即使 Flutter UI 组件在没有预热 FlutterEngine 的情况下启动,例如在 **Android** 上使用 FlutterActivity.createDefaultIntent(),或在 **iOS** 上使用 FlutterViewController initWithProject: nibName: bundle:,也是如此。在这些情况下,会隐式创建 FlutterEngine

在幕后,这两个平台的 UI 组件都为 FlutterEngine 提供渲染表面,例如 **Android** 上的 Surface 或 **iOS** 上的 CAEAGLLayerCAMetalLayer

此时,您的 Flutter 程序每帧生成的 Layer 树将转换为 OpenGL(或 Vulkan 或 Metal)GPU 指令。

内存和延迟

显示 Flutter UI 会产生非平凡的延迟成本。通过提前启动 Flutter 引擎可以降低此成本。

对于添加到应用程序的场景,最相关的选择是您决定何时预加载 FlutterEngine(即加载 Flutter 库、启动 Dart VM 并在隔离区中运行入口点),以及预热的内存和延迟成本是多少。您还需要了解预热如何影响随后将 UI 组件附加到该 FlutterEngine 时渲染第一帧 Flutter 的内存和延迟成本。

截至 Flutter v1.10.3,在低端 2015 级设备上进行测试,在发布-AOT 模式下,预热 FlutterEngine 的成本为

  • 在 **Android** 上预热需要 42 MB 和 1530 毫秒。其中 330 毫秒是主线程上的阻塞调用。
  • 在 **iOS** 上预热需要 22 MB 和 860 毫秒。其中 260 毫秒是主线程上的阻塞调用。

可以在预热期间附加 Flutter UI。剩余时间将加入到首帧延迟时间。

从内存角度来看,成本样本(取决于用例而变化)可能是

  • ~4 MB 操作系统的内存使用量,用于创建 pthreads。
  • ~10 MB GPU 驱动程序内存。
  • ~1 MB 用于 Dart 运行时管理的内存。
  • ~5 MB 用于 Dart 加载的字体映射。

从延迟角度来看,成本样本(取决于用例而变化)可能是

  • 从应用程序包中收集 Flutter 资产大约需要 20 毫秒。
  • 打开 Flutter 引擎库大约需要 15 毫秒。
  • 创建 Dart VM 并加载 AOT 快照大约需要 200 毫秒。
  • 加载 Flutter 依赖的字体和资产大约需要 200 毫秒。
  • 运行入口点、创建第一个小部件树并编译所需的 GPU 着色器程序大约需要 400 毫秒。

FlutterEngine 应该在足够晚的时候预热,以延迟所需的内存消耗,但要足够早,以避免将 Flutter 引擎启动时间与显示 Flutter 的第一帧延迟结合起来。

确切的计时取决于应用程序的结构和启发式方法。例如,可以在 Flutter 绘制屏幕之前加载 Flutter 引擎。

在引擎预热的情况下,UI 附加后的第一帧成本为

  • **Android** 上为 320 毫秒,另外 12 MB(高度依赖于屏幕的物理像素大小)。
  • **iOS** 上为 200 毫秒,另外 16 MB(高度依赖于屏幕的物理像素大小)。

从内存角度来看,成本主要是用于渲染的图形内存缓冲区,并且取决于屏幕大小。

从延迟角度来看,成本主要是等待操作系统回调为 Flutter 提供渲染表面并编译尚未预先预测的剩余着色器程序。这是一次性成本。

当 Flutter UI 组件被释放时,与 UI 相关的内存会被释放。这不会影响 Flutter 状态,Flutter 状态位于 FlutterEngine 中(除非 FlutterEngine 也被释放)。

有关创建多个 FlutterEngine 的性能详细信息,请参阅 多个 Flutter