加载顺序、性能和内存
本页介绍了显示 Flutter UI 所涉及步骤的详细分解。了解这些信息后,您可以就何时预热 Flutter 引擎、在哪个阶段可以进行哪些操作以及这些操作的延迟和内存成本做出更好、更明智的决定。
加载 Flutter
#Android 和 iOS 应用(集成到现有应用中支持的两个平台)、完整的 Flutter 应用以及 Add-to-App 模式在显示 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 代码的 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 应用在启动后会立即达到此状态。
在 Add-to-App 场景中,当您将 FlutterEngine
附加到 UI 组件时会发生此情况,例如在 Android 上通过使用 FlutterActivity.withCachedEngine()
构建的 Intent
调用 startActivity()
。或者,通过在 iOS 上呈现使用 initWithEngine: nibName: bundle:
初始化的 FlutterViewController
。
如果 Flutter UI 组件在未预热 FlutterEngine
的情况下启动,也会出现这种情况,例如在 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 场景,最相关的选择是决定何时预加载 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 UI 可以在预热期间附加。剩余时间会计入首次帧渲染延迟。
内存方面,成本样本(可变,取决于用例)可能为
- ~4 MB 操作系统用于创建 pthreads 的内存使用。
- ~10 MB GPU 驱动程序内存。
- ~1 MB 用于 Dart 运行时管理的内存。
- ~5 MB 用于 Dart 加载的字体映射。
延迟方面,成本样本(可变,取决于用例)可能为
- ~20 毫秒用于从应用程序包中收集 Flutter 资源。
- ~15 毫秒用于 dlopen Flutter 引擎库。
- ~200 毫秒用于创建 Dart VM 并加载 AOT 快照。
- ~200 毫秒用于加载 Flutter 依赖的字体和资源。
- ~400 毫秒用于运行入口点、创建第一个 widget 树并编译所需的 GPU 着色器程序。
FlutterEngine
应足够晚地预热,以延迟所需的内存消耗,但又要足够早,以避免将 Flutter 引擎启动时间与显示 Flutter 的首次帧延迟合并。
具体时机取决于应用程序的结构和启发式方法。例如,在 Flutter 绘制屏幕之前在屏幕中加载 Flutter 引擎。
给定引擎预热,UI 附加时的首帧成本为
- Android 上为 320 毫秒和额外 12 MB 内存(高度依赖于屏幕的物理像素大小)。
- iOS 上为 200 毫秒和额外 16 MB 内存(高度依赖于屏幕的物理像素大小)。
内存方面,成本主要是用于渲染的图形内存缓冲区,并且取决于屏幕大小。
延迟方面,成本主要是等待操作系统回调为 Flutter 提供渲染表面以及编译那些无法预先预测的剩余着色器程序。这是一次性成本。
当 Flutter UI 组件被释放时,与 UI 相关的内存会被释放。这不会影响存储在 FlutterEngine
中的 Flutter 状态(除非 FlutterEngine
也被释放)。
有关创建多个 FlutterEngine
的性能详情,请参阅多个 Flutter 实例。