vlambda博客
学习文章列表

学 Flutter 不理解 Widget/Element/Render 三棵树?啥也不是!

一、导语

Hi,大家好,这里是承香墨影!

Flutter 是 Google 发布的跨平台 UI 框架,而其中与 UI 相关的,最重要的就是 Widget & Element & RenderObject 这三棵树。

初入门的时候,会以为三棵树的节点,都是一一对应的,后来才知道并非如此。

我们在代码中写的 Widget,在运行时,会依据使用的属性「膨胀」出多个子节点结构,而他们才是真实的与 Element 对应的结构,而到了 RenderObject 又被收敛了,只有绘制类的 RenderObjectWidget 才会最终生成 RenderObject 节点,组成 RenderObject Tree,渲染到 UI 上显式。

而这一些的细节 Flutter Framework 都帮我们实现了,我们只需要跟着代码的流程去了解即可。

学 Flutter 不理解 Widget/Element/Render 三棵树?啥也不是!
承香墨影
我是承香墨影,8 年技术老司机。在这里,主要分享我个人的原创内容,不仅限于技术,职场、产品、设计思想等等,统统都有。这里已经汇集了有很多小伙伴了,欢迎你加入!
328篇原创内容
Official Account

二、引言

树是学习数据结构必学的一种结构,Flutter 采用了这种数据结构作为 UI 体系构建的核心。如果你已经有一定 Flutter 开发经验,一定接触到过这个概念,我们可以在各类文章中看到类似的介绍(图片来自文章《Flutter 的三棵树渲染机制和原理》)。

学 Flutter 不理解 Widget/Element/Render 三棵树?啥也不是!

当看到这个图,可能我们也默认为,理解了这个知识点,但实际上这三颗树真的长这样么?他们究竟是如何形成这样的结构?

本文将会从一个简单的例子,追溯源码一一揭晓这个过程。

先来看一个极简的 demo。

学 Flutter 不理解 Widget/Element/Render 三棵树?啥也不是!

Container 设置了绿色背景,一个 Cloumn 下套了四个子节点。

2.1 Widget 树

首先我们来看看 Widget 树,实际上在代码运行过程中,没有明确的 Widget 树的概念,这棵树更多的是我们在开发过程中,对 Flutter 嵌套结构的描述。

而根据代码我们可以认为 Widget 树长这个样子。

学 Flutter 不理解 Widget/Element/Render 三棵树?啥也不是!

2.2 Element 树的构建过程

Element 树可以说是最重要的结构,它桥接了我们开发中的 Widget,与实际渲染的 RenderObject 对象。

根据网上大多数文章的描述,可能我们会认为这个例子中 Element 树与 Widget 树一样。

学 Flutter 不理解 Widget/Element/Render 三棵树?啥也不是!

如果你也是这么认为的,那么恭喜你,回答错误!

这棵树究竟该长什么样,我们一步步分析。

纵观 Flutter 的 UI 体系,Widget 可以分为 3 类:组合类(紫色)、代理类(红色)、绘制类(黄色)。

学 Flutter 不理解 Widget/Element/Render 三棵树?啥也不是!

Flutter 中比较熟悉的 StatelessWidget & StatefulWidget,都属于组合类的 Widget,实际上他们并不负责绘制,仅仅起到组合子 Widget 的作用。

而我们在屏幕上看到的 UI,最终都会通过绘制类的 WidgetRenderObjectWidget 实现。

RenderObjectWidget 中有个 createRenderObject() 方法,会生成实际渲染的 RenderObject 对象。

在我们的例子中,Container 和 Text 就是组合类的 Widget,这类 Widget 我们可以通过查看他的 build() 方法,梳理他的嵌套关系。

我们的例子中,Container 和 Text 其实都是组合类 Widget。

学 Flutter 不理解 Widget/Element/Render 三棵树?啥也不是!

查看 Container 的 build() 方法,从源码得知,Container 嵌套的层级与我们设置的属性有关。

我们为 Container 设置了 childcolor 属性,对应会给我们的 child 嵌套一个 DecoratedBox(渲染类 Widget)。而 Text 组件内部只是返回了一个 RichText(渲染类 Widget)。

得到以下结构(发现了没,叶子节点一定是渲染类的 Widget,因为这样才会被渲染到屏幕上)。

学 Flutter 不理解 Widget/Element/Render 三棵树?啥也不是!

墨影说(id:cxmyDev):大家不要强记这个结构,实际上在 1.22.x 之后的 Flutter 版本中,Container 的 Color 属性,会被封装为 ColoredBox 而不是 DecoratedBox,拆分的更细了。

虽然实现细节在变,唯一不变的是 Widget 构造的思想,我们代码中的 Widget,与真实的运行时 Widget,并不一样。组合类型的 Widget 会在 build() 过程中,通过包装的方式,创建合适的 Widget,这也符合单一职责的设计思想。

那我们实际的 Element 树,会长什么这个样子么?

这已经快接近真相了。但不太准确,因为实际上并不存在 Container 类型的 Element,所以我们看看 Element 对象究竟有哪些类型。

学 Flutter 不理解 Widget/Element/Render 三棵树?啥也不是!

这是 Element 的结构关系。其中 ComponentElement 对应组合类 Widget(Stateless/Stateful 以及所有他们的子类 Container、Text),ProxyElement 对应代理类 Widget,而 RenderObjectElement 则对应渲染类的 Widget(例如 RichText、Column)。

所以更加准确的 Element 树结构应该是这个样子:

学 Flutter 不理解 Widget/Element/Render 三棵树?啥也不是!

为什么还差一丢丢,看以下分析。

这棵树是如何形成的?核心的方法在他们共同的祖先 Element.mount() 方法中(代码已尽量省略无关的逻辑)。

/// Add this element to the tree in the given slot of the given parent.
///
/// The framework calls this function when a newly created element is added to

/// the tree for the first time. Use this method to initialize state that
/// depends on having a parent. State that is independent of the parent can
/// more easily be initialized in the constructor.
///
/// This method transitions the element from the "initial" lifecycle state to

/// the "active" lifecycle state.
@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;
  _updateInheritance();
}

上面的注释告诉了我们,这个方法的作用,是将当前的 element 对象添加到树中父节点的 solt 位置上。

方法里,我们看到他将当前 Element 对象的 _parent = parent,所以这里其实是将子节点的 _parent 属性,指向与父节点,并且深度 +1。

而对于组合类的 Widget:Stateful/Stateless 重写了 mount() ,还会调到下面的方法 performRebuild()

@override
void performRebuild() {
  Widget built;
  built = build();
  _child = updateChild(_child, built, slot);
}

如果这棵树是第一次构建的情况下,会走 inflateWidget() 逻辑生成子节点,并且调用子节点的 mount(),将其插入树中(因为组合类的 Widget 中会嵌套多层结构,但是 build() 最终只会返回一个 child,将 child 插入树中)。

Element inflateWidget(Widget newWidget, dynamic newSlot) {
  final Element newChild = newWidget.createElement();
  //将子节点插入到树中
  newChild.mount(this, newSlot);
  return newChild;
}

RenderObjectElement 类似,我们放到下面解析。

所以这里更准确的 Element 树如下

学 Flutter 不理解 Widget/Element/Render 三棵树?啥也不是!

好家伙,原来只是多了一些节点间的引用关系,而且中间那一层关系怎么和以前不太一样了?(自行查看 Cloumn 对应 Element 的 mount()

2.3 RenderObejct 树的构建过程

那么 RenderObjectTree 会长什么样子呢?

我们暂且按下不表,先聊点题外话,一开始大家可能会搞混 RenderObjectWidget 和 RenderObject,RenderObjectElement 三个对象之间的关系。

我们简单看一下,首先 RenderObjectWidget 是 Widget(好像一句废话)。例如 SizeBox、Cloumn 等。RenderObjectElement 是这类 Widget 生成的 Element 类型,例如 SizeBox 对应 SingleChildRenderObjectElement(单子节点的 Element),而 RenderObject 才是真正负责绘制的对象,其中包含了 paint()layout() 等方法。

接着分析,我们刚才说了整个树核心的逻辑就在 mount() 这儿,所以们查看 RenderObjectElement.mount()

@override
void mount(Element parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _renderObject = widget.createRenderObject(this);
  attachRenderObject(newSlot);
  _dirty = false;
}

这里执行了 super.mount(parent, newSlot),即上面的 mount() 流程之后,执行 attachRenderObject(newSlot)

@override
void attachRenderObject(dynamic newSlot) {
  _slot = newSlot;
  //查询当前最近的RenderObject对象
  _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
  //将当前节点的renderobject对象插入到上面找到RenderObjecte下面
  _ancestorRenderObjectElement?.insertChildRenderObject(
      renderObject, newSlot);
  //................../
}

其中 findAncestorRenderObjectElement()

RenderObjectElement _findAncestorRenderObjectElement() {
  Element ancestor = _parent;
  while (ancestor != null && ancestor is! RenderObjectElement)
    ancestor = ancestor._parent;
  return ancestor;
}

这其实就是一个很简单的死循环。

我们可以看到,循环退出的条件有两个:

  1. ancestor 父节点为空;
  2. 父节点是 RenderObjectElement;

说明这个方法,会找到离当前节点最近的一个 RenderObjectElement 对象, 之后执行该对象的 insertChildRenderObject(renderObject, newSlot)执行这个方法的是当前节点的第一个 RenderObjectElement 类父节点), 然而这是一个抽象方法,我们查看两个类里面的重写逻辑。

1. SingleChildRenderObjectElement

@override
void insertChildRenderObject(RenderObject child, dynamic slot) {
  final RenderObjectWithChildMixin<RenderObject> renderObject = this
      .renderObject;
  renderObject.child = child;
}

如果找到节点是 SingleChildRenderObjectElement 的的话,过程非常简单,将父 RenderObjectElement 对象的 renderObject 取出,并且让其 child 属性等于我们之前传入的 RenderObject。

注意这里是 RenderObject 的 child 属性,并不是 element 的 child 属性,所以在这里将自己的 RenderObject 挂在了 RenderObject 树上。

以 demo 中的 Column 为例就是下面这个过程:

学 Flutter 不理解 Widget/Element/Render 三棵树?啥也不是!
  1. 首先 Column 的 element 对象向上查找,发现父节点 DecoratedBox 对应的 element 对象就是 RenderObjectElement 的子类 SingleRenderObjectElement( 单子节点);
  2. 之后调用 SingleRenderObjectElement( DecoratedBox 节点)的 insertChildRenderObject(RenderObject child, dynamic slot),这里传入的第一个参数是 Cloumn 对应的 renderObject 即 RenderFlex。将自己的 renderObject 插入 SingleRenderObjectElement( DecoratedBox 节点)的 child 属性上;

2. MultiChildRenderObjectWidget

如果找到最近的节点是 MultiChildRenderObjectWidget,则会调用其对应的 renderObject.insert(child, after: slot?.renderObject) 方法。

@override
void insertChildRenderObject(RenderObject child, Element slot) {
  final ContainerRenderObjectMixin<RenderObject,
      ContainerParentDataMixin<RenderObject>> renderObject = this
      .renderObject;
  renderObject.insert(child, after: slot?.renderObject);
}

该方法最终调用 ContainerRenderObjectMixin.insertIntoChildList()

这个 ContainerRenderObjectMixin 是什么?查看他的注释:

mixin ContainerRenderObjectMixin<ChildType extends RenderObject, ParentDataType extends ContainerParentDataMixin<ChildType>> on RenderObject
  • 泛型 mixin,用于渲染带有一组子对象的对象;
  • 为渲染对象的子类提供一个子模型,该子类具有一个双向链接的子类列表;

发现重点么?是双向链表。

所以我们大概知道了 MultiChildRenderObjectWidget 的子节点,通过双向链表链接,下面的逻辑一定是去构建这个链表。

void _insertIntoChildList(ChildType child, { ChildType after }) {
  final ParentDataType childParentData = child.parentData;
  _childCount += 1;
  if (after == null) {

    childParentData.nextSibling = _firstChild;
    if (_firstChild != null) {
      final ParentDataType _firstChildParentData = _firstChild.parentData;
      _firstChildParentData.previousSibling = child;
    }
    _firstChild = child;
    _lastChild ??= child;
  } else {
    final ParentDataType afterParentData = after.parentData;
    if (afterParentData.nextSibling == null) {

      childParentData.previousSibling = after;
      afterParentData.nextSibling = child;
      _lastChild = child;
    } else {

    }
  }
}

其中这个方法有两个参数:

  1. child 表示我们当前传入的 renderObject;
  2. after 表示他的前一个的节点( 所以可能为空);

根据上面的注释,我们知道这个方法有三个作用:

  1. 在 after 为 null 的时候,将 child 置于第一个节点;
  2. 将 child 节点插入链表的末端;
  3. 将 child 节点插入链表的中间( 已省略代码);

结合例子来看会比较清楚,例子中的 Column 下放了四个 child:

第一个子节点 SizeBox,在向上找到 Column(RenderObjectElement)之后,调用这个方法,这时 after 为空,所以 _firstChild 即 RendenrConstrainedBox(SizeBox 对应的 RenderObject)。

demo 中的第二个子节点是 Text。我们前面提到,Text 是组合类的 widget,所以他不会参与 Render 树的挂载。

参与挂载的是 RichText。RichText 向上查也找到了 Column(RenderObjectElement),之后调用该方法。

这时传入的两个参数就是:

  1. child-> RichText 对应的 RenderObject(RenderParagraph);
  2. after-> SizeBox 对应的 RenderObject(RendenrConstrainedBox);

根据方法的逻辑,执行以下代码:

final ParentDataType afterParentData = after.parentData;
if (afterParentData.nextSibling == null) {

  childParentData.previousSibling = after;
  afterParentData.nextSibling = child;
  _lastChild = child;
}

将当前 child 节点 ParentData 的 previousSibling 指向第一个节点,将第一个节点 ParentData 的 nextSibling 属性指向当前的 child,最后将 _lastChild 指向自身。

所以整个流程执行完,我们会得到这样一个 RenderTree。

学 Flutter 不理解 Widget/Element/Render 三棵树?啥也不是!

三、总结

对于我们这样的一个看似简单的 demo。

学 Flutter 不理解 Widget/Element/Render 三棵树?啥也不是!

在只关注页面内部的的结构下,三棵树分别是这样的结构:

学 Flutter 不理解 Widget/Element/Render 三棵树?啥也不是!
  1. 源码写的 Widget 在运行时会依据设置的属性,在 build() 方法包装成真实的运行时 WidgetTree;
  2. 运行时 Widget Tree 的每个节点,与 Element Tree 一一对应;
  3. 只有 RenderObjectWidget 才会通过对应的 RenderObjectElement 生成最终的 RenderObject,组成 RenderTree;

RenderTree 背后还有 Layer Tree,之后才有渲染显式,这就不是本文的范围了,有机会以后再分享吧!

承香墨影
我是承香墨影,8 年技术老司机。在这里,主要分享我个人的原创内容,不仅限于技术,职场、产品、设计思想等等,统统都有。这里已经汇集了有很多小伙伴了,欢迎你加入!
328篇原创内容
Official Account

-- End --

本文对你有帮助吗?留言、转发、点好看是最大的支持,谢谢!

推荐阅读: