加载顺序、性能和内存
本页介绍显示 Flutter UI 所涉及的步骤。了解这些步骤,可以帮助您在预热 Flutter 引擎的时机、各阶段可行的操作以及这些操作的延迟和内存成本等方面做出更明智的决策。
加载 Flutter
#Android 和 iOS 应用(集成到现有应用支持的两个平台)、完整的 Flutter 应用以及添加到应用模式(add-to-app patterns)在显示 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** 上,通过呈现使用 initWithEngine: nibName: bundle:
初始化过的 FlutterViewController
。
如果没有预热 FlutterEngine
而启动 Flutter UI 组件(例如在 **Android** 上使用 FlutterActivity.createDefaultIntent()
,或在 **iOS** 上使用 FlutterViewController initWithProject: nibName: bundle:
),情况也是如此。这些情况下会隐式创建 FlutterEngine
。
在后台,两个平台的 UI 组件都为 FlutterEngine
提供一个渲染表面,例如 **Android** 上的 Surface
,或 **iOS** 上的 CAEAGLLayer 或 CAMetalLayer。
此时,您的 Flutter 程序每帧生成的 Layer
树被转换为 OpenGL(或 Vulkan 或 Metal)GPU 指令。
内存和延迟
#显示 Flutter UI 会产生不小的延迟成本。通过提前启动 Flutter 引擎可以减轻这种成本。
对于添加到应用场景(add-to-app scenarios),最相关的选择是决定何时预加载 FlutterEngine
(即加载 Flutter 库、启动 Dart VM,并在 Isolate 中运行入口点),以及预热的内存和延迟成本是多少。您还需要了解预热如何影响 UI 组件随后附加到该 FlutterEngine
时首次渲染 Flutter 帧的内存和延迟成本。
截至 Flutter v1.10.3,并在低端 2015 年类设备上以 release-AOT 模式进行测试,预热 FlutterEngine
的成本为:
- **Android** 上预热成本为 42 MB 和 1530 ms。其中 330 ms 是主线程的阻塞调用。
- **iOS** 上预热成本为 22 MB 和 860 ms。其中 260 ms 是主线程的阻塞调用。
可以在预热期间附加 Flutter UI。剩余时间将计入首次帧延迟。
内存方面,成本样本(因用例而异)可能包括:
- ~4 MB OS 内存使用量,用于创建 pthreads。
- ~10 MB GPU 驱动程序内存。
- ~1 MB 用于 Dart 运行时管理的内存。
- ~5 MB 用于 Dart 加载的字体映射。
延迟方面,成本样本(因用例而异)可能包括:
- ~20 ms 用于从应用程序包收集 Flutter 资源。
- ~15 ms 用于 dlopen Flutter 引擎库。
- ~200 ms 用于创建 Dart VM 并加载 AOT 快照。
- ~200 ms 用于加载 Flutter 依赖的字体和资源。
- ~400 ms 用于运行入口点、创建第一个 widget 树以及编译所需的 GPU 着色器程序。
FlutterEngine
应在足够晚的时间进行预热,以延迟所需的内存消耗,但又足够早,以避免将 Flutter 引擎的启动时间与显示 Flutter 的首次帧延迟合并。
确切的时间取决于应用的结构和启发式方法。例如,可以在 Flutter 绘制屏幕之前,在前一个屏幕中加载 Flutter 引擎。
考虑到引擎预热,UI 附加时的首次帧成本为:
- **Android** 上为 320 ms,额外增加 12 MB(高度依赖于屏幕的物理像素尺寸)。
- **iOS** 上为 200 ms,额外增加 16 MB(高度依赖于屏幕的物理像素尺寸)。
内存方面,成本主要是在渲染中使用的图形内存缓冲区,并且取决于屏幕尺寸。
延迟方面,成本主要是等待操作系统回调向 Flutter 提供渲染表面以及编译剩余的、无法预先预测的着色器程序。这是一个一次性成本。
当 Flutter UI 组件被释放时,与 UI 相关的内存将被释放。这不会影响生活在 FlutterEngine
中的 Flutter 状态(除非 FlutterEngine
也被释放)。
有关创建多个 FlutterEngine
的性能详细信息,请参阅 multiple Flutters。