Flutter framework之Element

335 阅读12分钟

最近正在写一个思维导图Widget,但是遇到了一个很诡异的bug,猜测问题出在自定义的Element上。然而看了好几天掘金的文章也没看明白,最后还是决定亲自分析一下。

本文基于flutter 3.29.0

Element class - widgets library - Dart API

image.png

1.Element的职责

很多文章都会说

ElementFlutter的树中特定位置的Widget的实例化。

这句话真的很官方,因为它就是从官方注释直接翻译过来的。但是我觉得大多数人看到这个定义时,可能和看到同济高数里那些难懂的定义一样,仍然不太清楚Element到底有什么用。

我自己分析了一下,认为Element

  1. 是一个“箱子”,负责储存Widget、管理RenderObject(如果有),负责把二者关联起来
  2. 是一个“中介”,flutter在构建界面时是通过管理Element来实现的。

2.Element中的的主要对象和方法

1. _parent

Element按树形结构被组织起来,除根节点外,每个element对象都要保存其父节点

2. _widget

创建这个Element时用到的widget对象。有些文章会说widget负责配置Element,这句话其实不太准确。

widget负责配置Element主要体现在:widget会根据自己类型的不同创建不同类型的Element子类。 比如StatefulWidget会创建StatefulElementRenderObjectWidget会创建RenderObjectElement

通常来讲,我们在写widget时用到的那些属性(比如Text里的字符串和textStyle,Column里的mainAxisSizespacing...)是用来配置RenderObject的。Element大多数时候不会用到widget,只会把它储存起来,这点自定义过RenderBox的同学可能比较清楚。

3. _depth

Element按树形结构被组织起来,代表该element对象在树中的深度。刷新时会按从父到子刷新。

4. _slot

有些文章会说slot代表该element在父节点中的位置信息,这句话其实不太准确。

slot的具体值是没有意义的,当slot相对旧值发生改变时,说明它的位置变化了,具体变到哪和slot无关。

就像hashCode或md5abhashCode不相同说明它们在内存中的位置不同,但是hashCode这串数字没有意义。另外,slotelement其实没什么意义,它的作用是在自己更新时触发renderObject.move方法,最终调用markNeedsLayout()去重新计算布局。

5. _ownerbuildScope (不是重点)

_ownerBuildOwner实例,全局唯一。它负责

  1. 记录需要被刷新的节点(实际上交给BuildScope完成)
  2. 管理不活跃的Element对象,以在合适的时机将它们回收
  3. 管理GlobalKey和其绑定的Element,以在合适的时机复用它们

buildScopeBuildScope实例,子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,也就是StatelessWidgetStatefulWidget对应的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按树形结构被组织起来,那创建子节点的任务自然就交给了父节点来完成。

在父节点

  1. 被挂载到树上,即父节点自己的mount方法被调用时
  2. 有新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方法中,需要

  1. 记录自己的父节点_parent和位置_slot,更新自己的深度
  2. 将自己的状态更新为active,表示被激活,随时都可能被显示
  3. 记录父节点的BuildOwnerBuildScope
  4. 更新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方法

StatelessElementStatefulElement都是ComponmentElement的子类,会触发一次performRebuild方法,performRebuild又会调用widgetbuild方法,创建出自己的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额外做了几件事情:

  1. 调用Widget.createRenderObject方法,创建RenderObject
  2. 找到上级的RenderObjectElement,调用它的insertRenderObjectChild方法,把自己创建的RenderObject插入到树中。对应RenderObjectinsertsetChild方法
  3. 更新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,还会调用其statedispose方法

RenderObjectElementunmount方法

会额外调用自己管理的RenderObjectdispose方法

@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;
  }
}
  1. 如果没有需要刷新的element,也没有回调要执行,就跳过过程
  2. 否则,更新标志位,表示刷新开始,并执行回调callback。回调可能会重新将一部分Element标记为需要刷新的状态(可以参考ListView的源码)
  3. 调用buildScope._flushDirtyElements方法,刷新所有需要刷新的Element
  4. 更新标志位,表示刷新完成

题外话:对于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();
}
  1. BuildScope中,首先会对element对象按深度排序,保证先刷新父节点,再刷新子节点
  2. 如果element.buildScope和本buildScope是同一个对象,执行_tryRebuild方法,刷新自己。_tryRebuild会调用elementrebuild方法
  3. 更新被刷新的element的状态,表示他们已经不在脏列表中
  4. 清空脏列表,更新标志位,表示本BuildScope的刷新工作已完成
  5. todo: 什么时候BuildScope会不一致?

5.Element内部如何处理更新:rebuild & update

5.1 rebuildperformRebuild

rebuild方法只是简单的调用了performRebuild方法,Element的子类会重写这个方法。还是只看最常用的两个Element子类。

简单来说,rebuild方法执行完后,Widget树已经被更新到最新的状态,同时RenderObject树也被更新完毕

5.1.1 ComponentElement:更新widget树,并更新对应的Element

  1. 调用ComponentElement.build()方法创建出新的child。对于StatelessElement,build方法就是widget.build;对于StatefulElement就是state.build()
  2. 使用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

调用widgetupdateRenderObject(),以设置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对象

  1. child != null, newWidget == null:不再使用子element
  2. child.widgetnewWidget是相同的对象:只尝试更新slot
  3. child.widgetnewWidget不是同一个对象,但是类型和key相同,执行child.update方法更新
  4. child.widgetnewWidget均不为空,但类型或key不同:无法更新,移除旧child,创建新element
  5. child == 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 StatelessElementStatefulElement

会直接触发自己的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

Element中刷新的旧逻辑

RenderBox中布局的逻辑

RenderBox中布局的逻辑

为什么动一下屏幕,被删除的节点就不再显示了?

image.png

6.3 对于BuildScope不相同的情况是怎么处理的?

见 4.2.2,只有使用LayoutBuilder时会出现这种情况。

LayoutBuilderRenderLayoutBuilderperformLayout阶段调用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);
}

断断续续写了一周多,我是真能拖啊。。。就这么多吧,希望没啥问题