Flutter中的Tree(二) ElementTree

什么是Element?

Element是Flutter UI的示例. 上一篇文章中我们说Widget是配置文件, 那么Element就是通过配置文件生成的Model.

Element有两个职责: 根据Widget的变化来维护ElementTree(Element的增删改和移动); 作为Adapter, 协调Widget和RenderObject.

Element的生命周期

上图是Element的生命周期示意图.该图的说明如下:

Element的inflateWidget(Widget newWidget, Object newSlot)方法按照下面三种情况分别处理:

  1. newWidget没有Key或者Key不是GlobalKey. 先通过Widget的createElement()方法创建对应的新Element, 此时新Element处于生命周期的init阶段; 然后通过新Element的mount(Element parent, Object newSlot)方法, 将Element指定为新Elment的parent, 同时扭转新Element的生命周期为active阶段.

  2. newWidget的Key是GlobalKey, 但是这个GlobalKey没有被注册过. 操作同情况1, 只不过mount(Element parent, Object newSlot)方法在指定parent和扭转生命周期的同时, 还注册了GlobalKey.

  3. newWidget的Key是GlobalKey, 但是这个GlobalKey已经被注册了. 这时先通过GlobalKey找到这个Key正在标识的Element(为防止混淆, 称其为旧Element), 通过调用其parent的deactiveChild(Element child)方法来间接调用其deactive()方法, 将其生命周期扭转为inactive状态. 然后调用旧Widget的_activateWithParent(Element parent, Object newSlot), 将Element指定为旧Element的parent, 通知间接调用旧Element的active()方法, 将其生命周期扭转为active. 然后调用Element的updateChild(Element child, Widget newWidget, Object newSlot)方法来更新旧Element.

Element的update()方法, 根据其具体的实现和当时的状态, 有三种可能:

  1. 调用Element的inflateWidget(Widget newWidget, Object newSlot)方法

  2. 调用Element的deactiveChild(Element child)方法

  3. 调用child的update()方法

显然, 上述情况中, 只有情况3不会改变任何child的生命周期状态.

每一帧结束时, BuildOwner(下文会介绍到)会通过自己的finalizeTree()方法简介调用所有inacitve的Element的unmount()方法, 将其从ElementTree上移除, 同时将这些Element的生命周期扭转为defunct阶段. 至此, 这些Element的生命周期就结束了

最后需要强调的时, Element的deactive()方法和activate()方法, 影响的都是以这个Element为根节点的Element子树.

Element及其派生类的核心成员变量和方法

上图是Element的继承树. 我们按照深度优先的顺序来依次介绍

Element

关于Element这个抽象类, 要单独说的是它实现的BuildContext接口. BuildContext接口我们在build()方法里见过太多了, 实际上BuildContext就是Element, BuildContext这个接口提供的就是通过当前Element向上遍历Element Tree的能力. 一些常见的方法, 比如dependOnInheritedWidgetOfExactType()、findAncestorWidgetOfExactType()等都是BuildContext定义, Element实现的.

_slot

_slot是child在其parent的child list里的次序标识(parent Element通过_slot来给child排序). 不同的Element, _slot的类型不同. 比如SilverElement的_slot就是int类型的index; MultiChildRenderObjectElement的_slot是一个_IndexSlot类. 这个类是一个二元组, 第一个参数index, 这个index指的是对应的Widget的children参里的index; 第二个参数是一个Element, 指的是在这个Element前面的兄弟Element.

inflatedWidget()

inflateWidget方法的逻辑在Element的生命周期部分里已经说得很详细了, 这里不再着墨.

mount()

Element的mount(Element parent, Object slot)方法在Element的生命周期部分里提到了干了两件事: 给Element设置parent和slot以及注册新的GlobalKey.

除此之外, 其实mount(Element parent, Object slot)方法还干了一件事: 从parent那里获得BuildOwner. 关于BuildOwner, 下面会有更详细的介绍.

另外要说的是, 不同的Element子类也会通过重写的方式在mount(Element parent, Object slot)方法中增加一些自己的逻辑. 比如Compinent会在这时调用_firstBuild()方法; RenderObjectElement会在这时创建对应的RenderObject; SingleChildRenderObjectElement和MultiChildRenderObjectElement会在这个时候调用inflatedWidget()构建自己的child.

update()

update(Widget newWidget)方法, 顾名思义, 在Element对应的Widget发生变化时(即rumtimeType和Key均不相等时)被调用. 一般的Element都只是在这里简单调用rebuild()方法来更新自己的状态, 但是有些Element还有一些额外的逻辑. 比如StatefulElement会在这里调用State的didUpdatedWidget()方法; ProxyElement会在这时调用nofityClients()方法.

updateChild()

updateChild(Element child, Widget newWidget, Object newSlot)方法在Element的生命周期部分里也说过, 是parent用来根据最新的Widget和slot更新child的策略. 方法的逻辑可以用下面的表格来描述:

newWidget == null newWidget != null
child == null 不做任何处理, 返回null 创建一个新的Element
child != null 移除child, 返回null 进一步判断

当child!=null且newWidget!=null时, 可能有以下情况:

  1. child.widget == newWidget. 这时只更新child的slot, 返回child

  2. 通过Widget.canUpdate(Widget oldWidget, Widget newWidget)方法来判断child.widget能否被newWidget更新. 如果能那么就直接更新, 同时更新child的slot, 返回child

  3. Widget.canUpdate(Widget oldWidget, Widget newWidget)的结果是child.widget不能被newWidget更新. 此时移除child, 根据newWidget和newSlot创建一个新的Element并返回

_buildOwner

前面提到了好几次BuildOwner. 这里我们就来介绍一下它的作用.

BuildOwner的职责有两个: 收集所有被标记为dirty的Element, 在下一帧时rebuild它们; 收集并清理所有处于inactive的Element. inactive Elment的收集我们在Element的生命周期那里已经提到过了; dirty Element的收集在下面会讲到.

关于BuildOwner还有一点要说的时, 对于一颗Element Tree, BuildOwner()是唯一的. 即Element Tree上的所有Element共享同一个BuildOwner. 这个BuildOwner实例由WidgetsBinding创建, 传递给Element Tree的根节点: RenderObjectToWidgetElement. 随后, Element Tree被逐步创建, 这个BuildContext示例在mount(Element parent, Object slot)方法中被parent分享给了child.

最后要说的是, 虽然一颗Element Tree只有一个BuildOwner, 但是因为一个Flutter APP中可能有不止一颗Element Tree, 所以BuildOwner也可能不止有一个示例. 为什么说可能有不止一颗Element Tree呢? 因为只要你学会了这个系列, 你就可以自己把这几颗树捏出来, 只不过它们不会参与渲染而已(google管这些树叫off-screen tree).

google官方给出了一个off-screen的例子, 大家可以点链接进去看看(强烈建议学完整个系列后再进去看)链接

_inheritedWidgets和_updateInheritance()

上一篇文章中我们对InheritedWidget的实现原理卖了个关子, 现在我们在这里做一个说明.

首先, _inheritedWidgets是一个<Type, InheritedElement>类型的Map. 其中, T是继承自InheritedWidget的Widget的runtimeType. 为什么map的名字是_inheritedWidgets, Map的value却是一个Element呢? 因为Widget本身是随时更新的, Element是唯一且始终持有最新的Widget的.

或者我们可以这么理解: InheritedWidget是我们想要的物品, InheritedElement是物品的包装箱, Type是包装箱的标签, 这个Map是放置包装箱的货架. 我们通过标签来得知那个包装箱装着我们想要的物品, 找到包装箱后我们就可以取到物品了.

那么这个map是怎么更新的呢? 答案在_updateInheritance()这个方法里. 这个方法只在Element的mount()和active()方法中被调用, 因为只有这两个方法被调用的时候, Element的parent才会更新, child才可以从parent里继承parent的_inheritedWidgets(如果child自己是InheritedElement的话, 继承完后还要把自己加入到这个map里). 这个过程和BuildOwner的传递过程思路是一致的.

markNeedsBuild()

在介绍BuildOwner的时候我们没有说BuildOwner是怎么收集dirty Element的. 其实就是通过Element的markNeedsBuild(). 它将自己的_dirty属性标记为true, 然后调用BuildOwner示例的scheduleBuildFor(Element element)方法, 将自己加入到BuildOwner的dirty Element List中. 为什么要设置一个_dirty标记呢? 因为BuildOwner的dirty Element List在添加Element的时候没有判重(实际上也没有办法证明两个Element是否是同一个Element, 因为不同的Element, Widget等属性可能完全相同), 所以需要做一个标记, 防止被重复添加.

调用markNeedsBuild()的场景有下面四种:

  1. State调用setState的时候

  2. hot reload的时候

  3. Element依赖的Inherited Widget发生变化的时候

  4. StatefulElement重新active的时候

rebuild()和performRebuild()

rebuild()方法, 实际上是performRebuild()方法的一个预处理, 其主要作用就是保证只有被标记为dirty的active Element才会执行performRebuild()方法.

performRebuild()是Element类定义的一个模板方法, 不同的Element有不同的逻辑实现. 对于ComponentElement, 就是通过其build()方法得到一个Widget, 再以这个Widget创造一个Element作为自己的child; 对于RenderObjectElement, 就是通过最新的Widget来更新RenderObject.

ComponentElement

ComponentElement是StatefulWidget, StatelessWidget和ProxyWidget及其子类创建的Element. 谷歌对它的定义是, “通过组合其他Element间接而非直接创建RenderObject的Element”. 基于这个定义, ComponentElement定义了两个方法: _firstBuild()和build(), 用来创建其他Element, 从而间接创建RenderObject.

ComponentElement有三个抽象子类, StatefulElement, StatelessElement和ProxyElement.

_firstBuild()

对于大多数ComponentElement, _firstBuild()方法只是简单调用了build()方法. StatefulElement重写了这个方法, 在_firstBuild()前先调用了State的initState()和didChangeDependencies()方法.

build()

在前面说到, build()是ComponentElement定义的一个模板方法, 旨在提供一个Widget, 以便在performRebuild()方法中创建Element. ComponentElement的三个抽象子类各自有自己的实现: StatelessElement直接调用了StatelessWidget的build(BuildContext context); StatefulElement直接调用了State的build(BuildContext context); ProxyElement则是直接返回了ProxyWidget的child参数.

StatelessElement

作为最简单的ComponentElement, StatelessElement没有任何的逻辑重写, 只是新增了一个我们耳熟能详的build(BuildContext context)方法. 所以这里就不多着墨了.

StatefulElement

StatefulElement的新增逻辑前面基本上都提到了, 这里我们就主要说一下State.

_state

正如类图中描述的, State是被StatefulElement持有的(同时State也持有Element). 它在StatefulElement被mount的时候通过StatefulWidget的createState()方法创建. 一个State实例一旦创建, 将和Element永远绑定, 直到生命周期结束.

上图是State的生命周期, 虽然State的生命周期类_StateLifecyle和Element的生命周期类_ElementLifecycle并不完全一致, 但State的生命周期回调和Element是一模一样的.

ProxyElement

ProxyElement没有新增什么复杂的逻辑, 只是定义了一个供子类实现的模板方法notifyClients(), 这个方法执行的时机是ProxyElement的Widget发生变化后, 即update(Widget newWidget)方法中. 事实上, update(Widget newWidget)方法是通过调用ProxyElement新定义的updated(Widget newWidget)方法来间接调用notifyClients()的. 这样设计的意义在于, 在不改动Element内部使用的update(Widget newWidget)方法的前提下, 为我们提供了一个决定是否调用notifyClients()的时机. 如果我们想实现复杂的更新逻辑, 只需要重写updated()方法就可以了.

ParentDataElement

ParentDataElement是由ParentDataWidget创建的Element, 它有一个泛型标记, ParentData我们下篇文章再讲. ParentDataElement主要新增的逻辑为notifyClients()的自实现: _applyParentData().

_applyParentData()的逻辑很简单: 遍历自己所有的child, 如果child是RenderObjectElement, 就通过ParentDataWidget.applyParentData(RenderObject renderObject)方法来更新RenderObject的parentData; 如果child不是RenderObjectElement, 那么就递归查找下去, 直到找到一个RenderObjectElement为止.

InheritedElement

InheritedElement是由InheritedWidget创建的Element. 它的新增逻辑为notifyClients()的自实现: notifyDependent().

notifyDependent()的逻辑也很简单: InheritedElement维护了一个<Element, Object>的Map: _dependents. Map的Key值是通过BuildContext::dependOnInheritedWidgetOfExactType({Object aspect})来依赖这个InheritedElement的Element, Value值默认是null. 我们可以通过重写InheritedElement的updateDependencies(Element dependent, Object aspect)方法, 调用setDependencies(Element dependent, Object aspect), 将dependOnInheritedWidgetOfExactType的可选参数aspect存进Map里备用(那么为什么updateDependencies方法不直接把存aspect, 而是默认存null呢? 明明可选参数默认也是null呀).

那么, 存进去的这个aspect有什么用呢? InheritedElement实际上并没有用到, 它只是提供了一个getDependencies(Element dependent)方法来拿到aspect, 具体怎么用可以随我们发挥. google在Flutter框架中写了一个Example: InheritedModelWidget和InheritedModelElement. InheritedModelWidget提供了updateShouldNotifyDependent(InheritedModel oldWidget, Set dependencies)方法, 允许我们通过aspect的值来决定是否更新Widget. 这个Example的实现逻辑并不复杂, 大家可以去源码里看一下.

RenderObjectElement

ComponentElement是间接创建RenderObject的Element, 那么RenderObjectElement当然就是直接创建RenderObject的Element了. RenderObjectElement是由RenderObjectWidget创建的Element. 同Widget一样, 它也有LeafRenderObjectElement, SingleChildRenderObjectElement和MultiChildRenderObjectElement三种子类. 还是同Widget一样, RenderObjectElement是唯一参与渲染的一类Element. 页面的”渲染元素”–RenderObject, 就是RenderObjectElement在mount时创建的. 显然, RenderObject的增删改挪和RenderObjectElement的增删改挪是同步的.

关于RenderObjectElement, 有一个特殊的抽象子类需要提一下: RootRenderObjectElement. 顾名思义, 这个类代表的是Element Tree的根节点. 它的实现类为前面提到过的RenderObjectToWidgetElement.