Flutter 底层原理揭秘框架如何工作(十五)

4 阅读9分钟

前言

前课中,我们一直在使用 Flutter——写 Widget、搭布局、管状态、做导航。但你有没有好奇过:当你写下 Text('Hello') 时,Flutter 到底做了什么,才让这几个字出现在屏幕上?

本文基于官方教程的最后一章「How Flutter Works」,这是一个由 6 个视频组成的深度系列,带你从高层架构一路深入到渲染引擎。本文不涉及代码编写,而是帮你建立完整的心智模型——知道"为什么"往往比知道"怎么做"更重要。


一、Flutter 的整体架构

1.1 声明式 vs 命令式

传统的 UI 开发是命令式的——你需要一步步告诉系统"创建一个按钮、把它放到这里、改变它的颜色"。而 Flutter 是声明式的——你只需描述"界面长什么样",Flutter 负责把描述变成现实。

// 声明式:你描述"我要什么"
// Flutter 负责"怎么实现"
Widget build(BuildContext context) {
  return Container(
    color: isActive ? Colors.green : Colors.grey,
    child: Text('Hello'),
  );
}
// 当 isActive 变化时,你不需要手动找到 Container 再改颜色
// 只需调用 setState,Flutter 自动对比差异并更新

1.2 多平台框架

Flutter 不依赖原生平台的 UI 组件。它自己画每一个像素——按钮、文字、滚动条,全部由 Flutter 的渲染引擎绘制。这就是为什么同一份代码能在 iOS、Android、Web、桌面上运行且外观一致。

1.3 Dart 的角色

Flutter 选择 Dart 语言不是偶然的。Dart 有几个关键特性非常适合 UI 框架:AOT 编译(提前编译成机器码,启动快)、JIT 编译(即时编译,支持热重载)、垃圾回收(自动内存管理),以及异步支持(async/await)。


二、三棵树(The Three Trees)

这是理解 Flutter 最核心的概念。你写的每一行 Widget 代码,在 Flutter 内部会经过三棵树的处理:

你写的代码              Flutter 内部维护
    │
    ▼
┌──────────┐     ┌──────────┐     ┌──────────┐
│ Widget 树  │ ──→ │ Element 树 │ ──→ │ Render 树  │ ──→ 屏幕像素
│ (配置描述) │     │ (生命周期) │     │ (布局绘制) │
└──────────┘     └──────────┘     └──────────┘
  轻量级对象          持久化对象         实际绘制
  随时可重建          管理更新           计算布局和绘制

2.1 Widget 树:配置描述

Widget 是不可变的配置对象。每次调用 setState,Flutter 会重新调用 build 方法,创建一棵新的 Widget 树。Widget 非常轻量,创建和丢弃的成本很低。

你可以把 Widget 想象成菜单——它描述了"我想要什么",但它本身不是菜。

2.2 Element 树:粘合剂

Element 是 Widget 树和 Render 树之间的粘合层。每个 Widget 在首次构建时会创建一个对应的 Element。Element 是持久化的——当 Widget 重建时,Element 不会被丢弃,而是对比新旧 Widget 的差异,决定是否需要更新 Render 对象。

你可以把 Element 想象成服务员——他拿着菜单(Widget)去厨房(Render)下单,如果客人改了菜(Widget 重建),服务员只会告诉厨房"把这道菜换了",而不是重新下一整桌的单。

2.3 Render 树:实际绘制

RenderObject 负责真正的脏活累活——计算布局(每个组件多大、放在哪里)和绘制像素。Flutter 的布局规则很简单:约束向下流动,尺寸向上传递,父组件设置位置。

约束向下流动:父组件告诉子组件"你最多这么大"
              ↓
尺寸向上传递:子组件告诉父组件"我实际这么大"
              ↓
父组件定位:父组件决定子组件放在哪里

这就是为什么 Flutter 的布局系统能快速完成——它只需要一次遍历就能确定所有组件的大小和位置。


三、State 的生命周期

在第 7 课中我们学了 StatefulWidget,但只用了 setState。实际上 State 对象有完整的生命周期:

创建                        运行中                      销毁
  │                          │                          │
  ▼                          ▼                          ▼
initState()    ──→    didChangeDependencies()    ──→    dispose()
  │                          │
  │                    didUpdateWidget()
  │                          │
  └──────────→  build()  ←───┘
                  ↑
              setState()

各方法的作用:

  • initState() :State 创建时调用一次,适合初始化资源(如创建动画控制器)
  • didChangeDependencies() :当依赖的 InheritedWidget(如 MediaQuery、Theme)变化时调用
  • didUpdateWidget() :当父组件重建并传入新的 Widget 时调用
  • build() :每次需要重绘时调用,返回 Widget 树
  • setState() :标记 State 为"脏",触发下一帧重新调用 build
  • dispose() :State 永久销毁时调用,适合清理资源(如取消网络请求、释放动画控制器)

3.1 关于 const 构造函数的性能优化

// 没有 const:每次 build 都创建新对象
child: Text('Hello')

// 有 const:编译时创建,运行时复用同一个对象
child: const Text('Hello')
// Flutter 检测到 Widget 没变,跳过该子树的重建

这就是为什么官方推荐尽量使用 const 构造函数——它能显著减少 Widget 树重建的范围。


四、RenderObjectWidget

在前面的课程中,我们用过 StatelessWidgetStatefulWidget。但它们都不会直接渲染任何东西——它们只是组合和管理其他 Widget 的容器。

真正能在屏幕上画出东西的是 RenderObjectWidget。Flutter SDK 中的 PaddingAlignSizedBoxColoredBox 等底层组件都继承自 RenderObjectWidget。而 Container 只是把这些底层组件打包在一起的便利组件。

你写的代码               实际绘制的
Container          →   Padding + ColoredBox + SizedBox + ...
Text               →   RichText → RenderParagraph
Image              →   RawImage → RenderImage

作为应用开发者,你通常不需要直接创建 RenderObjectWidget。但了解这一层有助于理解为什么某些 Widget 的行为是这样的。


五、RenderObject 的职责

每个 RenderObject 有四个核心职责:

布局(Layout) :接收父组件传来的约束,计算自己的尺寸,然后为子组件分配位置。这就是 Flutter 文档中常说的"约束向下,尺寸向上,父组件定位"。

绘制(Paint) :在确定了大小和位置后,调用 paint 方法将自己绘制到画布上。绘制是分层的——可以有背景层、内容层和前景层。

命中测试(Hit Testing) :当用户点击屏幕时,Flutter 需要确定点击了哪个组件。RenderObject 通过命中测试从根节点逐层向下查找,确定最终接收触摸事件的组件。

语义(Semantics) :为辅助功能(如屏幕阅读器)提供信息。describeSemanticsConfiguration 方法告诉系统这个组件"是什么"——是按钮、文字还是图片。


六、Flutter 引擎和嵌入器

6.1 Flutter 引擎

Flutter 引擎是用 C++ 编写的,负责最底层的工作:渲染每一帧画面、处理文字排版、管理线程、与操作系统通信。它使用 Skia(或更新的 Impeller)图形库来绘制 UI。

为什么用 C++ 而不是 Dart?因为渲染引擎需要极致的性能,C++ 可以直接操作硬件加速(GPU),而 Dart 更适合编写应用层逻辑。

6.2 嵌入器(Embedder)

Flutter 应用在每个平台上都需要一个"宿主"来启动和运行。嵌入器就是这个宿主——在 Android 上是一个 Activity,在 iOS 上是一个 UIViewController,在 Web 上是一个 Canvas 元素。

嵌入器的职责包括:创建 Flutter 引擎实例、管理应用生命周期、处理输入事件、提供平台服务(如相机、GPS)。

6.3 Platform Channels

Flutter(Dart)和原生平台(Java/Kotlin/Swift/JS)之间通过 Platform Channels 通信。这是一种消息传递机制——Dart 端发送消息,原生端接收并处理,然后返回结果。

Dart 代码  ←──  Platform Channel  ──→  原生平台代码
(你的应用)     (消息传递管道)       (相机、GPS、蓝牙等)

七、完整的渲染流程

当你调用 setState 后,从代码到屏幕像素的完整流程:

1. setState() 被调用
   ↓
2. State 被标记为"脏"(dirty)
   ↓
3. 下一帧到来时,Flutter 遍历脏节点
   ↓
4. 重新调用 build(),生成新的 Widget 树
   ↓
5. Element 树对比新旧 Widget(diff 算法)
   ↓
6. 有差异 → 更新对应的 RenderObject
   无差异 → 跳过(const Widget 在这里发挥作用)
   ↓
7. RenderObject 重新计算布局(layout)
   ↓
8. RenderObject 重新绘制(paint)
   ↓
9. 渲染引擎(Skia/Impeller)将画面合成
   ↓
10. GPU 将画面输出到屏幕
    ↓
    用户看到更新后的界面 ✨

整个过程在 16 毫秒内完成(60fps),用户感觉界面更新是"瞬间"的。


八、视频学习资源

本章是一个由 6 个视频组成的系列,建议按顺序观看:

#视频主题核心内容
1Flutter's Architecture声明式编程、多平台架构、Dart 的角色
2The Three TreesWidget 树、Element 树、Render 树的协作
3The State ClassState 完整生命周期、const 优化、setState 原理
4The RenderObjectWidget哪些 Widget 真正渲染、Container 的真面目
5The RenderObject布局、绘制、命中测试、语义四大职责
6Engine and EmbeddersC++ 引擎、Skia/Impeller、Platform Channels

视频链接可在官方教程页面找到:How Flutter Works


九、本节知识点小结

三棵树: Widget 树(配置描述,轻量可重建)→ Element 树(持久粘合层,对比差异)→ Render 树(实际布局和绘制)。

State 生命周期: initState → didChangeDependencies → build → (setState → build) → dispose。理解生命周期有助于正确初始化和释放资源。

布局规则: 约束向下流动,尺寸向上传递,父组件设置位置。一次遍历完成所有布局计算。

渲染引擎: 用 C++ 编写,使用 Skia/Impeller 图形库绘制每一帧。嵌入器负责与各平台的原生环境对接。

const 优化: 使用 const 构造函数可以让 Flutter 跳过未变化的子树重建,显著提升性能。


十、整个系列完结语

恭喜你完成了 Flutter 官方入门教程的全部 17 课!让我们做最后一次回顾:

阶段你学到了什么
第 1-8 课从零搭建环境,理解 Widget 和布局,掌握状态和动画
第 9-12 课从网络获取数据,用 MVVM 架构管理应用状态
第 13-16 课iOS 风格 UI、自适应布局、高级滚动、页面导航
第 17 课Flutter 的底层原理——三棵树、渲染引擎、State 生命周期

你不仅会"用" Flutter,还理解了它"为什么"这样工作。这会让你在遇到问题时能更快地定位原因、在做架构决策时更有信心。

从零基础到理解底层原理,你已经走完了 Flutter 入门的完整路径。接下来,去构建属于你自己的应用吧!

参考资料:Flutter 官方教程 - How Flutter Works