控制加载顺序,优化性能与内存
本页面描述了展示一个 Flutter UI 的分解步骤。知道了这一点,你可以就何时对 Flutter 引擎进行预热,在哪个阶段可以进行哪些操作,以及这些操作的潜在问题和内存成本做出更好、更明智的决策。
加载 Flutter
#在展示 Flutter UI 时, Android 与 iOS 应用(用于集成到现有应用的两个受支持的平台),纯 Flutter 应用,以及 add-to-app 的模式,在概念上的加载步骤顺序相似。
查找 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 代码的 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 上的 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 用户界面可以在预热期间被加载。所需时间与渲染出首帧的时间有关。
在内存方面的开销(具体根据使用情况而定)可能是:
-
约 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 的内存(在很大程度上取决于屏幕的物理像素大小)。
在内存方面,开销主要用于渲染的图形内存缓冲区,并且取决于屏幕大小。
在时间方面,开销主要是在等待系统回调,为 Flutter 提供渲染层,并编译其余无法预先预测的着色器程序。这是一次性的开销。
释放 Flutter UI 组件后,将释放与 UI 相关的内存。这不会影响 Flutter 状态(除非也释放了 FlutterEngine
),状态位于 FlutterEngine
中。
关于创建多个 FlutterEngine
对性能影响的详细情况,请参考文档:
多个 Flutter 实例。
除非另有说明,本文档之所提及适用于 Flutter 的最新稳定版本,本页面最后更新时间: 2024-08-05。 查看文档源码 或者 为本页面内容提出建议。