(一)什么是 Widget
在官方的架构图中,Widget 是整个视图描述的基础。Widget 是 Flutter 功能的抽象描述,是视图的配置信息,同样也是数据的映射,是 Flutter 开发框架中最基本的概念。前端框架中常见的名字,比如视图(View)、视图控制器(View Controller)、活动(Activity)、应用(Application)、布局(Layout)等,在 Flutter 中都是 Widget。
事实上,Flutter 的核心设计思想便是“一切皆 Widget”。所以,学习 Flutter,首先得从学会使用 Widget 开始。
(二)Widget 渲染过程
在开发中,我们往往会关注的一个问题:如何结构化地组织视图数据,提供给渲染引擎,最终完成界面显示。
通常情况下,无一例外,都会用到视图树(View Tree)的概念。而 Flutter 将视图树的概念进行了扩展,把视图数据的组织和渲染抽象为三部分,即 Widget,Element 和 RenderObject。
三者的关系如下:
(1)Widget
Widget 是 Flutter 中对视图的一种结构化描述,它可以看作是前端中的“控件”或“组件”。Widget 是控件实现的基本逻辑单位,里面存储的是有关视图渲染的配置信息,包括布局、渲染属性、时间响应信息等。
在页面渲染上,Flutter 将 Widget 设计成不可变的,所以当视图渲染的配置信息发生变化时,Flutter 会选择重建 Widget 树的方式进行数据更新,以数据驱动 UI 构建的方式简单高效。
缺点就是涉及到大量对象的销毁和重建,所以会对垃圾回收造成压力。不过,Widget 本身并不涉及实际渲染位图,所以它只是一份轻量级的数据结构,重建的成本很低。
另外,由于 Widget 的不可变性,可以以较低成本进行渲染节点复用,因此在一个真实的渲染树种可能存在不同 Widget 对应同一个渲染节点的情况,这无疑又降低了重建 UI 的成本。
(2)Element
Element 是 Widget 的一个实例化对象,它承载了视图构建的上下文数据,是连接结构化的配置信息到完成最终渲染的桥梁。
Flutter 渲染过程,可以分为三步:
- 首先,通过 Widget 树生成对应的 Element 树;
- 然后,创建相应的 ReaderObject 并关联到 Element .renderObject 属性上;
- 最后,构建成 RenderObject 树,以完成最终的渲染。
可以看到,Element 同时持有 Widget 和 RenderObject。最终负责渲染工作的只有 RenderObject。那么,为什么要增加 Element 树这个中间层呢?而不是由 Widget 直接命令 RenderObject 呢?
答案是,可以,但是这样做会极大地增加渲染带来的性能损耗。
因为 Widget 具有不可变性,但 Element 却是可变的。实际上,Element 树这一层将 Widget 树的变化(类似 React 虚拟 DOM diff)做了抽象,可以只将真正需要修改的部分同步到真实的 RenderObject 树种,最大程度降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个渲染视图树重建。
这就是 Element 树存在的意义。
(3)RenderObject
RenderObject 主要负责实现视图渲染的对象。
Flutter 通过控件树(Widget 树)中的每个控件(Widget)创建不同类型的渲染对象,组成渲染对象树。
而渲染对象树在 Flutter 的展示过程分为四个阶段:布局、绘制、合成和渲染。其中,布局和绘制在 RenderObject 中完成,Flutter 采用深度优先机制遍历渲染对象树,确定树中各个对象的位置和尺寸,并把它们绘制到不同的图层上。绘制完毕后,合成和渲染的工作则交给 Skia 搞定。
(二)RenderObjectWidget
我们知道,在 Flutter 中有 StatelessWidget 和 StatefulWidget 两种用来组装控件的容器,但并不负责组件最后的布局和绘制。在 Flutter 中,布局和绘制工作实际上是在 Widget 的另一个子类 RenderObjectWidget 内完成的。
通过查看 RenderObjectWidget 的源码,来分析 Element 和 RenderObject 是如何完成图形渲染工作的。
abstract class RenderObjectWidget extends Widget {
@override
RenderObjectElement createElement();
@protected
RenderObject createRenderObject(BuildContext context);
@protected
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }
...
}
RenderObjectWidget 是一个抽象类。我们通过源码可以看到,这个类中同时拥有创建 Element、RenderObject,以及更新 RenderObject 的方法。
但实际上,RenderObjectWidget 本身并不负责这些对象的创建和更新。
对于 Element 的创建,Flutter 会在遍历 Widget 树时,调用 creatElement 去同步 Widget 自身配置,从而生成对应节点的 Element 对象。而对于 RenderObject 的创建与更新,其实是在 RenderObjectElement 类中完成的。
abstract class RenderObjectElement extends Element {
RenderObject _renderObject;
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
_dirty = false;
}
@override
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
...
}
在 Element 创建完毕后,Flutter 会调用 Element 的 mount 方法。在这个方法中,会完成与之关联的 RenderObject 对象的创建,以及与渲染树的插入工作,插入到渲染树后的 Element 就可以显示到屏幕中。
如果 Widget 的配置数据发生变化,那么持有该 Widget 的 Element 节点也会被标记为 dirty。在下一个周期的绘制时,Flutter 就会触发 Element 树的更新,并使用最新的 Widget 数据更新自身以及关联的 RenderObject 对象,接下来便会进入 Layout 和 Paint 的流程。而真正的绘制和布局过程,则完全交由 RenderObject 完成:
abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
...
void layout(Constraints constraints, { bool parentUsesSize = false }) {...}
void paint(PaintingContext context, Offset offset) { }
}
布局和绘制完成后,接下来的事情就交给 Skia 了。在 VSync 信号同步时直接从渲染树合成 Bitmap,然后提交给 GPU。
(三)案例
根据下面的案例,了解说明 Widget、Element 与 RenderObject 在渲染过程中的关系。
一个 Row 容器放置了 4 个子 Widget,左边是 Image,右边是一个 Column 容器下排布的两个 Text。
那么,在 Flutter 遍历完 Widget 树,创建了各个子 Widget 对应的 Element 的同时,也创建了与之关联的、负责实际布局和绘制的 RenderObject。
总结
主要学习了 Flutter 中视图数据的组织和渲染抽象的三个核心概念:Widget、Element 和 RenderObject。
Widget 是 Flutter 世界里对视图的一种结构化描述,里面储存的是有关视图渲染的配置信息;
Element 是 Widget 的一个实例化对象,讲 Widget 树的变化做了抽象,能够做到只将真正需要修改的部分同步到真是的 RenderObject 树中,最大程度地优化了从结构化的配置信息到完成最终渲染的过程;
RenderObject 是负责实现视图的最终呈现,通过布局、绘制完成界面的展示。