通过一系列文章记录一下学习 Flutter 的过程,总结一下相关知识点。
- 学习Flutter -- 框架总览
- 学习Flutter -- 启动过程做了什么
- 学习Flutter -- Widget 的组成
- 学习Flutter -- Element 的作用
- 学习Flutter -- RenderObject 布局过程
- 学习Flutter -- RenderObject 绘制过程
在 Flutter 开发中,我们直接接触最多的就是 Widget,那么这些 Widget 具体是如何工作的,以及最后是如何显示到屏幕上的呢?接下来我们就去探索一下 Widget 背后的原理。
什么是三棵树?
简而言之,三棵树存在的目的是为了性能。
我们通过 Widget 构建 UI 的过程中,会形成以 Widget 为节点的树形结构(简称Widget树),在最后渲染到屏幕之前, 经过 Flutter framework 的一系列转换,又形成了分别以 Element 和 RenderObject 为节点的树形结构(简称 Element 树和 Render 树),通过这三棵树的协同工作,各司其职,实现了最终的高效渲染的目的。
-
Widget
是用户界面不可变描述,开发中直接接触的就是Widget;
-
Element
是 Widget 的实例化对象,作为 Widget树 和 Render树之间的协调者,持有了对应的 Widget 和 RenderObject 的引用;
-
RenderObject
是最终实现界面的布局和绘制的对象,保存了元素的大小、布局等信息;
如下图,分别以 Widget、Element、RenderObject 为节点构建的树形结构。
注意,Element Tree与 Widget Tree 的之间节点是一一对应的,但是 Render Tree 与 Element Tree 的节点不是一一对应的, 只有继承自 RenderObjectWidget 的Widget 才会有对应的 RenderObject 对象节点。
为了方便理解每个知识点,上文引出的是三棵树的概念,在本篇,将针对围绕其中的 Element 树的职责以及实现原理展开介绍。
Element
由于 Widget 会不断的被重建和销毁,所以 Widget 树是非常不稳定的,那么 Flutter 是如何来维护树形结构的稳定呢,答案就是 Element。Element 就是 Widget 的实例, 在树中的位置关系是一一对应的。
Element 有两个主要职责,分别是:
-
根据 Widget 树的变化来维护 Element 树,包括节点的插入、删除、更新等;
-
作为 Widget 和 RenderObject 的协调者;
Element 的种类
如图所示,Element 从功能上可分为两大类,分别是:
-
ComponentElement
组合类Element,对应的 Widget 类型是 ComponentWidget
特点:包含一个子节点,其子节点对应的 Widget 需要通过 build 方法去创建。
-
RenderObjectElement
渲染类 Element,对应的 Widget 类型是 RenderWidget
特点:不同的子类包含的子节点个数不同,如:Leaf (无子节点)、Single(单个子节点)、 Multi (多个子节点)。
图中可以看出,Element 实现了 BuildContext 接口,其实我们在开发 Widget 的过程中,其中 Widget build(BuildContext context) 函数中的参数 context 就是该 Widget 对应的 Element。
Element 与其他元素的关系
如图,图中的数字索引表示的关系是:
- 1、2 表示 Element 通过持有 parent、child 指针,从而构成了 Element 树
- 3、4 表示 Element 持有 Widget、RenderObject 的引用
- 5、6 表示 State 是绑定在 Element 上的,而不是绑定在 StatefulWidget 上; 同时 7 表示 State 也持有 Wdiget;
注意:只有 StatefulElement 才有 State (即 5、6、7),只有RenderObjectElement 才持有 RenderObject (即 4)。
生命周期状态
// ELEMENTS
enum _ElementLifecycle {
initial,
active,
inactive,
defunct,
}
在 UI 的变化中,一个 Element 对象在其生命周期中共有四个状态:initial、active、inactive、defunct,它们之间的转换关系如图:
下面详细介绍一下每个状态产生的过程,以及涉及到的方法的调用
- initial
初始状态,Element 被初始化后的状态
方法调用关系:parent 通过 Element.inflatWidget -> Widget.createElement 创建 child element。
-
active
活跃状态
方法调用关系:parent 通过 Element.mount 将新创建的 child element 放到 Element Tree 的指定插槽处(slot),mount 方法会调用 Widget.createRenderObject 创建与 element 相关连的 RenderObject 对象,然后调用 element.attachRenderObject 将 RenderObject 对象插入到 Render Tree 的指定插槽处。此时 element 就处于 active 状态,内容随时可能显示到屏幕上,也可能是隐藏的。
随着 UI 的刷新或状态的变化,Widget Tree 的结构发生了变化,此时也需要构建对应的 Element Tree,此时 parent 会调用 Element.update 方法去更新子节点,update 操作会在在以当前节点为根节点的子树上递归进行,一直到叶子节点;
为了element 的复用,在 Elementre Tree 的重新构建时,会优先尝试旧的 Element Tree 中相同位置的 Element 对象能否复用,能否复用的依据是:调用 Widget.canUpdate 方法,判断new Widget 与 old Widget 的 runtimeType & key 是否相等,相等则更新旧的 Element, 否则创建新的。
-
inactive
非活跃状态
随着 Widget Tree 的变化,Element 对应的 Widget 有可能被移除了,此时,parent 调用 deactivateChild 方法,源码如下:
void deactivateChild(Element child) {
child._parent = null;
child.detachRenderObject();
owner!._inactiveElements.add(child);
}
主要做了三件事:
- 将 Element 从 Element Tree 中移除,即将 parent 指针置为 null;
- 将对应的 RenderObject 从 Render Tree 中移除;
- 将 Element 添加到 owner!._inactiveElements 中,此时,会对以该 Element 为根节点的子树上的所有子节点调用 deactivate 方法,即移除子树。
此时,Element 的状态变为 inactive 状态,会从屏幕上消失,但直到当前动画最后一帧结束执之前仍会保留(保留的目的是:为了避免在一次动画执行过程中反复创建、移除某个element),如果最后一帧动画结束之后它仍旧未变成 active 状态,则会调用它的 unmount 方法彻底将其移除,此时Element 的状态变为 defunct 状态。
上述提到,Element 仍然能够从 inactive 状态变为 active 状态,前提是其对应的 Widget 的 key 是 GlobalKey,并且又被重新插入到 Element Tree 中,这个过程主要涉及到的方法调用有:
这个过程又做了什么呢?
-
首先将该 element 从 owner._inactiveElements 中移除
-
对该 element subtree 上所有子节点重新调用 activate 方法
-
将对应的 RenderObject 重新插入到 Render Tree 中
-
此时该 element subtree 又变成了 active 状态,再次显示到屏幕上
-
defunct
失效状态
变为 inactive 状态的 Element,在最后一帧动画结束之后仍然未变回 active 状态, 此时它的状态将变为 dafunct,Element 的生命周期也就彻底结束了。
根据以上四个状态之间的切换得知, Element 的生命周期大致流程总结如下图:
接下来针对 Element 中的几个核心方法简要分析一下。
核心方法介绍
updateChild
在 Element Tree 上,parent 节点通过该方法来修改 child 节点对应的 Widget。
源码:(为方便查看,去除了多余的注释和断言)
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
//1. newWidget == null
if (newWidget == null) {
if (child != null) {
deactivateChild(child);//子节点需要被移除
}
return null;
}
//2. newWidget != null
final Element newChild;
//2.1 child != null
if (child != null) {
//2.1.1 新旧 widget 相同,直接复用
if (child.widget == newWidget) {
if (child.slot != newSlot) {
//更新插槽
updateSlotForChild(child, newSlot);
}
newChild = child;
//2.1.2 尝试更新(复用)
} else if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot) {
updateSlotForChild(child, newSlot);
}
//使用 newWidget 更新 element
child.update(newWidget);
newChild = child;
} else {
//2.1.3 销毁旧的,创建新的 element
deactivateChild(child);
newChild = inflateWidget(newWidget, newSlot);
}
} else {
//2.2 child == null, 直接创建新的 element
newChild = inflateWidget(newWidget, newSlot);
}
return newChild;
}
根据方法入参不同,实现了不同的逻辑,官方注释如图:
- newWidget == null
- child != null,说明子节点对应的 Widget 被移除了,直接对其调用 deactivateChild
- newWidget != null
- child == null, 说明 newWidget 是新插入的 Widget,直接通过 inflateWidget 创建新的子节点 newChild
- child != null, 都不为空,此时又分三种情况
-
child.widget == newWidget 说明新旧 Widget 没有发生变化,child.slot != newSlot 说明插槽位置发生变化,直接更新插槽即可(在兄弟节点之间移动了位置)。
-
若 Widget 不相同,则通过 Widget.canUpdate 判断是否可以用 newWidget 去更新 child element,如果可以,则直接调用 update 方法更新即可
-
否则,需要先将 child element 移除,在使用 newWidget 创建新的 child element 节点。
-
update
在上面 updateChild 方法中,可知,如果新旧 Widget.[runtimeType & key] 相等,则会走到该方法,Element 的不同子类需要该方法。
Element 父类
源码:
void update(covariant Widget newWidget) {
_widget = newWidget;
}
父类中,只是根据传入的 newWidget 对_widget 属性更新。
StatelessElement
源码:
void update(StatelessWidget newWidget) {
super.update(newWidget);
//重建
rebuild(force: true);
}
通过 rebuild 方法重建 child Widget,rebuild 方法下面会介绍,最终会调用到我们写的 build 方法。
StatefulElement
源码:
void update(StatefulWidget newWidget) {
super.update(newWidget);
//先获取旧的 Widget
final StatefulWidget oldWidget = state._widget!;
//使用 newWidgt 更新 state 的 _widget 属性
state._widget = widget as StatefulWidget;
//调用 State 的生命周期方法 didUpdateWidget
state.didUpdateWidget(oldWidget) as dynamic;
//重建
rebuild(force: true);
}
StatefulElement 比 StatelessElement 稍微复杂一点,需要处理 State。
小结:
StatelessElement 和 StatefulElement 都继承自 ComponentElement,属于组合型Element,都会在 update 方法中执行 rebuild,从而重新 build child widget。
RenderObjectElement
源码:
@override
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
_performRebuild(); // calls widget.updateRenderObject()
}
void _performRebuild() {
(widget as RenderObjectWidget).updateRenderObject(this, renderObject);
super.performRebuild(); // clears the "dirty" flag
}
不同于组合型 Element,渲染型 Element 的 update 操作会调用 Widget.updateRenderObject 方法来更新 renderObject 对象。
SingleChildRenderObjectElement
源码:
@override
void update(SingleChildRenderObjectWidget newWidget) {
super.update(newWidget);
//会对子节点调用 updateChild 方法。
_child = updateChild(_child, (widget as SingleChildRenderObjectWidget).child, null);
}
MultiChildRenderObjectElement
源码:
@override
void update(MultiChildRenderObjectWidget newWidget) {
super.update(newWidget);
final MultiChildRenderObjectWidget multiChildRenderObjectWidget = widget as MultiChildRenderObjectWidget;
//会通过 updateChildren 方法处理子节点的插入、移动、更新、删除等操作。
_children = updateChildren(_children, multiChildRenderObjectWidget.children, forgottenChildren: _forgottenChildren);
_forgottenChildren.clear();
}
具体细节,可查看 updateChildren 源码。
inflateWidget
该方法的主要作用是:根据传入的 newWidget 生成对应的 Element,并将其挂载(mount) 到 Element Tree 上。
源码:
Element inflateWidget(Widget newWidget, Object? newSlot) {
final Key? key = newWidget.key;
//key 是 GlobalKey,则考虑复用
if (key is GlobalKey) {
//从 inactive elements 中查找是否有 inactive 状态的节点(即刚被从树上移除的)
final Element? newChild = _retakeInactiveElement(key, newWidget);
if (newChild != null) {
//找到了,则激活
newChild._activateWithParent(this, newSlot);
//并调用 updateChild 方法进行更新
final Element? updatedChild = updateChild(newChild, newWidget, newSlot);
return updatedChild!;
}
}
//创建新的 element
final Element newChild = newWidget.createElement();
//挂载到指定插槽
newChild.mount(this, newSlot);
return newChild;
}
mount
源码:
void mount(Element? parent, Object? newSlot) {
//设置 _parent 等属性,将 Element 关联到 Element Tree 上
_parent = parent;
_slot = newSlot;
//此时状态为 active
_lifecycleState = _ElementLifecycle.active;
_depth = _parent != null ? _parent!.depth + 1 : 1;
if (parent != null) {
_owner = parent.owner;
}
final Key? key = widget.key;
// 将 GlobalKey 注册到 owner 中
if (key is GlobalKey) {
owner!._registerGlobalKey(key, this);
}
_updateInheritance();//只是继承父节点的 _inheritedWidgets(实现如下)
attachNotificationTree();
}
void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active);
_inheritedWidgets = _parent?._inheritedWidgets;
}
调用时机:
Element 首次被创建后被插入到 Element Tree 上时会调用该方法,此时 elelment 的状态变为 active。
ComponentElement
源码:
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
_firstBuild();
}
void _firstBuild() {
// StatefulElement overrides this to also call state.didChangeDependencies.
rebuild(); // This eventually calls performRebuild.
}
组合型 Element 会在挂载时候执行 _firstBuild -> rebuild 操作,下面会具体分析 rebuild 方法。
RenderObjectElement
源码:
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
//创建 RenderObject 对象
_renderObject = (widget as RenderObjectWidget).createRenderObject(this);
//将其插入到 Render Tree 上的指定插槽除
attachRenderObject(newSlot);
super.performRebuild(); // clears the "dirty" flag
}
//父类的
void performRebuild() {
_dirty = false;
}
makeNeedsBuild
源码:
void markNeedsBuild() {
if (_lifecycleState != _ElementLifecycle.active) {
return;
}
if (dirty) {
return;
}
//标记成 dirty
_dirty = true;
//并添加到了 owner 的 _dirtyElments 中,以便在下一帧 rebuild
owner!.scheduleBuildFor(this);
}
//BuildOwner
void scheduleBuildFor(Element element) {
_dirtyElements.add(element);
element._inDirtyList = true;
}
主要作用是:将 element 标记成 dirty,并添加到全局的列表中,以便在下一帧 rebuild。
该方法的调用场景有:
- Element.reassemble ( debug hot reload )
- Element.didChangeDependecies
- StatefulElement.activate
- State.setState
rebuild
源码:
void rebuild({bool force = false}) {
if (_lifecycleState != _ElementLifecycle.active || (!_dirty && !force)) {
return;
}
//只有是活跃的、脏节点才调用
performRebuild();
}
调用场景有:
-
BuildOwner.buildScope 方法内只针对加入到 _dirtyElements 中的脏节点调用
-
ComponentElement.mount
-
ComponentElement.update
performRebuild
Element 父类
源码:
void performRebuild() {
//在父类中只是清空 dirty 标记
_dirty = false;
}
ComponentElement 父类
源码:
void performRebuild() {
//[StatelessWidget.build] or [State.build]
Widget? built = build();
super.performRebuild(); // clears the "dirty" flag
//更新 child element
_child = updateChild(_child, built, slot);
}
class StatelessElement extends ComponentElement {
@override
Widget build() => (widget as StatelessWidget).build(this);
}
class StatefulElement extends ComponentElement {
@override
Widget build() => state.build(this);
}
作用:调用 build() 方法生成新的 Widget,然后调用 updateChild 去更新 child element。
RenerObjectElement 父类
源码:
void performRebuild() {
(widget as RenderObjectWidget).updateRenderObject(this, renderObject);
super.performRebuild(); // clears the "dirty" flag
}
//例如:
class CustomPaint extends SingleChildRenderObjectWidget {
void updateRenderObject(BuildContext context, RenderCustomPaint renderObject) {
renderObject
..painter = painter
..foregroundPainter = foregroundPainter
..preferredSize = size
..isComplex = isComplex
..willChange = willChange;
}
}
渲染类 Element,只是调用了 Widget 的 updateRenderObject 更新 renderObject 对象,具体实现可由子类自定义。
上面针对 Element 中的核心方法进行了简单的介绍,具体看某个方法已经能了解了它的职责,但是具体什么时机调用这一系列方法还是比较混乱,下面通过生命周期的视角来将这些方法串联起来。
Element 生命周期流程
一个Element 的生命周期过程中会经历:创建、更新、重建、销毁等流程,下面针对这几个流程来分别梳理下各自的方法调用情况。
创建流程
起源于父节点调用 inflateWidget,创建出子节点 element,并被挂载到 Element Tree 中。
更新流程
若新旧 Widget 的 [runtimeType && key] 相等,则父节点通过 child.update 触发子 Element 的更新。
重建流程
rebuild 方法被调用时的重建流程。
销毁流程
随着 UI 的变化,element 节点及其子树会被移除。
以上通过 Element 的生命周期的视角,将 Element 中的核心方法调用流程进行了简要的总结,至此,对 Flutter 三棵树中 Element 的整体面貌有了进一步的认识。
小结
- Element 与 Widget 是一一对应的,类似 json 与 object 的关系。
- Widget 是无状态的,Element 是有状态的。Widget 随着 UI 的刷新会被不断的重建,但 Element 是能够被复用的,这也是 Element 存在的必要性,保证了 Flutter 树形结构的稳定;
- 只有 RenderObjectElment 才有对应的 renderOjbect 对象;
- Element 作为 Widget 与 RenderObject 之间的协调者,会根据 Widget Tree 的变化对 Element Tree 做出更新,同时对 Render Tree 进行更新。