Widget、Element、Render是如何形成树结构?

最近因为在做Flutter中相关的性能优化,搜刮了网上所有的文章之后,看到了闲鱼的Flutter 高性能、多功能的全场景滚动容器。但奈何该组件没有开源,因此准备从文章给出的思路尝试研究和开发一个高性能的ScrollView。这个系列预计会分为4-5篇文章,前三篇主要对现有问题研究和分析,后两篇实际的进行开发。

原理篇:

1、Widget、Element、Render树究竟是如何形成的?

2、ListView中存在的性能问题分析

在原理介绍的第一篇,我想先和大家分析一个老生常谈的话题:Flutter中的三棵树。为什么选择这个主题,因为Flutter的UI体系由这三棵树构成,掌握这三棵树的构建过程可以让我们清晰认识到Flutter的渲染过程,这样我们才能基于此分析其中的可优化点。

读完本文你将收获:从源码一步步认识Flutter中三棵树是如何形成?


引言:

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

当看到这个图可能我们也默认为理解了这个知识点,但实际上这三颗树真的长这样么?他们究竟是如何形成这样的结构?本文将会从一个简单的例子追溯源码一一揭晓这个过程。

本文基于一个极简的demo

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


1、Widget树

首先我们来看看Widget树,实际上在代码运行过程中没有明确的Widget树概念,这棵树更多的是我们在开发过程中对Flutter嵌套结构的描述。而根据代码我们可以认为Widget树长这个样子


2、Element树的构建过程

Element树可以说是最重要的结构,它桥接了我们开发中的Widget与实际渲染的RenderObject对象。根据网上大多数文章的描述,可能我们会认为这个例子中Element树与Widget树一样

如果你也是这么认为的,那么恭喜你,回答错误~ 这棵树究竟该长什么样,我们一步步分析

我在总结了30个例子之后,我悟到了Flutter的布局原理中提到过,Flutter的UI体系中整个Widget大概可以分为三类组合类(紫色标识)代理类(红色标识)绘制类(黄色标识)

像大家一般接触到的StatelessWidget和StatefulWidget属于组合类的Widget,实际上他并不负责绘制,里面嵌套了多层Widget。所有我们在屏幕上看到的UI最终几乎都会通过绘制类的WidgetRenderObjectWidget实现。RenderObjectWidget中有个createRenderObject()方法生成实际渲染的RenderObject对象。 在我们的例子中,Container和Text就是组合类的Widget,这类的Widget我们可以通过查看他的build方法梳理他的嵌套。我们的例子中,ContainerText其实都是组合类Widget

查看Container的build方法,从源码得知Container嵌套的层级与我们设置的属性有关,我们为Container设置了child和color属性,对应会给我们的child嵌套一个DecoratedBox(渲染类Widget)。而Text组件内部只是返回了一个RichText(渲染类Widget)。得到以下结构(发现了没,叶子节点一定是渲染类的Widget,因为这样才会被渲染到屏幕上) 那我们实际的Element树会长什么这个样子么?这已经快接近真相了。但不太准确,因为实际上并不存在Container类型的Element,所以我们看看Element对象究竟有哪些类型。 这是Element的结构关系,其中ComponentElement对应组合类Widget(Stateless/Stateful以及所有他们的子类Container,Text),ProxyElement对应代理类Widget,而RenderObjectElement则对应渲染类的Widget(例如RichText,Column)。 所以更加准确的Element树结构应该是这个样子: 为什么还差一丢丢,看以下分析:

这棵树是如何形成的?核心的方法在他们共同的祖先 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树如下

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

3、RenderObejct树的构建过程

那么RenderObjectTree会长什么样子呢?先插点题外话,一开始大家可能会搞混RenderObjectWidget和RenderObject,RenderObjectElement对象之间的关系。我们简单看一下,首先RenderObjectWidget是Widget!!(好像一句废话)例如SizeBox,Column等。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类父节点!!!!),然而这是一个抽象方法,我们查看两个类里面的重写逻辑

一、SingleChildRenderObjectElement.insertChildRenderObject(renderObject, newSlot)

  @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为例就是下面这个过程

1、首先Column的element对象向上查找,发现父节点DecoratedBox对应的element对象就是RenderObjectElement的子类SingleRenderObjectElement(单子节点)。

2、之后调用SingleRenderObjectElement(DecoratedBox节点)的insertChildRenderObject(RenderObject child, dynamic slot) 这里传入的第一个参数是Column对应的renderObject即RenderFlex。将自己的renderObject插入SingleRenderObjectElement(DecoratedBox节点)的child属性上。

二、MultiChildRenderObjectWidget.insertChildRenderObject(renderObject, newSlot)

如果找到最近的节点是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是什么?查看他的注释

/// Generic mixin for render objects with a list of children.
///
/// Provides a child model for a render object subclass that has a doubly-linked
/// list of children.
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) {
      // insert at the start (_firstChild)
      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) {
        // insert at the end (_lastChild); we'll end up with two or more children
        childParentData.previousSibling = after;
        afterParentData.nextSibling = child;
        _lastChild = child;
      } else {
        // insert in the middle; we'll end up with three or more children
        // set up links from child to siblings
      	//..........忽略下面逻辑........./
      }
    }
  }

其中这个方法有两个参数:第一个child表示我们当前传入的renderObject,第二个表示他的前一个的节点(所以可能为空)。根据上面的注释,我们知道这个方法有三个作用: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) 根据方法的逻辑,执行以下代码:

  // insert at the end (_lastChild); we'll end up with two or more children
 final ParentDataType afterParentData = after.parentData;
      if (afterParentData.nextSibling == null) {
        // insert at the end (_lastChild); we'll end up with two or more children
        childParentData.previousSibling = after;
        afterParentData.nextSibling = child;
        _lastChild = child;
      } 

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

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


总结

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

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

如果对上面的内容理解较为吃力,可以先看看原来我一直在错误的使用 setState()? 面试官问我State的生命周期,该怎么回答 熟悉Flutter的构建过程~

推荐阅读:

Flutter进阶指南

原来我一直在错误的使用 setState()?

面试官问我State的生命周期,该怎么回答

总结了30个例子之后,我悟到了Flutter的布局原理

猜你喜欢

转载自blog.csdn.net/jdsjlzx/article/details/125630809