最近正在写一个思维导图Widget,但是遇到了一个很诡异的bug,猜测问题出在自定义的Element上。然而看了好几天掘金的文章也没看明白,最后还是决定亲自分析一下。
本文基于flutter 3.29.0
Element class - widgets library - Dart API
1.Element的职责
很多文章都会说
Element是Flutter的树中特定位置的Widget的实例化。
这句话真的很官方,因为它就是从官方注释直接翻译过来的。但是我觉得大多数人看到这个定义时,可能和看到同济高数里那些难懂的定义一样,仍然不太清楚Element到底有什么用。
我自己分析了一下,认为Element
- 是一个“箱子”,负责储存
Widget、管理RenderObject(如果有),负责把二者关联起来 - 是一个“中介”,flutter在构建界面时是通过管理
Element来实现的。
2.Element中的的主要对象和方法
1. _parent
Element按树形结构被组织起来,除根节点外,每个element对象都要保存其父节点
2. _widget
创建这个Element时用到的widget对象。有些文章会说widget负责配置Element,这句话其实不太准确。
widget负责配置Element主要体现在:widget会根据自己类型的不同创建不同类型的Element子类。 比如StatefulWidget会创建StatefulElement,RenderObjectWidget会创建RenderObjectElement。
通常来讲,我们在写widget时用到的那些属性(比如Text里的字符串和textStyle,Column里的mainAxisSize和spacing...)是用来配置RenderObject的。Element大多数时候不会用到widget,只会把它储存起来,这点自定义过RenderBox的同学可能比较清楚。
3. _depth
Element按树形结构被组织起来,代表该element对象在树中的深度。刷新时会按从父到子刷新。
4. _slot
有些文章会说slot代表该element在父节点中的位置信息,这句话其实不太准确。
slot的具体值是没有意义的,当slot相对旧值发生改变时,说明它的位置变化了,具体变到哪和slot无关。
就像hashCode或md5,a和b的hashCode不相同说明它们在内存中的位置不同,但是hashCode这串数字没有意义。另外,slot对element其实没什么意义,它的作用是在自己更新时触发renderObject.move方法,最终调用markNeedsLayout()去重新计算布局。
5. _owner 和 buildScope (不是重点)
_owner是BuildOwner实例,全局唯一。它负责
- 记录需要被刷新的节点(实际上交给
BuildScope完成) - 管理不活跃的
Element对象,以在合适的时机将它们回收 - 管理
GlobalKey和其绑定的Element,以在合适的时机复用它们
buildScope是BuildScope实例,子element通常会使用父节点的buildScope,但是有例外(比如LayoutBuilder类)。buildScope实际上负责管理该element下需要刷新的节点
3.Element的生命周期和生命周期方法
3.1 Element的生命周期
enum _ElementLifecycle { initial, active, inactive, defunct }
stateDiagram-v2
[*] --> initial: widget.createElement
initial --> active: Element.mount()
active --> inactive: Element.deactivate()
inactive --> active: Element.active()
inactive --> defunct: Element.unmount()
defunct --> [*]
1.initial:flutter framework 通过Widget.createElement()实例化了一个Element对象,是Element生命周期的开始。
2.active:对于组合型Element,也就是StatelessWidget和StatefulWidget对应的Element类型,表示这个Element对应的widget对象的子widget均已被展开(inflate)成element。对于渲染型Element,也就是RenderObjectWidget对应的Element,代表自己创建的RenderObject已经被创建并添加到渲染树。此时这个widget随时都可能显示在屏幕上。
3.inactive:由于各种原因(通常是widget树的变化),该Element从树中移除。这个Element将不会显示在屏幕上,并存活到当前帧结束。
4.defunct:该帧结束时此element对象没有被重新添加到树中,需要被卸载。这个Element的生命周期正式结束。
3.2 Element的生命周期过程
之前提到过主要有两种Element:组合型ComponentElement和渲染型RenderObjectElement,这两种Element在执行生命周期方法时的工作也有所不同,这里我们分类讨论
initial: parent.inflateWidget(Widget newWidget, Object? newSlot)
Element inflateWidget(Widget newWidget, Object? newSlot) {
final Key? key = newWidget.key;
// 这里会对使用了globalKey的Widget做特殊处理,尝试复用,省略
....
// 父element创建子widget对应的Element,此时为initial状态
final Element newChild = newWidget.createElement();
// 将其挂载到Element树上,此时为active状态
// 下一节会讨论
newChild.mount(this, newSlot);
return newChild;
}
flutter中,Element按树形结构被组织起来,那创建子节点的任务自然就交给了父节点来完成。
在父节点
- 被挂载到树上,即父节点自己的
mount方法被调用时 - 有新
widget被插入到树中,父Element调用updateChild方法更新自己的孩子时
父节点parent会通过Element.inflateWidget(Widget newWidget, Object? newSlot)方法,递归的将子Widget'膨胀'成Element对象并挂载。
父节点调用inflateWidget方法创建、挂载mount子节点,而子节点被挂载mount的过程中又会调用自己的inflateWidget方法去挂载自己的孩子节点......当自己的孩子节点全部被挂载完成时自己才算完。
initial -> active: mount(Element? parent, Object? newSlot)
Element类的mount方法
// 父节点的inflateWidget方法,在这里挂载子节点
Element inflateWidget(Widget newWidget, Object? newSlot) {
final Element newChild = newWidget.createElement();
// this就是自己,对于newChild而言就是parent element
newChild.mount(this, newSlot);
assert(newChild._lifecycleState == _ElementLifecycle.active);
return newChild;
}
在mount方法中,需要
- 记录自己的父节点
_parent和位置_slot,更新自己的深度 - 将自己的状态更新为
active,表示被激活,随时都可能被显示 - 记录父节点的
BuildOwner和BuildScope - 更新
InherientWidget,并将自己关联到通知树上attachNotificationTree
void mount(Element? parent, Object? newSlot) {
_parent = parent;
_slot = newSlot;
// 标记自己的状态为active
_lifecycleState = _ElementLifecycle.active;
_depth = 1 + (_parent?.depth ?? 0);
if (parent != null) {
_owner = parent.owner;
_parentBuildScope = parent.buildScope;
}
assert(owner != null);
final Key? key = widget.key;
if (key is GlobalKey) {
owner!._registerGlobalKey(key, this);
}
_updateInheritance();
attachNotificationTree();
}
我自己看到这段代码的时候都是懵逼的,赋值就完事了?但是mount方法干的事就是这么简单。其实仔细想想,flutter中真正负责测量、绘制的是RenderObject,而Element的最大作用就是根据Widget树尝试复用已有的RenderObject,所以并不需要什么复杂的操作。
ComponmentElement的mount方法
StatelessElement和StatefulElement都是ComponmentElement的子类,会触发一次performRebuild方法,performRebuild又会调用widget的build方法,创建出自己的widget并挂载他们。
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
_firstBuild();
assert(_child != null);
}
void _firstBuild() {
// StatefulElement overrides this to also call state.didChangeDependencies.
rebuild(); // This eventually calls performRebuild.
}
RenderObjectElement的mount方法
RenderObjectElement额外做了几件事情:
- 调用
Widget.createRenderObject方法,创建RenderObject - 找到上级的
RenderObjectElement,调用它的insertRenderObjectChild方法,把自己创建的RenderObject插入到树中。对应RenderObject的insert和setChild方法 - 更新ParentData
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
// 创建了RenderObject对象
_renderObject = (widget as RenderObjectWidget).createRenderObject(this);
// 把RenderObject对象插入到RenderObject树中
// 这个方法在ComponentElement是没有的,只有RenderObjectElement才有这个方法
attachRenderObject(newSlot);
super.performRebuild(); // _dirty = false; clears the "dirty" flag
}
@override
void attachRenderObject(Object? newSlot) {
_slot = newSlot;
// 找到上级离自己最近的一个RenderObjectElement
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
// 调用上级Element的insertRenderObjectChild方法
_ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);
final List<ParentDataElement<ParentData>> parentDataElements =
_findAncestorParentDataElements();
for (final ParentDataElement<ParentData> parentDataElement in parentDataElements) {
// 调用parentDataWidget.applyParentData,将Widget携带的信息传递给RenderObject
_updateParentData(parentDataElement.widget as ParentDataWidget<ParentData>);
}
}
active -> inactive: deactivate()
@mustCallSuper
void deactivate() {
if (_dependencies?.isNotEmpty ?? false) {
// 将自己从之前订阅的InheritedElement中移除,不再收听变化
for (final InheritedElement dependency in _dependencies!) {
dependency.removeDependent(this);
}
}
_inheritedElements = null;
// 标记自己为inactive状态
_lifecycleState = _ElementLifecycle.inactive;
}
inactive -> defunct: unmount()
默认的unmount
void unmount() {
final Key? key = _widget?.key;
if (key is GlobalKey) {
owner!._unregisterGlobalKey(key, this);
}
_widget = null;
_dependencies = null;
_lifecycleState = _ElementLifecycle.defunct;
}
对于StatefulElement,还会调用其state的dispose方法
RenderObjectElement的unmount方法
会额外调用自己管理的RenderObject的dispose方法
@override
void unmount() {
final RenderObjectWidget oldWidget = widget as RenderObjectWidget;
super.unmount();
oldWidget.didUnmountRenderObject(renderObject);
_renderObject!.dispose();
_renderObject = null;
}
4.更新Element的回调流程
推荐两篇写的比较好的文章:# 纷争再起:Flutter-UI绘制解析 & # Flutter 必知必会系列—— Element 的更新复用机制
4.1 markNeedsRebuild():主动触发更新
// setState的背后也是markNeedsBuild
void markNeedsBuild() {
if (_lifecycleState != _ElementLifecycle.active) {
return;
}
if (dirty) {
return;
}
// 标脏
_dirty = true;
// owner是BuildOwner对象,在mount方法被赋值,和parent相同
// 可以简单理解成全局只有一个BuildOwner实例
owner!.scheduleBuildFor(this);
}
最终会调用BuildScope._scheduleBuildFor(element)
// BuildOwner类
void scheduleBuildFor(Element element) {
final BuildScope buildScope = element.buildScope;
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
// onBuildScheduled是个回调,在BuildOwner创建时被赋值
// 在下一帧时刷新element树
onBuildScheduled!();
}
buildScope._scheduleBuildFor(element);
}
BuildScope._scheduleBuildFor(element)会将自己添加到BuildScope的脏列表_dirtyElements中等待刷新。并且如果是本帧内第一次被调用,还会执行一次scheduleRebuild回调。
// BuildScope类
void _scheduleBuildFor(Element element) {
assert(identical(element.buildScope, this));
if (!element._inDirtyList) {
_dirtyElements.add(element);
element._inDirtyList = true;
}
if (!_buildScheduled && !_building) {
_buildScheduled = true;
scheduleRebuild?.call();
}
if (_dirtyElementsNeedsResorting != null) {
_dirtyElementsNeedsResorting = true;
}
}
结束后,请求被刷新的element对象会被存储到本对象的buildScope._dirtyElements中。
4.2 刷新
对于markNeedsBuild()是如何注册回调的,详见
# Flutter中setState原理及更新机制。最终会回调到BuildOwner.buildScope方法。
4.2.1 BuildOwner.buildScope
flutter会在WidgetFlutterBindings初始化时创建一个rootElement。通过WidgetsBinding.drawFrame调用BuildOwner.buildScope时,接收的context参数就是rootElement。我们在这里假设buildScope全局唯一。
// WidgetsBinding.drawFrame
void drawFrame() {
buildOwner!.buildScope(rootElement!);
super.drawFrame();
buildOwner!.finalizeTree();
}
// BuildOwner.buildScope
void buildScope(Element context, [VoidCallback? callback]) {
final BuildScope buildScope = context.buildScope;
if (callback == null && buildScope._dirtyElements.isEmpty) {
return;
}
try {
_scheduledFlushDirtyElements = true;
buildScope._building = true;
if (callback != null) {
callback();
}
buildScope._flushDirtyElements(debugBuildRoot: context);
} finally {
buildScope._building = false;
_scheduledFlushDirtyElements = false;
}
}
- 如果没有需要刷新的
element,也没有回调要执行,就跳过过程 - 否则,更新标志位,表示刷新开始,并执行回调
callback。回调可能会重新将一部分Element标记为需要刷新的状态(可以参考ListView的源码) - 调用
buildScope._flushDirtyElements方法,刷新所有需要刷新的Element - 更新标志位,表示刷新完成
题外话:对于
buildScope这种末尾接受一个lambda作为参数的方法,kotlin的语法是这样的:fun buildScope(element: Element){ // callback }现在想想,kotlin的语法是真好看啊
4.2.2 BuildScope._flushDirtyElements
从子节点到父节点,依次调用element对象的rebuild方法
@pragma('vm:notify-debugger-on-exception')
void _flushDirtyElements({required Element debugBuildRoot}) {
// 按element._depth排序,浅的在前
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
try {
for (int index = 0; index < _dirtyElements.length; index = _dirtyElementIndexAfter(index)) {
final Element element = _dirtyElements[index];
// 如果element的buildScope和本buildScope是同一个,我们暂时将其视为true
if (identical(element.buildScope, this)) {
// tryRebuild会调用element的Rebuild方法
_tryRebuild(element);
}
}
} finally {
for (final Element element in _dirtyElements) {
if (identical(element.buildScope, this)) {
element._inDirtyList = false;
}
}
_dirtyElements.clear();
_dirtyElementsNeedsResorting = null;
_buildScheduled = false;
}
}
void _tryRebuild(Element element) {
element.rebuild();
}
- BuildScope中,首先会对
element对象按深度排序,保证先刷新父节点,再刷新子节点 - 如果
element.buildScope和本buildScope是同一个对象,执行_tryRebuild方法,刷新自己。_tryRebuild会调用element的rebuild方法 - 更新被刷新的
element的状态,表示他们已经不在脏列表中 - 清空脏列表,更新标志位,表示本
BuildScope的刷新工作已完成 - todo: 什么时候BuildScope会不一致?
5.Element内部如何处理更新:rebuild & update
5.1 rebuild和performRebuild
rebuild方法只是简单的调用了performRebuild方法,Element的子类会重写这个方法。还是只看最常用的两个Element子类。
简单来说,rebuild方法执行完后,Widget树已经被更新到最新的状态,同时RenderObject树也被更新完毕
5.1.1 ComponentElement:更新widget树,并更新对应的Element
- 调用
ComponentElement.build()方法创建出新的child。对于StatelessElement,build方法就是widget.build;对于StatefulElement就是state.build() - 使用
updateChild(_child, built, slot)更新Element
void performRebuild() {
// 这个built,就是stless.build()方法 或state.build()方法返回的Widget
Widget built = build();
// 注意,如果是StatefulElement,didChangeDependencies也是在这里被调用的
super.performRebuild(); // _dirty = false;
// _child就是本ComponentElement的唯一子Element。updateChild会更新widget树,并更新对应的Element
_child = updateChild(_child, built, slot);
}
5.1.2 RenderObjectElement:更新RenderObject
调用widget的updateRenderObject(),以设置RenderObject的各个属性
@override
void performRebuild() {
_performRebuild(); // calls widget.updateRenderObject()
}
void _performRebuild() {
(widget as RenderObjectWidget).updateRenderObject(this, renderObject);
super.performRebuild(); // _dirty = false;
}
5.2 updateChild
这个方法是更新子Element的核心方法,根据旧element和新widget,决定复用、更新或新建Element对象
child != null, newWidget == null:不再使用子elementchild.widget和newWidget是相同的对象:只尝试更新slotchild.widget和newWidget不是同一个对象,但是类型和key相同,执行child.update方法更新child.widget和newWidget均不为空,但类型或key不同:无法更新,移除旧child,创建新elementchild == null, newWidget != null:创建一个新的element
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
if (newWidget == null) {
if (child != null) {
// 情况1,将子树添加到BuildOwner的_inactiveElements列表中
deactivateChild(child);
}
return null;
}
final Element newChild;
if (child != null) {
if (child.widget == newWidget) {
// 情况2:widget对象未发生变化,如果有需要(child的slot和新的slot不相同),更新slot
if (child.slot != newSlot) {
updateSlotForChild(child, newSlot);
}
newChild = child;
} else if (Widget.canUpdate(child.widget, newWidget)) {
// 情况3:两个widget类型和key相同,不是同一实例:更新slot,调用update方法
if (child.slot != newSlot) {
updateSlotForChild(child, newSlot);
}
child.update(newWidget);
newChild = child;
} else {
// 情况4:两个widget类型或key不同,无法复用
deactivateChild(child);
newChild = inflateWidget(newWidget, newSlot);
}
} else {
// 情况5:widget!= null, child == null:创建新的element
newChild = inflateWidget(newWidget, newSlot);
}
return newChild;
}
5.3 Element.update(Widget)方法
当flutter framework决定更改这个element对应的widget时会使用这个方法。Element种类不同,执行过程也不相同
5.3.1 StatelessElement和StatefulElement
会直接触发自己的rebuild()方法。调用performRebuild方法更新自己的子element,这又会触发下级element去更新,从而从上到下的更新完element树。
这里的调用链就是 parent.performRebuild -> child.rebuild -> child.performRebuild -> ....
5.3.2 RenderObjectElement及其子类
RenderObjectElement会调用widget.updateRenderObject,更新RenderObject的信息
// RenderObjectElement
@override
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
(widget as RenderObjectWidget).updateRenderObject(this, renderObject);
}
SingleChildRenderObjectWidget会接受一个widget参数作为child,需要尝试更新它
@override
void update(SingleChildRenderObjectWidget newWidget) {
super.update(newWidget);
_child = updateChild(_child, (widget as SingleChildRenderObjectWidget).child, null);
}
MultiChildRenderObjectWidget会接受一个List<Widget> children作为参数,需要用updateChildren更新所有的孩子
@override
void update(MultiChildRenderObjectWidget newWidget) {
super.update(newWidget);
final MultiChildRenderObjectWidget multiChildRenderObjectWidget =
widget as MultiChildRenderObjectWidget;
_children = updateChildren(
_children,
multiChildRenderObjectWidget.children,
forgottenChildren: _forgottenChildren,
);
_forgottenChildren.clear();
}
6.一些细小的点
6.1 修改slot是如何触发重新布局的
会直接调用renderObject.markNeedsLayout()
@override
void updateSlot(Object? newSlot) {
final Object? oldSlot = slot;
_ancestorRenderObjectElement?.moveRenderObjectChild(renderObject, oldSlot, slot);
}
@override
void moveRenderObjectChild(
RenderObject child,
IndexedSlot<Element?> oldSlot,
IndexedSlot<Element?> newSlot,
) {
final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>>
renderObject = this.renderObject;
renderObject.move(child, after: newSlot.value?.renderObject);
}
void move(ChildType child, {ChildType? after}) {
final ParentDataType childParentData = child.parentData! as ParentDataType;
if (childParentData.previousSibling == after) {
return;
}
_removeFromChildList(child);
_insertIntoChildList(child, after: after);
markNeedsLayout();
}
6.2 作者遇到了什么问题?怎么解决的?
// todo
其实RenderBox部分的布局逻辑倒是没问题,问题确实是出在Element部分。performLayout不是会无条件被调用的。flutter framework看来Element没有发生变化,对应的RenderObject也不会重新布局,在layout阶段请求重新构建的代码也自然不会执行。
解决方法: 1. 更换slot,把slot从节点id更换为节点的数据类;2. 及时删掉不用的widget
RenderBox中布局的逻辑
为什么动一下屏幕,被删除的节点就不再显示了?
6.3 对于BuildScope不相同的情况是怎么处理的?
见 4.2.2,只有使用LayoutBuilder时会出现这种情况。
LayoutBuilder在RenderLayoutBuilder的performLayout阶段调用updateChild方法
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot); // Creates the renderObject.
renderObject.updateCallback(_rebuildWithConstraints);
}
void _rebuildWithConstraints(ConstraintType constraints) {
void updateChildCallback() {
Widget built;
built = (widget as ConstrainedLayoutBuilder<ConstraintType>).builder(this, constraints);
_child = updateChild(_child, built, null);
_needsBuild = false;
_previousConstraints = constraints;
}
}
final VoidCallback? callback =
_needsBuild || (constraints != _previousConstraints) ? updateChildCallback : null;
owner!.buildScope(this, callback);
}
断断续续写了一周多,我是真能拖啊。。。就这么多吧,希望没啥问题