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 的方式,不是直接操作原生视图,而是构建三棵树。

三棵树的关系

text
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 的抽象层。

posted on 2026-09-20 22:07  风zk  阅读(9)  评论(0)    收藏  举报

导航