03:flutter引擎初始化
Flutter 的加载生命周期与 iOS 原生有着本质区别。原生 App 的启动是 **dyld → Runtime → UIKit** 的控制权交接;而 Flutter 的启动,本质上是 **宿主平台加载 Flutter 引擎 → 启动 Dart VM → 运行 Dart Isolate → 构建渲染树** 的过程。
下面按阶段拆解,每一步都标出与 iOS 原生启动的对应关系。
---全局视角:Flutter 加载链
宿主 App 启动(iOS: main → UIApplicationMain)
│
▼
┌───────────────────────────────────────────────────────────────────────┐
│ Step 1: 查找 Flutter 资源 │
│ 在 .app/.ipa 包内定位引擎动态库、Dart AOT 快照、图片字体等资源 │
└───────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────┐
│ Step 2: 加载 Flutter 引擎库 │
│ iOS: 首次调用 FlutterEngine 的 runWithEntrypoint: 时加载 │
│ 把 libFlutter 等动态库 mmap 到进程内存 │
└───────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────┐
│ Step 3: 启动 Dart VM │
│ 加载 Dart 快照,初始化 Dart 运行时(每个进程仅一次) │
└───────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────┐
│ Step 4: 创建并运行 Dart Isolate │
│ 执行 Dart 入口点 main(),调用 runApp() │
└───────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────┐
│ Step 5: 将 UI 挂载到引擎 │
│ 创建 FlutterViewController,将 Flutter UI 嵌入原生视图层级 │
└───────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────┐
│ Step 6: 构建 Widget 树 → Element 树 → RenderObject 树 │
│ 调度 warm-up frame,完成首帧 build / layout / paint │
└───────────────────────────────────────────────────────────────────────┘
│
▼
首帧上屏,Flutter UI 可见
Step 1:查找 Flutter 资源
Flutter 引擎运行时和编译后的 Dart 代码,在 iOS 上以动态库形式打包在 .app 内,同时还有图片、字体等资源。
这一步发生在首次构造 FlutterEngine 时。iOS 上,资源查找与引擎创建绑定。
与原生启动的对应:原生 App 在 pre-main 阶段,dyld 会解析 Mach-O 的 LC_LOAD_DYLIB,递归加载依赖库。Flutter 的资源查找相当于把「加载哪些库」这一步前置到了 FlutterEngine 的构造阶段。
Step 2:加载 Flutter 引擎库
找到资源后,引擎的动态库会被 mmap 到进程内存,每个进程只加载一次。
iOS 与 Android 的差异在这里出现:
| 平台 | 库加载时机 |
|---|---|
| Android | 构造 FlutterEngine 时即加载,因为 JNI 连接器需要引用 Flutter C++ 库 |
| iOS | 首次调用 runWithEntrypoint: 时加载,而非构造 FlutterEngine 时 |
这意味着 iOS 上你可以先构造 FlutterEngine 而不触发库加载,真正加载发生在运行 Dart 入口点时。
与原生启动的对应:这一步相当于 dyld 的 Rebase/Bind 阶段,修正符号地址、绑定外部函数。区别在于,Flutter 引擎库的加载是由宿主主动触发的,而不是 dyld 自动递归完成的。
Step 3:启动 Dart VM
Dart VM 负责管理 Dart 内存和并发。在 JIT 模式下,它还负责运行时编译 Dart 源码为机器码。
关键特性:
-
每个应用会话只有一个 Dart 运行时。
-
Dart VM 启动是一次性的:iOS 上在首次运行 Dart 入口点时启动。
-
Dart VM 启动后永不关闭,生命周期跟随进程。
-
此时 Dart 代码的快照(snapshot) 被加载到内存。
iOS 上,Dart VM 的启动时机是 runWithEntrypoint: 被调用时,而非构造 FlutterEngine 时。
与原生启动的对应:Dart VM 相当于 Objective-C Runtime。原生启动中,_objc_init 在 pre-main阶段初始化 Runtime;Flutter 中,Dart VM 的初始化由引擎在宿主调用 runWithEntrypoint: 时主动触发,时机更晚、更可控。
Step 4:创建并运行 Dart Isolate
Dart VM 初始化后,Flutter 引擎在 Dart 运行时中启动一个 DartIsolate。
Isolate 是 Dart 的内存和线程容器:
-
每个
FlutterEngine实例对应一个 Isolate,同一个 Dart VM 可托管多个 Isolate。 -
创建 Isolate 时,宿主平台会同步创建辅助线程:一个用于 GPU 卸载,一个用于图像解码。
-
此时 Dart 入口点
main()被执行,runApp()被调用,Widget 树开始构建。 -
如果
FlutterEngine未挂载到任何 UI 组件,AppLifecycleState.detached会表明这一状态。
与原生启动的对应:Isolate 相当于原生 App 的主线程 RunLoop。main() 执行相当于原生 App 进入 main() 函数。区别在于,原生 App 的 main() 由 dyld 跳转执行,Flutter 的 Dart main() 由引擎在 Isolate 创建后主动调用。
Step 5:将 UI 挂载到 Flutter 引擎
对于标准 Flutter App,此时需要将 Flutter UI 嵌入宿主视图层级。
iOS 上,创建一个 FlutterViewController,将其 view 添加到原生视图树中。FlutterEngine 与 FlutterViewController 建立关联,Flutter 的渲染输出被嵌入到原生视图的某个子视图。
这一步标志着 Flutter 引擎开始接收 vsync 信号,渲染流水线被激活。
与原生启动的对应:相当于原生 App 中 UIWindow 设置 rootViewController 并 makeKeyAndVisible()。区别在于,原生 App 的 UI 直接由 UIKit 渲染,而 Flutter 的 UI 是引擎渲染完成后,将结果嵌入原生视图。
Step 6:Widget → Element → RenderObject 三棵树
这是 Flutter 渲染的核心。Dart 代码描述 UI 的方式,不是直接操作原生视图,而是构建三棵树。
三棵树的关系
Widget 树(不可变配置)
│ build()
▼
Element 树(中间管理者,持有 Context 和 State,生命周期长,尽量复用)
│ createRenderObject()
▼
RenderObject 树(渲染实体,负责测量、布局、绘制,生命周期最长)
Widget 树:每次 build() 都会重新创建,是轻量的纯数据配置类,属性全部 final,不可变。它只是“图纸”,描述 UI 应该长什么样。
Element 树:中间代理人,持有 Context 和 State,管理生命周期。它在 build() 之间被复用,不会轻易销毁。三棵树的核心在于 Element 的复用机制。
RenderObject 树:真正执行测量、布局、绘制的实体,性能开销最大,必须复用。
渲染流水线
当 runApp() 调用后,WidgetsFlutterBinding 的 scheduleWarmUpFrame() 被触发。
Warm-up frame 是 Flutter 启动优化的关键机制:Flutter 引擎通常等待系统 vsync 才请求帧,但 vsync 可能在 App 启动后几毫秒才到来。框架利用这段时间主动调度一个 warm-up frame,提前执行 build、layout、paint,使得真正 vsync 到来时,大部分工作已经完成,首帧可以以最小代价生成。
Flutter 与 iOS 原生启动的核心差异
| 维度 | iOS 原生 | Flutter |
|---|---|---|
| UI 渲染 | UIKit + Core Animation | 自绘引擎(Impeller/Skia) |
| 启动入口 | dyld 跳转 main() |
引擎调用 Dart main() |
| 运行时 | ObjC Runtime(_objc_init) |
Dart VM(进程级单例) |
| 线程模型 | 主 RunLoop | UI Thread / Raster Thread / 辅助线程 |
| 首帧调度 | RunLoop + CADisplayLink | Warm-up Frame + vsync |
| 热重载 | 不支持 | JIT 模式下支持 |
启动优化要点
基于以上流程,Flutter 启动优化的核心思路是预加载引擎和延迟非首屏任务。
-
引擎预热:在 App 空闲时提前构造
FlutterEngine并调用runWithEntrypoint:,将引擎库加载和 Dart VM 启动提前完成。首屏直接复用已就绪的引擎,显著降低冷启动到首帧的时间。 -
Dart VM 预热:Dart VM 启动后永不关闭,可以在后台线程提前初始化。
-
减少首帧工作量:利用
scheduleWarmUpFrame的机制,但注意首帧 build 不宜过重。首屏只渲染必要内容,非关键组件延迟到首帧之后。 -
资源预缓存:仅预缓存用户接下来 1-2 屏会看到的图片和字体,不要预缓存整个 App,否则反而拖慢启动并浪费内存。
一句话总结:Flutter 的加载生命周期是 宿主加载引擎 → 启动 Dart VM → 运行 Isolate → 构建三棵树 → 首帧上屏。与原生启动最大的区别在于,Flutter 把“UI 渲染”这件事从原生平台手中完全接管了,Dart 代码 AOT 编译后直接驱动 GPU,绕过了 UIKit 的抽象层。
浙公网安备 33010602011771号