学 Flutter 不理解 Widget/Element/Render 三棵树?啥也不是!
一、导语
Hi,大家好,这里是承香墨影!
Flutter 是 Google 发布的跨平台 UI 框架,而其中与 UI 相关的,最重要的就是 Widget & Element & RenderObject 这三棵树。
初入门的时候,会以为三棵树的节点,都是一一对应的,后来才知道并非如此。
我们在代码中写的 Widget,在运行时,会依据使用的属性「膨胀」出多个子节点结构,而他们才是真实的与 Element 对应的结构,而到了 RenderObject 又被收敛了,只有绘制类的 RenderObjectWidget 才会最终生成 RenderObject 节点,组成 RenderObject Tree,渲染到 UI 上显式。
而这一些的细节 Flutter Framework 都帮我们实现了,我们只需要跟着代码的流程去了解即可。
二、引言
树是学习数据结构必学的一种结构,Flutter 采用了这种数据结构作为 UI 体系构建的核心。如果你已经有一定 Flutter 开发经验,一定接触到过这个概念,我们可以在各类文章中看到类似的介绍(图片来自文章《Flutter 的三棵树渲染机制和原理》)。
当看到这个图,可能我们也默认为,理解了这个知识点,但实际上这三颗树真的长这样么?他们究竟是如何形成这样的结构?
本文将会从一个简单的例子,追溯源码一一揭晓这个过程。
先来看一个极简的 demo。
Container 设置了绿色背景,一个 Cloumn 下套了四个子节点。
2.1 Widget 树
首先我们来看看 Widget 树,实际上在代码运行过程中,没有明确的 Widget 树的概念,这棵树更多的是我们在开发过程中,对 Flutter 嵌套结构的描述。
而根据代码我们可以认为 Widget 树长这个样子。
2.2 Element 树的构建过程
Element 树可以说是最重要的结构,它桥接了我们开发中的 Widget,与实际渲染的 RenderObject 对象。
根据网上大多数文章的描述,可能我们会认为这个例子中 Element 树与 Widget 树一样。
如果你也是这么认为的,那么恭喜你,回答错误!
这棵树究竟该长什么样,我们一步步分析。
纵观 Flutter 的 UI 体系,Widget 可以分为 3 类:组合类(紫色)、代理类(红色)、绘制类(黄色)。
Flutter 中比较熟悉的 StatelessWidget & StatefulWidget,都属于组合类的 Widget,实际上他们并不负责绘制,仅仅起到组合子 Widget 的作用。
而我们在屏幕上看到的 UI,最终都会通过绘制类的 WidgetRenderObjectWidget
实现。
RenderObjectWidget 中有个 createRenderObject()
方法,会生成实际渲染的 RenderObject 对象。
在我们的例子中,Container 和 Text 就是组合类的 Widget,这类 Widget 我们可以通过查看他的 build()
方法,梳理他的嵌套关系。
我们的例子中,Container 和 Text 其实都是组合类 Widget。
查看 Container 的 build()
方法,从源码得知,Container 嵌套的层级与我们设置的属性有关。
我们为 Container 设置了 child
和 color
属性,对应会给我们的 child
嵌套一个 DecoratedBox(渲染类 Widget)。而 Text 组件内部只是返回了一个 RichText(渲染类 Widget)。
得到以下结构(发现了没,叶子节点一定是渲染类的 Widget,因为这样才会被渲染到屏幕上)。
墨影说(id:cxmyDev):大家不要强记这个结构,实际上在 1.22.x 之后的 Flutter 版本中,Container 的 Color 属性,会被封装为 ColoredBox 而不是 DecoratedBox,拆分的更细了。
虽然实现细节在变,唯一不变的是 Widget 构造的思想,我们代码中的 Widget,与真实的运行时 Widget,并不一样。组合类型的 Widget 会在
build()
过程中,通过包装的方式,创建合适的 Widget,这也符合单一职责的设计思想。
那我们实际的 Element 树,会长什么这个样子么?
这已经快接近真相了。但不太准确,因为实际上并不存在 Container 类型的 Element,所以我们看看 Element 对象究竟有哪些类型。
这是 Element 的结构关系。其中 ComponentElement 对应组合类 Widget(Stateless/Stateful 以及所有他们的子类 Container、Text),ProxyElement 对应代理类 Widget,而 RenderObjectElement 则对应渲染类的 Widget(例如 RichText、Column)。
所以更加准确的 Element 树结构应该是这个样子:
为什么还差一丢丢,看以下分析。
这棵树是如何形成的?核心的方法在他们共同的祖先 Element.mount()
方法中(代码已尽量省略无关的逻辑)。
/// Add this element to the tree in the given slot of the given parent.
///
/// The framework calls this function when a newly created element is added to
/// the tree for the first time. Use this method to initialize state that
/// depends on having a parent. State that is independent of the parent can
/// more easily be initialized in the constructor.
///
/// This method transitions the element from the "initial" lifecycle state to
/// the "active" lifecycle state.
@mustCallSuper
void mount(Element parent, dynamic newSlot) {
_parent = parent;
_slot = newSlot;
_depth = _parent != null ? _parent.depth + 1 : 1;
_active = true;
if (parent != null) // Only assign ownership if the parent is non-null
_owner = parent.owner;
_updateInheritance();
}
上面的注释告诉了我们,这个方法的作用,是将当前的 element 对象添加到树中父节点的 solt 位置上。
方法里,我们看到他将当前 Element 对象的 _parent = parent,所以这里其实是将子节点的 _parent 属性,指向与父节点,并且深度 +1。
而对于组合类的 Widget:Stateful/Stateless 重写了 mount()
,还会调到下面的方法 performRebuild()
。
@override
void performRebuild() {
Widget built;
built = build();
_child = updateChild(_child, built, slot);
}
如果这棵树是第一次构建的情况下,会走 inflateWidget()
逻辑生成子节点,并且调用子节点的 mount()
,将其插入树中(因为组合类的 Widget 中会嵌套多层结构,但是 build()
最终只会返回一个 child,将 child 插入树中)。
Element inflateWidget(Widget newWidget, dynamic newSlot) {
final Element newChild = newWidget.createElement();
//将子节点插入到树中
newChild.mount(this, newSlot);
return newChild;
}
RenderObjectElement 类似,我们放到下面解析。
所以这里更准确的 Element 树如下
好家伙,原来只是多了一些节点间的引用关系,而且中间那一层关系怎么和以前不太一样了?(自行查看 Cloumn 对应 Element 的 mount()
)
2.3 RenderObejct 树的构建过程
那么 RenderObjectTree 会长什么样子呢?
我们暂且按下不表,先聊点题外话,一开始大家可能会搞混 RenderObjectWidget 和 RenderObject,RenderObjectElement 三个对象之间的关系。
我们简单看一下,首先 RenderObjectWidget 是 Widget(好像一句废话)。例如 SizeBox、Cloumn 等。RenderObjectElement 是这类 Widget 生成的 Element 类型,例如 SizeBox 对应 SingleChildRenderObjectElement(单子节点的 Element),而 RenderObject 才是真正负责绘制的对象,其中包含了 paint()
、layout()
等方法。
接着分析,我们刚才说了整个树核心的逻辑就在 mount()
这儿,所以们查看 RenderObjectElement.mount()
。
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
_dirty = false;
}
这里执行了 super.mount(parent, newSlot)
,即上面的 mount()
流程之后,执行 attachRenderObject(newSlot)
。
@override
void attachRenderObject(dynamic newSlot) {
_slot = newSlot;
//查询当前最近的RenderObject对象
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
//将当前节点的renderobject对象插入到上面找到RenderObjecte下面
_ancestorRenderObjectElement?.insertChildRenderObject(
renderObject, newSlot);
//................../
}
其中 findAncestorRenderObjectElement()
。
RenderObjectElement _findAncestorRenderObjectElement() {
Element ancestor = _parent;
while (ancestor != null && ancestor is! RenderObjectElement)
ancestor = ancestor._parent;
return ancestor;
}
这其实就是一个很简单的死循环。
我们可以看到,循环退出的条件有两个:
-
ancestor 父节点为空; -
父节点是 RenderObjectElement;
说明这个方法,会找到离当前节点最近的一个 RenderObjectElement 对象, 之后执行该对象的 insertChildRenderObject(renderObject, newSlot)
(执行这个方法的是当前节点的第一个 RenderObjectElement 类父节点), 然而这是一个抽象方法,我们查看两个类里面的重写逻辑。
1. SingleChildRenderObjectElement
@override
void insertChildRenderObject(RenderObject child, dynamic slot) {
final RenderObjectWithChildMixin<RenderObject> renderObject = this
.renderObject;
renderObject.child = child;
}
如果找到节点是 SingleChildRenderObjectElement 的的话,过程非常简单,将父 RenderObjectElement 对象的 renderObject
取出,并且让其 child
属性等于我们之前传入的 RenderObject。
注意这里是 RenderObject 的 child
属性,并不是 element 的 child
属性,所以在这里将自己的 RenderObject 挂在了 RenderObject 树上。
以 demo 中的 Column 为例就是下面这个过程:
-
首先 Column 的 element
对象向上查找,发现父节点 DecoratedBox 对应的element
对象就是 RenderObjectElement 的子类 SingleRenderObjectElement( 单子节点); -
之后调用 SingleRenderObjectElement( DecoratedBox 节点)的 insertChildRenderObject(RenderObject child, dynamic slot)
,这里传入的第一个参数是 Cloumn 对应的renderObject
即 RenderFlex。将自己的renderObject
插入 SingleRenderObjectElement( DecoratedBox 节点)的child
属性上;
2. MultiChildRenderObjectWidget
如果找到最近的节点是 MultiChildRenderObjectWidget,则会调用其对应的 renderObject.insert(child, after: slot?.renderObject)
方法。
@override
void insertChildRenderObject(RenderObject child, Element slot) {
final ContainerRenderObjectMixin<RenderObject,
ContainerParentDataMixin<RenderObject>> renderObject = this
.renderObject;
renderObject.insert(child, after: slot?.renderObject);
}
该方法最终调用 ContainerRenderObjectMixin.insertIntoChildList()
。
这个 ContainerRenderObjectMixin 是什么?查看他的注释:
mixin ContainerRenderObjectMixin<ChildType extends RenderObject, ParentDataType extends ContainerParentDataMixin<ChildType>> on RenderObject
-
泛型 mixin,用于渲染带有一组子对象的对象; -
为渲染对象的子类提供一个子模型,该子类具有一个双向链接的子类列表;
发现重点么?是双向链表。
所以我们大概知道了 MultiChildRenderObjectWidget 的子节点,通过双向链表链接,下面的逻辑一定是去构建这个链表。
void _insertIntoChildList(ChildType child, { ChildType after }) {
final ParentDataType childParentData = child.parentData;
_childCount += 1;
if (after == null) {
childParentData.nextSibling = _firstChild;
if (_firstChild != null) {
final ParentDataType _firstChildParentData = _firstChild.parentData;
_firstChildParentData.previousSibling = child;
}
_firstChild = child;
_lastChild ??= child;
} else {
final ParentDataType afterParentData = after.parentData;
if (afterParentData.nextSibling == null) {
childParentData.previousSibling = after;
afterParentData.nextSibling = child;
_lastChild = child;
} else {
}
}
}
其中这个方法有两个参数:
-
child
表示我们当前传入的 renderObject; -
after
表示他的前一个的节点( 所以可能为空);
根据上面的注释,我们知道这个方法有三个作用:
-
在 after 为 null 的时候,将 child 置于第一个节点; -
将 child 节点插入链表的末端; -
将 child 节点插入链表的中间( 已省略代码);
结合例子来看会比较清楚,例子中的 Column 下放了四个 child:
第一个子节点 SizeBox,在向上找到 Column(RenderObjectElement)之后,调用这个方法,这时 after
为空,所以 _firstChild
即 RendenrConstrainedBox(SizeBox 对应的 RenderObject)。
demo 中的第二个子节点是 Text。我们前面提到,Text 是组合类的 widget,所以他不会参与 Render 树的挂载。
参与挂载的是 RichText。RichText 向上查也找到了 Column(RenderObjectElement),之后调用该方法。
这时传入的两个参数就是:
-
child-> RichText 对应的 RenderObject(RenderParagraph); -
after-> SizeBox 对应的 RenderObject(RendenrConstrainedBox);
根据方法的逻辑,执行以下代码:
final ParentDataType afterParentData = after.parentData;
if (afterParentData.nextSibling == null) {
childParentData.previousSibling = after;
afterParentData.nextSibling = child;
_lastChild = child;
}
将当前 child
节点 ParentData 的 previousSibling 指向第一个节点,将第一个节点 ParentData 的 nextSibling 属性指向当前的 child,最后将 _lastChild
指向自身。
所以整个流程执行完,我们会得到这样一个 RenderTree。
三、总结
对于我们这样的一个看似简单的 demo。
在只关注页面内部的的结构下,三棵树分别是这样的结构:
-
源码写的 Widget 在运行时会依据设置的属性,在 build()
方法包装成真实的运行时 WidgetTree; -
运行时 Widget Tree 的每个节点,与 Element Tree 一一对应; -
只有 RenderObjectWidget 才会通过对应的 RenderObjectElement 生成最终的 RenderObject,组成 RenderTree;
RenderTree 背后还有 Layer Tree,之后才有渲染显式,这就不是本文的范围了,有机会以后再分享吧!
-- End --
本文对你有帮助吗?留言、转发、点好看是最大的支持,谢谢!
推荐阅读: