Flutter 三棵树:Widget、Element、RenderObject 详解
为什么是三棵树?
初学 Flutter 会觉得奇怪:为什么不直接用 Widget 渲染?因为 Widget 是不可变描述,频繁创建销毁的代价应该很低,而真正昂贵的布局计算和绘制只应该在必要时发生。三棵树的设计就是为了分离这两件事:
Widget Tree Element Tree RenderObject Tree
(轻量描述) → (持久化实例) → (昂贵的布局/绘制)
每次 rebuild 跨 rebuild 只在尺寸/样式
全部重建 继续复用 变化时更新
Widget Tree:配置蓝图
Widget 是不可变的 Dart 对象,仅仅是「配置描述」。就像 HTML 标签,本身不 paint 任何东西。
// Widget 的核心特征:@immutable,所有字段 final
@immutable
class MyWidget extends StatelessWidget {
final String title;
const MyWidget({required this.title}); // const 构造
@override
Widget build(BuildContext context) {
// build 返回新的 Widget,描述子树结构
return Text(title);
}
}
每次 setState 或 build 触发,整个子树的 Widget 对象都会重新创建。这是设计意图,不是性能问题——Widget 对象极轻量。
Element Tree:持久化中间层
Element 是 Widget 的「实例化对象」,在 Widget Tree 和 RenderObject Tree 之间担当协调者。
关键特性:Element 跨 rebuild 持久存在,不随 Widget 重建。
首次渲染:
Widget → 创建 Element → 创建 RenderObject
rebuild 后:
新 Widget → 找到对应的旧 Element → 更新 Element(不新建)
↓
Element 决定是否更新 RenderObject
Element 的职责
- 维持标识:通过 Widget 的
key和runtimeType判断 Widget 是否「同类」 - 持有 State:
StatefulElement持有State对象,所以State能跨 rebuild 存活 - 管理子树:负责调用子 Widget 的
build,递归构建树
Key 对 Element 复用的影响
// 没有 Key:Dart 按位置匹配 Widget 和 Element
Column(
children: [
Container(color: Colors.red), // 位置 0 → 复用位置 0 的 Element
Container(color: Colors.blue), // 位置 1 → 复用位置 1 的 Element
],
)
// 有 Key:按 Key 匹配,即使位置改变也能复用对应 Element
Column(
children: [
Container(key: const ValueKey('red'), color: Colors.red),
Container(key: const ValueKey('blue'), color: Colors.blue),
],
)
// 交换顺序后,每个 Container 仍复用正确的 Element(及其状态)
RenderObject Tree:布局与绘制
RenderObject 是真正「知道自己多大、画在哪里」的对象。创建和更新代价相对昂贵,所以尽量复用。
布局协议(Constraints → Size)
Flutter 的布局模型是约束向下传递,尺寸向上返回:
父节点下发 Constraints(最大/最小宽高范围)
↓
子节点收到 Constraints,计算自己的 Size
↓
父节点收到子 Size,决定子节点的 Offset(位置)
↓
递归完成,根节点知道了整棵树的布局
这个协议保证了单次遍历就能完成布局(O(N)),而不是像 Android View 系统需要多次 measure。
Paint 与合成
布局完成后,每个 RenderObject 调用 paint 方法,把自己画到 Canvas 上。某些 RenderObject(如有动画的)会请求一个独立的 Layer,这样下次只需重绘这一层,而不影响其他层。
三棵树的协作流程
以 setState() 触发 rebuild 为例:
① setState() 被调用
↓
② StatefulElement 标记为 dirty(需要重建)
↓
③ 下一帧,Flutter 引擎触发 drawFrame
↓
④ 遍历 dirty Elements,调用对应 Widget 的 build()
↓
⑤ build() 返回新的 Widget 子树
↓
⑥ Element 对比新旧 Widget:
- runtimeType 相同 + key 相同 → 复用 Element,更新配置
- 不同 → 卸载旧 Element,创建新 Element(及新 RenderObject)
↓
⑦ 更新了的 RenderObject 触发 layout + paint
↓
⑧ Skia/Impeller 合成图层,上屏
StatelessWidget vs StatefulWidget 的本质区别
| StatelessWidget | StatefulWidget | |
|---|---|---|
| 对应 Element | StatelessElement |
StatefulElement |
| 持有 State | ❌ | ✅(State 对象持久存活) |
| rebuild 触发 | 父 Widget rebuild | setState() 或父 rebuild |
| 何时用 | 纯展示,无内部状态 | 有需要跨 build 存活的状态 |
StatefulElement 在创建时调用 widget.createState() 创建 State 对象,并持有它。之后无论 Widget 如何 rebuild,State 对象都是同一个,这就是「状态」能存活的原因。
实际意义:如何优化
理解三棵树,能帮你做出正确的性能决策:
// ❌ 每次 rebuild 都重建整个 children 列表
Widget build(BuildContext context) {
return Column(
children: [
ExpensiveWidget(), // 每次都 rebuild,尽管内容没变
Text(_counter.toString()),
],
);
}
// ✅ 用 const 标记不变的 Widget,Element 的 update 会跳过 RenderObject 更新
Widget build(BuildContext context) {
return Column(
children: [
const ExpensiveWidget(), // Flutter 识别为同一 const 对象,跳过
Text(_counter.toString()),
],
);
}
另一个优化方向:缩小 rebuild 范围。
// ❌ 整个 MyPage 都会因为 _counter 变化而 rebuild
class MyPage extends StatefulWidget { ... }
class _MyPageState extends State<MyPage> {
int _counter = 0;
Widget build(BuildContext context) {
return Column(children: [
HeavyListView(), // 没必要 rebuild!
Text('$_counter'),
]);
}
}
// ✅ 把状态下沉,只有 CounterText rebuild
class CounterText extends StatefulWidget { ... }
// HeavyListView 不受影响