深入浅出 Flutter Framework 之 Element

本文是『 深入浅出 Flutter Framework 』系列文章的第三篇,主要围绕 Element 相关内容进行分析介绍,包括 Element 分类、Element 与其他几个核心元素的关系、Element 生命周期以及核心方法解读等。

©原创文章,转载请注明出处!

本系列文章将深入 Flutter Framework 内部逐步去分析其核心概念和流程,主要包括:
『 深入浅出 Flutter Framework 之 Widget 』
『 深入浅出 Flutter Framework 之 BuildOwner 』
『 深入浅出 Flutter Framework 之 Element 』
『 深入浅出 Flutter Framework 之 PaintingContext 』
『 深入浅出 Flutter Framework 之 Layer 』
『 深入浅出 Flutter Framework 之 PipelineOwner 』
『 深入浅出 Flutter Framework 之 RenderObejct 』
『 深入浅出 Flutter Framework 之自定义渲染型 Widget 』

Overview


通过『 深入浅出 Flutter Framework 之 Widget 』的介绍,我们知道 Widget 本质上是 UI 的配置数据 (静态、不可变),Element 则是通过 Widget 生成的『实例』,两者间的关系就像是 json 与 object。

同一份配置 (Widget) 可以生成多个实例 (Element),这些实例可能会被安插在树上不同的位置。

UI 的层级结构在 Element 间形成一棵真实存在的树「Element Tree」,Element 有 2 个主要职责:

  • 根据 UI (「Widget Tree」) 的变化来维护「Element Tree」,包括:节点的插入、更新、删除、移动等;
  • Widget 与 RenderObject 间的协调者。

分类



如图所示,Element 根据特点可以分为 2 类:

  • 「Component Element」 —— 组合型 Element,「Component Widget」、「Proxy Widget」对应的 Element 都属于这一类型,其特点是子节点对应的 Widget 需要通过build方法去创建。同时,该类型 Element 都只有一个子节点 (single child);
  • 「Renderer Element」 —— 渲染型 Element,对应「Renderer Widget」,其不同的子类型包含的子节点个数也不一样,如:LeafRenderObjectElement 没有子节点,RootRenderObjectElement、SingleChildRenderObjectElement 有一个子节点,MultiChildRenderObjectElement 有多个子节点。

    原生型 Element,只有 MultiChildRenderObjectElement 是多子节点的,其他都是单子节点。

同时,可以看到,Element实现了BuildContext接口 —— 我们在 Widget 中遇到的context,其实就是该 Widget 对应的 Element。

关系


在继续之前有必要先了解一下 Element 与其他几个核心元素间的关系,以便在全局上有个认识。

如图:

  • Element 通过 parent、child 指针形成「Element Tree」;
  • Element 持有 Widget、「Render Object」;
  • State 是绑定在 Element 上的,而不是绑在「Stateful Widget」上(这点很重要)。

    上述这些关系并不是所有类型的 Element 都有,如:「Render Object」只有「RenderObject Element」才有,State 只有「Stateful Element」才有。

生命周期


Element 作为『实例』,随着 UI 的变化,有较复杂的生命周期:

  • parent 通过Element.inflateWidget->Widget.createElement创建 child element,触发场景有:UI 的初次创建、UI 刷新时新老 Widget 不匹配(old element 被移除,new element 被插入);

  • parent 通过Element.mount将新创建的 child 插入「Element Tree」中指定的插槽处 (slot);

    dynamic Element.slot——其含意对子节点透明,父节点用于确定其下子节点的排列顺序 (兄弟节点间的排序)。因此,对于单子节点的节点 (single child),child.slot 通常为 null。
    另外,slot 的类型是动态的,不同类型的 Element 可能会使用不同类型的 slot,如:Sliver 系列使用的是 int 型的 index,MultiChildRenderObjectElement 用兄弟节点作为后一个节点的 slot。
    对于「component element」,mount方法还要负责所有子节点的 build (这是一个递归的过程),对于「render element」,mount方法需要负责将「render object」添加到「render tree」上。其过程在介绍到相应类型的 Element 时会详情分析。

  • 此时,(child) element 处于 active 状态,其内容随时可能显示在屏幕上;

  • 此后,由于状态更新、UI 结构变化等,element 所在位置对应的 Widget 可能发生了变化,此时 parent 会调用Element.update去更新子节点,update 操作会在以当前节点为根节点的子树上递归进行,直到叶子节点;(执行该步骤的前提是新老 Widget.[key && runtimeType] 相等,否则创建新 element,而不是更新现有 element);

  • 状态更新时,element 也可能会被移除 (如:新老 Widget.[key || runtimeType] 不相等),此时,parent 将调用deactivateChild方法,该方法主要做了 3 件事:

    • 从「Element Tree」中移除该 element (将 parent 置为 null);
    • 将相应的「render object」从「render tree」上移除;
    • 将 element 添加到owner._inactiveElements中,在添加过程中会对『以该 element 为根节点的子树上所有节点』调用deactivate方法 (移除的是整棵子树)。
      1
      2
      3
      4
      5
      void deactivateChild(Element child) {
      child._parent = null;
      child.detachRenderObject();
      owner._inactiveElements.add(child); // this eventually calls child.deactivate()
      }
  • 此时,element 处于 “inactive” 状态,并从屏幕上消失,该状态一直持续到当前帧动画结束;

  • 从 element 进入 “inactive” 状态到当前帧动画结束期间,其还有被『抢救』的机会,前提是『带有「global key」&& 被重新插入树中』,此时:

    • 该 element 将会从owner._inactiveElements中移除;
    • 对该 element subtree 上所有节点调用activate方法 (它们又复活了!);
    • 将相应的「render object」重新插入「render tree」中;
    • 该 element subtree 又进入 “active” 状态,并将再次出现在屏幕上。

      上述过程经历这几个方法:Parent Element.inflateWidget–>Parent Element._retakeInactiveElement–>BuildOwner._inactiveElements.remove–>Child Element._activateWithParent

  • 对于所有在当前帧动画结束时未能成功『抢救』回来的「Inactive Elements」都将被 unmount;

  • 至此,element 生命周期圆满结束。

核心方法


下面对 Element 中的几个核心方法进行简单介绍:

updateChild

updateChild是 flutter framework 中的核心方法之一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}

if (child != null) {
if (child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
return child;
}

if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
assert(child.widget == newWidget);

return child;
}

deactivateChild(child);
assert(child._parent == null);
}

return inflateWidget(newWidget, newSlot);
}

在「Element Tree」上,父节点通过该方法来修改子节点对应的 Widget

根据传入参数的不同,有以下几种不同的行为:

  • newWidget == null —— 说明子节点对应的 Widget 已被移除,直接 remove child element (如有);
  • child == null —— 说明 newWidget 是新插入的,创建子节点 (inflateWidget);
  • child != null —— 此时,分为 3 种情况:
    • 若 child.widget == newWidget,说明 child.widget 前后没有变化,若 child.slot != newSlot 表明子节点在兄弟结点间移动了位置,通过updateSlotForChild修改 child.slot 即可;
    • 通过Widget.canUpdate判断是否可以用 newWidget 修改 child element,若可以,则调用update方法;
    • 否则先将 child element 移除,并通 newWidget 创建新的 element 子节点。

子类一般不需要重写该方法,该方法有点类似设计模式中的『模板方法』。

update

在更新流程中,若新老 Widget.[runtimeType && key] 相等,则会走到该方法。
子类需要重写该方法以处理具体的更新逻辑:

Element 基类

1
2
3
4
@mustCallSuper
void update(covariant Widget newWidget) {
_widget = newWidget;
}

基类中的update很简单,只是对_widget赋值。

子类重写该方法时必须调用 super.

StatelessElement

父类ComponentElement没有重写该方法

1
2
3
4
5
void update(StatelessWidget newWidget) {
super.update(newWidget);
_dirty = true;
rebuild();
}

通过rebuild方法触发重建 child widget (第 4 行),并以此来 update child element,期间会调用到StatelessWidget.build方法 (也就是我们写的 Flutter 代码)。

组合型 Element 都会在update方法中触发rebuild操作,以便重新 build child widget。

StatefulElement

1
2
3
4
5
6
7
8
9
10
11
12
void update(StatefulWidget newWidget) {
super.update(newWidget);
final StatefulWidget oldWidget = _state._widget;
_dirty = true;
_state._widget = widget;
try {
_state.didUpdateWidget(oldWidget) as dynamic;
}
finally {
}
rebuild();
}

相比StatelessElementStatefulElement.update稍微复杂一些,需要处理State,如:

  • 修改 State 的 _widget属性;
  • 调用State.didUpdateWidget (熟悉么)。

最后,同样会触发rebuild操作,期间会调用到State.build方法。

ProxyElement

1
2
3
4
5
6
7
8
9
10
11
12
13
void update(ProxyWidget newWidget) {
final ProxyWidget oldWidget = widget;
super.update(newWidget);
updated(oldWidget);
_dirty = true;
rebuild();
}

void updated(covariant ProxyWidget oldWidget) {
notifyClients(oldWidget);
}

Widget build() => widget.child;

ProxyElement.update方法需要关注的是对updated的调用,其主要用于通知关联对象 Widget 有更新。
具体通知逻辑在子类中处理,如:InheritedElement会触发所有依赖者 rebuild (对于 StatefulElement 类型的依赖者,会调用State.didChangeDependencies)。

ProxyElement 的build操作很简单:直接返回widget.child

RenderObjectElement

1
2
3
4
5
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
widget.updateRenderObject(this, renderObject);
_dirty = false;
}

RenderObjectElement.update方法调用了widget.updateRenderObject来更新「Render Object」(熟悉么)。

SingleChildRenderObjectElement

SingleChildRenderObjectElementMultiChildRenderObjectElementRenderObjectElement的子类。

1
2
3
4
void update(SingleChildRenderObjectWidget newWidget) {
super.update(newWidget);
_child = updateChild(_child, widget.child, null);
}

第 3 行,通过newWidget.child调用updateChild方法递归修改子节点。

MultiChildRenderObjectElement

1
2
3
4
void update(MultiChildRenderObjectWidget newWidget) {
super.update(newWidget);
_children = updateChildren(_children, widget.children, forgottenChildren: _forgottenChildren);
}

上述实现看似简单,实则非常复杂,在updateChildren方法中处理了子节点的插入、移动、更新、删除等所有情况。

inflateWidget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Element inflateWidget(Widget newWidget, dynamic newSlot) {
final Key key = newWidget.key;
if (key is GlobalKey) {
final Element newChild = _retakeInactiveElement(key, newWidget);
if (newChild != null) {
newChild._activateWithParent(this, newSlot);
final Element updatedChild = updateChild(newChild, newWidget, newSlot);
return updatedChild;
}
}

final Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);
return newChild;
}

inflateWidget 属于模板方法,故一般情况下子类不用重写。

该方法的主要职责:通过 Widget 创建对应的 Element,并将其挂载 (mount) 到「Element Tree」上。

如果 Widget 带有 GlobalKey,首先在 Inactive Elements 列表中查找是否有处于 inactive 状态的节点 (即刚从树上移除),如找到就直接复活该节点。

主要调用路径来自上面介绍的updateChild方法。

mount

当 Element 第一次被插入「Element Tree」上时,调用该方法。由于此时 parent 已确定,故在该方法中可以做依赖 parent 的初始化操作。经过该方法后,element 的状态从 “initial” 转到了 “active”。

Element

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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;

if (widget.key is GlobalKey) {
final GlobalKey key = widget.key;
key._register(this);
}

_updateInheritance();
}

还记得BuildOwner吗,正是在该方法中父节点的 owner 传给了子节点。
如果,对应的 Widget 带有 GlobalKey,进行相关的注册。
最后,继承来自父节点的「Inherited Widgets」。

子类重写该方法时,必须调用 super。
关于「Inherited Widgets」,后文会详细分析

ComponentElement

1
2
3
4
5
6
7
8
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_firstBuild();
}

void _firstBuild() {
rebuild();
}

组合型 Element 在挂载时会执行_firstBuild->rebuild操作。

RenderObjectElement

1
2
3
4
5
6
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
_dirty = false;
}

RenderObjectElement.mount中做的最重要的事就是通过 Widget 创建了「Render Object」(第 3 行),并将其插入到「RenderObject Tree」上 (第 4 行)。

SingleChildRenderObjectElement

1
2
3
4
5
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_child = updateChild(_child, widget.child, null);
}

SingleChildRenderObjectElement在 super (RenderObjectElement) 的基础上,调用updateChild方法处理子节点,其实此时_childnil,前面介绍过当 child 为nil时,updateChild会调用inflateWidget方法创建 Element 实例。

MultiChildRenderObjectElement

1
2
3
4
5
6
7
8
9
10
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_children = List<Element>(widget.children.length);
Element previousChild;
for (int i = 0; i < _children.length; i += 1) {
final Element newChild = inflateWidget(widget.children[i], previousChild);
_children[i] = newChild;
previousChild = newChild;
}
}

MultiChildRenderObjectElement在 super (RenderObjectElement) 的基础上,对每个子节点直接调用inflateWidget方法。

markNeedsBuild

1
2
3
4
5
6
7
8
9
10
void markNeedsBuild() {
if (!_active)
return;

if (dirty)
return;

_dirty = true;
owner.scheduleBuildFor(this);
}

markNeedsBuild方法其实在介绍BuildOwer时已经分析过,其作用就是将当前 Element 加入_dirtyElements中,以便在下一帧可以rebuild。
那么,哪些场景会调用markNeedsBuild呢?

  • State.setState —— 这个在介绍 Widget 时已分析过了;
  • Element.reassemble —— debug hot reload;
  • Element.didChangeDependencies —— 前面介绍过当依赖的「Inherited Widget」有变化时会导致依赖者 rebuild,就是从这里触发的;
  • StatefulElement.activate —— 还记得activate吗?前文介绍过当 Element 从 “inactive” 到 “active” 时,会调用该方法。为什么StatefulElement要重写activate?因为StatefulElement有附带的 State,需要给它一个activate的机会。

子类一般不必重写该方法。

rebuild

1
2
3
4
5
6
void rebuild() {
if (!_active || !_dirty)
return;

performRebuild();
}

该方法逻辑非常简单,对于活跃的、脏节点调用performRebuild,在 3 种场景下被调用:

  • 对于 dirty element,在新一帧绘制过程中由BuildOwner.buildScope
  • 在 element 挂载时,由Element.mount调用;
  • update方法内被调用。

上述第 2、3 点仅「Component Element」需要

performRebuild

Element 基类中该方法是no-op

ComponentElement

1
2
3
4
5
6
void performRebuild() {
Widget built;
built = build();

_child = updateChild(_child, built, slot);
}

对于组合型 Element,rebuild 过程其实就是调用build方法生成「child widget」,再由其更新「child element」。

StatelessElement.build: Widget build() => widget.build(this);
StatefulElement.build: Widget build() => state.build(this);
ProxyElement.build: Widget build() => widget.child;

RenderObjectElement

1
2
3
4
void performRebuild() {
widget.updateRenderObject(this, renderObject);
_dirty = false;
}

在渲染型 Element 基类中只是用 Widget 更新了对应的「Render Object」。
在相关子类中可以执行更具体的逻辑。

生命周期视角

至此,Element 的核心方法基本已介绍完,是不是有点晕乎乎的感觉?inflateWidgetupdateChildupdatemountrebuild以及performRebuild等你中有我、我中有你,再加上不同类型的子类对这些方法的重写。

下面,我们以 Element 生命周期为切入点将这些方法串起来。
对于一个 Element 节点来说在其生命周期内可能会历经几次『重大事件』:

  • 被创建 —— 起源于父节点调用inflateWidget,随之被挂载到「Element Tree」上, 此后递归创建子节点;

  • 被更新 —— 由「Element Tree」上祖先节点递归传递下来的更新操作,parent.updateChild->child.update

  • 被重建 —— 被调用rebuild方法(调用场景上面已分析);

  • 被销毁 —— element 节点所在的子树随着 UI 的变化被移除。

依赖 (Dependencies)


在 Element 基类中有这样两个成员:

1
2
Map<Type, InheritedElement> _inheritedWidgets;
Set<InheritedElement> _dependencies;

它们是干嘛用的呢?

  • _inheritedWidgets —— 用于收集从「Element Tree」根节点到当前节点路径上所有的「Inherited Elements」;
    前文提到过在mount方法结束处会调用_updateInheritance
    以下是 Element 基类的实现,可以看到子节点直接获得父节点的_inheritedWidgets
    1
    2
    3
    void _updateInheritance() {
    _inheritedWidgets = _parent?._inheritedWidgets;
    }

以下是InheritedElement类的实现,其在父节点的基础上将自己加入到_inheritedWidgets中,以便其子孙节点的_inheritedWidgets包含它 (第 8 行):

1
2
3
4
5
6
7
8
9
void _updateInheritance() {
final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
if (incomingWidgets != null)
_inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
else
_inheritedWidgets = HashMap<Type, InheritedElement>();

_inheritedWidgets[widget.runtimeType] = this;
}

  • _dependencies —— 用于记录当前节点依赖了哪些「Inherited Elements」,通常我们调用context.dependOnInheritedWidgetOfExactType<T>时就会在当前节点与目标 Inherited 节点间形成依赖关系。

    在 Element 上提供的便利方法of,一般殾会调用dependOnInheritedWidgetOfExactType

同时,在InheritedElement中还有用于记录所有依赖于它的节点:final Map<Element, Object> _dependents
最终,在「Inherited Element」发生变化,需要通知依赖者时,会利用依赖者的_dependencies信息做一下 (debug) check (第 4 行):

1
2
3
4
5
6
7
void notifyClients(InheritedWidget oldWidget) {
for (Element dependent in _dependents.keys) {
// check that it really depends on us
assert(dependent._dependencies.contains(this));
notifyDependent(oldWidget, dependent);
}
}

小结

至此,Element 相关的内容基本已介绍完。总结提炼一下:

  • Element 与 Widget 一一对应,它们间的关系就像 object 与 json;
  • 只有「Render Element」才有对应的「Render Object」;
  • Element 作为 Widget 与 RenderObejct 间协调者,会根据 UI(「Widget Tree」) 的变化对「Element Tree」作出相应的调整,同时对「RenderObject Tree」进行必要的修改;
  • Widget 是不可变的、无状态的,而 Element 是有状态的。

最后,强烈推荐Keys! What are they good for?这篇文章,对于理解本文相关的内容有很大的帮助。