Flutter 渲染流程与三棵树详解
目录
- 一、Flutter 渲染架构全览
- 二、三棵树是什么
- 三、三棵树的协作流程
- 四、各层职责与修改时机
- 五、常见场景:该动哪一层?
- 六、总结
一、Flutter 渲染架构全览
Flutter 的渲染流程从 Dart 代码到屏幕像素,经过了多个层次的处理:
你写的 Widget
↓
Widget Tree(配置描述)
↓
Element Tree(生命周期 + 状态管理)
↓
RenderObject Tree(布局 + 绘制)
↓
Layer Tree(合成)
↓
Skia / Impeller(光栅化)
↓
GPU → 屏幕
这套架构的核心设计目标是把"描述"和"执行"分离,让 Widget 可以廉价重建,而真正昂贵的布局和绘制操作尽量复用。
二、三棵树是什么
2.1 Widget Tree — 配置树
Widget 是不可变的配置描述,你每次调用 setState 或 build 都会产生全新的 Widget 对象。它极其轻量,创建和销毁的成本几乎可以忽略。
// Widget 只是一个数据类,描述"我想要什么"
Text('Hello', style: TextStyle(fontSize: 16))
Widget 本身不持有状态,也不负责渲染,它只是一份蓝图。
2.2 Element Tree — 实例树
Element 是 Widget 在运行时的实例,负责:
- 持有对应 Widget 和 RenderObject 的引用
- 管理组件生命周期(mount / unmount / activate)
- 决定 Widget 更新时是否能复用现有 RenderObject(通过
canUpdate比较runtimeType和key) - 维护
BuildContext(BuildContext其实就是Element本身)
abstract class Element implements BuildContext {
Widget _widget;
RenderObject? renderObject;
// ...
}
Element 树的结构与 Widget 树一一对应,但生命周期更长——Widget 重建时 Element 会尝试复用,而不是销毁重建。
2.3 RenderObject Tree — 渲染树
RenderObject 负责真正的布局(layout)和绘制(paint),是性能敏感的核心。
layout():通过父传入的Constraints计算自身尺寸,再把约束传给子节点paint():把自己绘制到PaintingContext提供的Canvas上- 每个 RenderObject 缓存自己的布局结果,只在
markNeedsLayout()/markNeedsPaint()被调用后才重新执行
常见子类:
| 类名 | 职责 |
|---|---|
RenderBox | 二维盒模型布局,绝大多数 Widget 的基础 |
RenderSliver | 滚动视口内的懒加载布局(ListView 用到) |
RenderFlex | Row / Column 的弹性布局实现 |
RenderCustomPaint | 自定义绘制入口 |
三、三棵树的协作流程
首次渲染
runApp(MyWidget())
→ WidgetsBinding.attachRootWidget
→ Element.mount() // 创建 Element 树
→ RenderObjectElement 同时创建 RenderObject 并插入渲染树
→ RendererBinding.drawFrame()
→ PipelineOwner.flushLayout() // 深度优先 layout
→ PipelineOwner.flushPaint() // 深度优先 paint
→ SceneBuilder 合成 Layer Tree
→ Window.render() → GPU
setState 触发更新
setState(() { ... })
→ Element.markNeedsBuild()
→ SchedulerBinding 在下一帧调用 BuildOwner.buildScope()
→ Element.rebuild() → widget.build()
→ 对每个子 Widget 调用 Element.updateChild()
├─ canUpdate == true → 复用 Element,调用 element.update(newWidget)
└─ canUpdate == false → 卸载旧 Element,mount 新 Element
→ 若 RenderObject 属性变化 → markNeedsLayout / markNeedsPaint
→ 下一帧重新 layout + paint
关键点:Widget 重建不等于 RenderObject 重建。只要 runtimeType 和 key 不变,Element 和 RenderObject 都会被复用,仅更新属性。这是 Flutter 性能的核心保障。
四、各层职责与修改时机
Widget 层
何时动这一层: 绝大多数业务开发都在这里。组合已有 Widget、控制 UI 结构和状态。
// 普通业务 UI:只需要组合 Widget
class MyCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
child: Text('内容'),
);
}
}
注意: Widget 层动不了底层渲染行为,改的只是"配置"。如果你想自定义布局规则或绘制,Widget 层就不够用了。
Element 层
何时动这一层: 需要精细控制组件复用逻辑、或者实现跨 Widget 树的数据透传。直接操作 Element 的场景非常少见,常见案例:
1. 自定义 Key 控制复用
// 不加 Key:列表顺序变化时 State 会错位
// 加 Key:强制 Flutter 按 key 匹配 Element
ListView(
children: items.map((e) => ItemWidget(key: ValueKey(e.id), item: e)).toList(),
)
2. GlobalKey 跨树访问 State
final key = GlobalKey<FormState>();
Form(key: key, child: ...);
// 在其他地方访问:
key.currentState?.validate();
GlobalKey 的底层实现就是在 Element 树上注册全局索引。
3. 实现 InheritedWidget
InheritedElement 维护了一张依赖表,当数据变化时精确通知依赖它的 Element 重建,而不是整棵树。Provider、Theme、MediaQuery 都基于此。
class MyInheritedWidget extends InheritedWidget {
final int value;
// ...
@override
bool updateShouldNotify(MyInheritedWidget old) => value != old.value;
}
RenderObject 层
何时动这一层: 需要自定义布局算法或绘制逻辑,现有 Widget 无法满足需求时。
1. 自定义绘制 — CustomPainter
class WavePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.blue;
// 直接操作 Canvas,画任意图形
final path = Path();
path.moveTo(0, size.height * 0.5);
for (double x = 0; x <= size.width; x++) {
path.lineTo(x, size.height * 0.5 + sin(x * 0.05) * 20);
}
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(WavePainter old) => false;
}
2. 自定义布局 — CustomMultiChildLayout / MultiChildRenderObjectWidget
当 Row/Column/Stack 都满足不了你的布局需求时,直接继承 RenderBox:
class RenderRadialLayout extends RenderBox
with ContainerRenderObjectMixin<RenderBox, RadialParentData> {
@override
void performLayout() {
// 把子节点排列成圆形
double angle = 0;
RenderBox? child = firstChild;
while (child != null) {
child.layout(constraints, parentUsesSize: true);
final parentData = child.parentData as RadialParentData;
parentData.offset = Offset(
cos(angle) * radius,
sin(angle) * radius,
);
angle += 2 * pi / childCount;
child = childAfter(child);
}
size = constraints.biggest;
}
@override
void paint(PaintingContext context, Offset offset) {
// 按计算好的 offset 绘制子节点
RenderBox? child = firstChild;
while (child != null) {
final parentData = child.parentData as RadialParentData;
context.paintChild(child, offset + parentData.offset);
child = childAfter(child);
}
}
}
3. RepaintBoundary — 隔离重绘区域
// 把频繁重绘的子树隔离在独立 Layer,避免污染父节点
RepaintBoundary(
child: AnimatedWidget(...),
)
底层原理:RenderRepaintBoundary 会在 Layer Tree 中创建新的 OffsetLayer,让子树的 paint 结果缓存在独立纹理上。
五、常见场景:该动哪一层?
| 场景 | 应该动哪层 | 具体手段 |
|---|---|---|
| 普通 UI 布局、状态管理 | Widget | StatelessWidget / StatefulWidget |
| 列表顺序变化导致状态错乱 | Element | 给 Widget 加 Key |
| 跨组件共享数据 | Element | InheritedWidget / Provider |
| 跨树直接操作 State | Element | GlobalKey |
| 自定义图形、动画、粒子效果 | RenderObject | CustomPainter |
| 现有布局 Widget 无法满足排列需求 | RenderObject | 继承 RenderBox + MultiChildRenderObjectWidget |
| 局部频繁重绘性能优化 | RenderObject | RepaintBoundary |
| 需要在 layout 阶段拿到子节点尺寸 | RenderObject | LayoutBuilder 或自定义 RenderBox |
| 实现类似 Sliver 的懒加载布局 | RenderObject | 继承 RenderSliver |
六、总结
- Widget 树:轻量、不可变的配置描述,随时重建没有压力,绝大多数业务代码都在这里
- Element 树:运行时实例,管理生命周期和复用逻辑,通常不直接操作,但 Key 和 InheritedWidget 的工作都在这一层
- RenderObject 树:真正的布局和绘制,性能敏感,只有在需要自定义渲染行为时才下到这一层
理解三棵树的分工,核心是记住一句话:Widget 描述想要什么,Element 决定怎么复用,RenderObject 负责真正干活。