在 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
表现出色的原因之一。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。