Flutter 中的 Widget, Element, RenderObject
在 Flutter 中,我们写的各种 Widget,如 Text, Padding, Image 等是如何转化成像素显示在屏幕上的呢?
今天我们来讲讲 Flutter 中的 Widget, Element, RenderObject,也就是我们常说的三棵树,以及 Flutter 是如何将 Widgets 渲染至屏幕上的。
Widget
Describes the configuration for an Element .
A widget is an immutable description of part of a user interface (link).
Widget 是描述 UI 元素的配置信息,可以理解为前端开发中的控件或者组件,是 Flutter 最基本的概念。
所谓元素配置信息就是这些 Widget 类所接受的参数,比如对于 Text 来讲,文本内容、样式、对齐方式都是它的配置信息。
Text(
"Hello wolrd",
style: TextStyle(
fontSize: 16,
color: Colors.red,
),
textAlign: TextAlign.center,
)
我们在代码中写的 Text, Row, Padding 等都是 Flutter 内置的 Widget,我们平时就是用这些内置的 Widget 来“搭建”页面的,这些 Widget 就构成了一棵 Widget 树。
Flutter 中的 Widget 最终都是继承自 Widget 接口。

从源码中可以看到有 @immutable 注解,表明 Widget 是不可变的,当配置信息发生变化时,Flutter 会选择重新构建 Widget 树方式来进行数据更新。
那么 Flutter 是如何实现动态化的呢?我们接着来看 Element 和 RenderObject。
Element
An instantiation of a Widget at a particular location in the tree.
Element 是 Widget 的一个实例化对象,承载中视图构建的上下文(context)数据,是连接配置信息到最终渲染的桥梁。这些 Element 实例是可变的,可以与 RenderObject 进行通信。

RenderObject
An object in the render tree.
顾名思义,RenderObject 主要负责实现视图渲染的对象。渲染时所涉及到的尺寸、布局、约束条件等都是由它来控制的。因此,RenderObject 实例化成本非常昂贵。和 Widget, Element 的一样,Flutter 程序中也有一棵RenderObject 树。
注:RenderObject 是个总称,不同的 Widget 会有不同类型的 RenderObject,如 RenderOpacity, RenderParagraph, RenderImage 等,它们都继承自 RenderObject。
为了方便理解,我们可以与前端中的框架做个简单的类比。
- Vue: Template → Virtual DOM → DOM
- React: JSX → Virtual DOM → DOM
- React Native: JSX → Virtual DOM → Android/iOS 原生控件
- Flutter: Widget → Element → RenderObject
渲染过程
接下来,我们来探究一下,Flutter 是如何使用 Widget, Element, RenderObject 来完成渲染流程的。
我们以一个例子来说明。
void main() {
runApp(
RichText(
text: const TextSpan(
text: 'Hello World',
style: TextStyle(color: Colors.red),
),
textDirection: TextDirection.ltr,
),
);
}
当执行到 runApp 时
void runApp(Widget app) {
final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
assert(binding.debugCheckZone('runApp'));
binding
..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
..scheduleWarmUpFrame();
}
首先要做的是将 RichText 添加至 Widget 树中。

然后,Flutter 会根据 Widget 树会创建 Element 树, 用来管理 Widget 树生命周期和状态。
abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {
/// Initializes fields for subclasses.
const MultiChildRenderObjectWidget({ super.key, this.children = const <Widget>[] });
final List<Widget> children;
@override
MultiChildRenderObjectElement createElement() => MultiChildRenderObjectElement(this);
}
现在已经存在 Widget 和 Element 两棵树了。

接着,Element 会在挂载的时候创建 RenderObject。
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
// ...
_renderObject = (widget as RenderObjectWidget).createRenderObject(this);
// ...
attachRenderObject(newSlot);
super.performRebuild(); // clears the "dirty" flag
}
当我们书写 RichText(...) 时,会将里面的配置信息(各种参数)传入至 RenderObject(这里是 RenderParagraph) 中
@override
RenderParagraph createRenderObject(BuildContext context) {
assert(textDirection != null || debugCheckHasDirectionality(context));
return RenderParagraph(text,
textAlign: textAlign,
textDirection: textDirection ?? Directionality.of(context),
softWrap: softWrap,
overflow: overflow,
textScaler: textScaler,
maxLines: maxLines,
strutStyle: strutStyle,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior,
locale: locale ?? Localizations.maybeLocaleOf(context),
registrar: selectionRegistrar,
selectionColor: selectionColor,
);
}
现在,Widget, Element, RenderObject 就都有了。

最终,RenderParagraph 会根据 RichText 中所声明的各种配置信息进行绘制,最终显示在屏幕上。
我们来总结一下整个渲染过程:
- 首先,通过
Widget树生成对应的Element树; - 然后,
Element挂载时创建RenderObject,并关联至Element.renderObject 属性上; - 最后,
RenderObject树完成最终的渲染。
可以看出,Element 是 Widget 和 RenderObject 的桥梁,那可不可以不要 Element 呢?
为了理解这个,我们来看看当 Widget 发生变化时的情况。
void main() {
runApp(
RichText(
text: const TextSpan(
text: 'Hello World',
style: TextStyle(color: Colors.red),
),
textDirection: TextDirection.ltr,
),
);
runApp(
RichText(
text: const TextSpan(
text: 'Hello Shanghai',
style: TextStyle(color: Colors.red),
),
textDirection: TextDirection.ltr,
),
);
}
我们连续执行两遍 runApp 函数来模拟更新的情况。
第一次执行 runApp 的时候会创建三棵树,第二次执行的时候,发现 text 发生了变化

这时,canUpdate 会执行。
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
这个方法用来决定是重新创建 Element,还是复用之前的 Element,复用时执行 updateRenderObject 方法。
@override
void updateRenderObject(BuildContext context, RenderParagraph renderObject) {
assert(textDirection != null || debugCheckHasDirectionality(context));
renderObject
..text = text
..textAlign = textAlign
..textDirection = textDirection ?? Directionality.of(context)
..softWrap = softWrap
..overflow = overflow
..textScaler = textScaler
..maxLines = maxLines
..strutStyle = strutStyle
..textWidthBasis = textWidthBasis
..textHeightBehavior = textHeightBehavior
..locale = locale ?? Localizations.maybeLocaleOf(context)
..registrar = selectionRegistrar
..selectionColor = selectionColor;
}
可以看出,这与创建 RenderObject 非常相似,只是复用了 RenderObject,仅更新其值,而没有重新创建新的 RenderObject(RenderParagraph)。

最后,我们看到,当配置信息发生变化时,真正用来渲染的 RenderObject 并没有变化。
前面说到 Widget 具有不可变性,体现在当其发生变化时,Flutter 会直接丢弃原有的 Widget 树,重新构建一棵新的 Widget树,因为只是一份轻量的数据结构,并不参与最终的渲染,重建的成本很低。这也是为什么 Widget 中的属性必须是 final 的原因。
而 Element 却是可变的,实际上,Element 树这一层就是将 Widget 树中的变化做了抽象,将真正修改的部分 patch 到真实的 RenderObject 树中,最大程度了降低对 RenderOject 树的修改,提高渲染效率,而不是销毁整个渲染树重建。这就是 Element 存在的意义。
接下来,我们用个例子来验证一下我们的结论。

在这个例子中,当点击按钮时,图片和文本都会发生变化,两张图片的尺寸是不一样的。如果上述分析是正确的,当屏幕内容发生变化时,Flutter 会尽可能地复用 RenderObject,也就是说变化前后 Image 和 Text对应 RenderObject 的 ID 应该保持不变。我们用 Flutter 开发者工具来查看 Widget 树和它的详情。

RenderObject ID for dart-logo image (ID = RenderImage#14fe4)

RenderObject ID for flutter-logo image (ID = RenderImage#14fe4)
我们可以看到,用于渲染图片的 RenderObject (RenderImage) 前后的 ID 保持不变,这就意味着两张图片都是由同一个 RenderObject 来绘制的,说明元素得到了复用。

RenderObject ID for the text (ID = RenderParagraph#19921)

RenderObject ID for the text (ID = RenderParagraph#19921)
同理也发生在 Text,尽管文本发生了变化,但是 RenderObject 得到了复用,RenderParagraph 的 ID 保持不变。
正是这种复用的能力,使得我们 Widget 树不断变化时能够快速地反映至屏幕上, 这也是 Flutter 表现出色的原因之一。
转载自:https://juejin.cn/post/7368302136687755274