再说 Flutter 的渲染机制之前还是先说一下 Flutter 的三棵树。
一. 三棵树
三棵树分别是 Widget 树、Element 树 和 RenderObject 树。
1. Widget 树
-
Widget是声明式的UI配置。它们是非常轻量级的、不可变的类,只包含最终的UI样子的信息(如颜色、字体、尺寸、布局方向等)。 -
核心特点:
-
不可变 :一旦创建就不能修改。要改变UI,你必须创建一个新的 Widget 实例。
-
轻量级 :创建和销毁的成本很低。
-
组合性 :简单的 Widget 可以组合成复杂的 Widget。
-
-
职责:描述UI元素的配置。
2. Element 树
-
Element是Widget的实例化对象,它代表了屏幕上某个特定位置的Widget。它是Widget和RenderObject之间的 “粘合剂” 或管理者。 -
核心特点:
-
可变 :
Element持有状态,并且会更新。 -
生命周期:它负责管理
Widget的生命周期(initState,dispose等)。 -
高效的关键:它通过比较新旧
Widget来决定是否需要更新底层的RenderObject(Diff 算法)。
-
-
职责:管理
UI元素的生命周期和持有对RenderObject和Widget的引用。
工作流程
当 Flutter 构建 UI 时,它会遍历 Widget 树,并为每个 Widget 创建一个对应的 Element。Element 会检查是否已经有一个 RenderObject 与之关联:
-
如果没有,它就调用
Widget.createRenderObject()来创建一个。 -
如果已经有
RenderObject,它会比较新旧 Widget(Widget.canUpdate),如果配置变了,就调用RenderObject.update()来更新它, 如果key或者runtimeType变了就重新创建一个新的RenderObject。
3. RenderObject 树
-
RenderObject是真正负责布局、绘制和命中测试的“实干家”。它们是非常重量级的对象,包含了所有的布局逻辑和几何信息。 -
核心特点:
-
重量级 :布局和绘制计算非常昂贵。
-
复用性 :除非绝对必要(
Key或者runtimeType发生改变),否则会被复用,而不是重新创建。
-
-
职责:
-
布局 :根据父级给的 约束 ,决定自己的 大小 和 位置 。
-
绘制 :将自己绘制到屏幕上(通过
CanvasAPI)。 -
合成:将自己放入不同的图层以供高效渲染。
-
4. 三棵树如何协同工作?(以 setState 为例)
-
触发变化:你调用
setState(() { _counter++; }),标记state为“脏”(dirty)。 -
重建 Widget 树:
Framework调用build()方法,生成一棵新的 Widget 树。 -
Diff 过程 :
Flutter不会推倒重来。它会将新的Widget树与旧的Element树进行对比。-
Flutter遍历Element树,对于每个位置的Element,用与之对应的新 Widget 和旧 Widget 进行比较。 -
如果新旧
Widget的runtimeType和key相同,Flutter就认为这是同一个逻辑组件,只是配置更新了。它会复用现有的Element和RenderObject,并让Element用新的Widget配置去更新(update)RenderObject。 -
如果
runtimeType或key不同,Flutter就会销毁旧的Element和RenderObject,然后根据新的Widget创建新的Element和RenderObject。
-
-
更新
RenderObject:被标记为需要更新的RenderObject会执行布局和绘制计算。 -
光栅化:更新后的渲染信息被发送到
Skia图形引擎,最终绘制到屏幕上。
5. 为什么需要三棵树?—— 为了性能
这种设计的核心优势在于:将轻量级的、不可变的配置(Widget)与重量级的、可变的渲染对象(RenderObject)分离开。
-
高效更新:通过
Diff算法和Element树的协调,可以最小化对RenderObject树的操作。只有真正需要变化的部分才会触发昂贵的布局和绘制计算。 -
声明式UI的性能:它让
Flutter在享受声明式UI开发便利的同时,获得了接近原生命令式UI的高性能。
总结:Widget 描述配置,Element 管理生命周期协调 Widget 和 RenderObject,RenderObject 负责实际的渲染工作。三棵树各司其职,共同构成了 Flutter 高效渲染的基石。
如图所示:
二. 渲染过程
1. Build(构建)阶段
-
输入:声明式代码(
Widget树)。 -
输出:创建或更新 Element Tree。
-
过程:
-
当
build()方法被调用时,Flutter会根据你的代码生成新的Widget树。 -
Flutter会将新的Widget树与旧的Widget Tree进行对比(Diff) 。 -
Element 是 Widget 的实例化对象,是连接“善变”的
Widget和“稳定”的RenderObject的“粘合剂”。
-
2. Layout(布局)阶段
-
输入:来自父级的约束 (例如,“你的最大宽度是屏幕宽度”)。
-
输出:每个
RenderObject的大小(Size)和位置(Position)。 -
过程:
- 这是一个自上而下的传递约束、自下而上的确定大小的过程。
3. Paint(绘制)阶段
-
输入:已经确定好大小和位置的
RenderObject。 -
输出:一系列的绘制指令 ,最终由
Skia图形引擎转化为像素。 -
过程:
-
每个
RenderObject都有一个paint方法,它接收一个Canvas对象。 -
RenderObject通过在Canvas上调用 API(如drawRect,drawText,drawImage)来记录如何绘制自己。 -
这些指令被优化后,交给底层的
Skia图形引擎进行光栅化 ,即转化为实际的像素点,显示在屏幕上。
-
过程如下图所示:
三. 在构建 UI 时,三棵树是怎么变化的,谁是变化的,谁是不变的?
-
Widget 不变
-
Widget 本身是不可变的 。它们只是配置信息。当状态改变时,不是修改旧的
Widget,而是创建一个新的Widget对象。 -
原因: 因为创建轻量级的、不可变的
Widget对象开销非常小。这使得上图“核心差异化过程”中的Diff对比过程非常快。
-
-
Element 可变可不变
-
如果
Widget的key或者runtimeType没有变化,Element复用之前的Element只是更新配置即可。 -
如果
Widget的key或者runtimeType发生变化,旧的Element会被卸载,会创建一个新的Element树。 -
比如以下代码
Element会被重新创建
-
// 初始状态
Container(width: 100, height: 100) -> 创建 RenderDecoratedBox
// 状态改变后
Text('Hello') -> 创建 RenderParagraph
// 初始状态
Container(key:key1,width: 100, height: 100)
// 状态改变后
Container(key:key2,width: 100, height: 100)
-
RenderObject 可变可不变
-
RenderObject是稳定且可复用的。除非 Widget 的runtimeType发生变化,否则 Flutter 在 Diff 后会复用现有的 RenderObject,只是用新 Widget 的配置去更新它(例如,更新颜色、大小等属性)。 -
Widget 的
key发生变化对RenderObject带来的影响又分为两种-
- 发生根本变化 由key1->key2,
RenderObject会被重建。
- 发生根本变化 由key1->key2,
-
- 当 Widget 类型相同但 key 不同时(常见于列表),Flutter 会匹配 key 来对 Element 和 RenderObject 进行重新排序,而不是销毁和重建。(列表复用)
-
-
原因:
RenderObject负责的布局和绘制计算是昂贵的。复用它们意味着避免了昂贵的重新计算。布局和绘制结果也经常被缓存。
-
-
状态(State)是变的
State对象是 (可变的) 。setState()方法的作用就是标记这个State对象为“脏”(dirty),从而在下一帧触发build()方法,用新的状态数据来创建新的Widget。状态是变化的源泉。
整体过程图解
Element 和 RenderObject 变化详细图解
四. Element的职责详解
如果说 Widget 是“设计图纸”,那么 Element 就是根据设计图纸施工并负责维护的“项目经理”。它确保了 Widget 所描述的UI组件能够正确地被创建、更新和销毁。
1. 创建与挂载
当一个新的 Widget 第一次被插入到树中时,Framework 会为其创建一个对应的 Element,并调用其 mount方法。
-
mount方法:这是生命周期的起点。在这个方法中,Element会:-
将自身添加到
Element树中的正确位置。 -
调用
Widget.createRenderObject()来创建并关联对应的 RenderObject(如果这个 Widget 是一个RenderObjectWidget,比如Text或Image)。 -
如果这个
Widget是StatefulWidget,Element 会负责创建并关联其State对象,这是最关键的一步! -
然后,它会调用
State的initState()方法。这是执行一次性初始化操作的理想位置(例如,订阅一个 Stream,初始化复杂的变量)。
-
dart
// 伪代码示意
void mount() {
super.mount();
_renderObject = widget.createRenderObject(this); // 创建RenderObject
attachRenderObject(_renderObject); // 将RenderObject加入渲染树
if (isStatefulWidget) {
_state = widget.createState(); // 创建State对象
_state._element = this; // 将Element赋值给State
_state._widget = widget; // 将Widget赋值给State
_state.initState(); // 调用initState生命周期
}
}
2. 更新
当父组件重建并提供了一个新的 Widget(与旧 Widget 的 runtimeType 和 key 相同)时,Element 需要更新。
-
update方法:Element会持有对新的Widget的引用,并调用:-
如果 Widget 是
StatefulWidget,它会调用State的didUpdateWidget(oldWidget)方法。你可以在这里比较新旧Widget的配置,并决定是否需要做一些响应操作(例如,如果某个配置变了,就取消旧的订阅并开启新的订阅)。 -
调用
RenderObject.update(newWidget)或用新的配置去更新关联的RenderObject(例如,更新文本颜色或大小)。
-
dart
// 伪代码示意
void update(newWidget) {
super.update(newWidget);
if (isStatefulWidget) {
_state.didUpdateWidget(oldWidget); // 通知State Widget更新了
}
_renderObject.update(newWidget); // 更新RenderObject
_widget = newWidget; // 更新持有的Widget引用
}
3. 销毁与卸载
当 Widget 被从树中永久移除时,Element 负责清理工作。
-
unmount方法:这是生命周期的终点。在这个方法中,Element会:-
调用
State的dispose()方法(如果存在)。这是执行清理操作的唯一机会,例如取消所有订阅、停止动画控制器、释放资源。一旦dispose被调用,State对象就永远不会再被重建。 -
销毁关联的
RenderObject。调用RenderObject.dispose()来释放渲染相关的资源。 -
将自身从
Element树中分离。
-
dart
// 伪代码示意
void unmount() {
if (isStatefulWidget) {
_state.dispose(); // 调用dispose进行清理
}
_renderObject.dispose(); // 销毁RenderObject
super.unmount(); // 从树中脱离
}
具体过程如下图所示: