4.学习Flutter -- Element 的作用

245 阅读11分钟

通过一系列文章记录一下学习 Flutter 的过程,总结一下相关知识点。

  1. 学习Flutter -- 框架总览
  2. 学习Flutter -- 启动过程做了什么
  3. 学习Flutter -- Widget 的组成
  4. 学习Flutter -- Element 的作用
  5. 学习Flutter -- RenderObject 布局过程
  6. 学习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 有两个主要职责,分别是:

  1. 根据 Widget 树的变化来维护 Element 树,包括节点的插入、删除、更新等;

  2. 作为 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 进行更新。