FlutterFlutterFlutter

Flutter的渲染——三棵树

2021-01-10  本文已影响0人  邱穆

一、引子

Flutter 中有三棵树:Widget 树,Element 树和 RenderObject 树。当应用启动时 Flutter 会遍历并创建所有的 Widget 形成 Widget Tree,同时与 Widget Tree 相对应,通过调用 Widget 上的 createElement() 方法创建每个 Element 对象,形成 Element Tree。最后调用 Element 的 createRenderObject() 方法创建每个渲染对象,形成一个 Render Tree。 Element就是Widget在UI树具体位置的一个实例化对象,大多数Element只有唯一的renderObject,但还有一些Element会有多个子节点,如继承自RenderObjectElement的一些类,比如MultiChildRenderObjectElement。最终所有Element的RenderObject构成一棵树,我们称之为”Render Tree“即”渲染树“。总结一下,我们可以认为Flutter的UI系统包含三棵树:Widget树、Element树、渲染树。他们的依赖关系是:根据Widget树生成Element树,再依赖于Element树生成RenderObject 树,如下图:

这种树形结构类似于HTML中的DOM树,如默认的计数器应用的结构如下图:

在 flutter 中,Container、Text 等组件都属于 Widget,所以这课树就是 Widget 树,也可以叫做控件树,它就表示了我们在 dart 代码中所写的控件的结构。Element 就是 Widget 的另一种抽象。我们在代码中使用的像 Container、Text 等这类组件和其属性只不过是我们想要构建的组件的配置信息,当我们第一次调用 build()`方法想要在屏幕上显示这些组件时,Flutter 会根据这些信息生成该 Widget 控件对应的 Element,同样地,Element 也会被放到相应的 Element 树当中。RenderObject 在 Flutter 当中做组件布局渲染的工作,其为了组件间的渲染搭配及布局约束也有对应的 RenderObject 树,我们也称之为渲染树。

二、Widget 树

Widget 是 Flutter 的核心部分,是用户界面的不可变描述信息。Widget的功能是“描述一个UI元素的配置数据”,它就是说,Widget其实并不是表示最终绘制在设备屏幕上的显示元素,而它只是描述显示元素的一个配置数据。正如 Flutter 的口号 Everything’s a widget, 用 Flutter 开发应用就是在写 Widget 。Widget 的 canUpdate 方法通过比较新部件和旧部件的 runtimeType 和 key 属性是否相同来决定更新部件对应的 Element。

static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
  }
@protected
  Element createElement();

三、Element 树

实际上,Flutter中真正代表屏幕上显示元素的类是Element,Widget只是UI元素的一个配置数据,并且一个Widget可以对应多个Element。Element就是Widget在UI树具体位置的一个实例化对象,大多数Element只有唯一的renderObject,但还有一些Element会有多个子节点,如继承自RenderObjectElement的一些类,比如MultiChildRenderObjectElement。Widget 是不可变,它的改变就意味着要重建,而其重建也非常频繁,如果我们将更多的任务都交给它将会对性能造成很大的损伤,因此我们把 Widget 组件当作一个虚拟的组件树,而真正被渲染在屏幕上的其实是 Elememt 这棵树,它持有其对应 Widget 的引用,如果他对应的 Widget 发生改变,它就会被标记为 dirty Element,于是下一次更新视图时根据这个状态只更新被修改的内容,从而达到提升性能的效果。

Element的生命周期如下:

  1. Framework 调用Widget.createElement 创建一个Element实例,记为element

  2. Framework 调用 element.mount(parentElement,newSlot) ,mount方法中首先调用element所对应Widget的createRenderObject方法创建与element相关联的RenderObject对象,然后调用element.attachRenderObject方法将element.renderObject添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在Element树结构发生变化时才需要重新attach)。插入到渲染树后的element就处于“active”状态,处于“active”状态后就可以显示在屏幕上了(可以隐藏)。

  3. 当有父Widget的配置数据改变时,同时其State.build返回的Widget结构与之前不同,此时就需要重新构建对应的Element树。为了进行Element复用,在Element重新构建前会先尝试是否可以复用旧树上相同位置的element,element节点在更新前都会调用其对应Widget的canUpdate方法,如果返回true,则复用旧Element,旧的Element会使用新Widget配置数据更新,反之则会创建一个新的Element。Widget.canUpdate主要是判断newWidget与oldWidget的runtimeType和key是否同时相等,如果同时相等就返回true,否则就会返回false。根据这个原理,当我们需要强制更新一个Widget时,可以通过指定不同的Key来避免复用。

  4. 当有祖先Element决定要移除element 时(如Widget树结构发生了变化,导致element对应的Widget被移除),这时该祖先Element就会调用deactivateChild 方法来移除它,移除后element.renderObject也会被从渲染树中移除,然后Framework会调用element.deactivate 方法,这时element状态变为“inactive”状态。

  5. “inactive”态的element将不会再显示到屏幕。为了避免在一次动画执行过程中反复创建、移除某个特定element,“inactive”态的element在当前动画最后一帧结束前都会保留,如果在动画执行结束后它还未能重新变成“active”状态,Framework就会调用其unmount方法将其彻底移除,这时element的状态为defunct,它将永远不会再被插入到树中。

  6. 如果element要重新插入到Element树的其它位置,如element或element的祖先拥有一个GlobalKey(用于全局复用元素),那么Framework会先将element从现有位置移除,然后再调用其activate方法,并将其renderObject重新attach到渲染树。

Element树的生命周期如图:

四、RenderObject 树(渲染树)

4.1 介绍

渲染树的任务就是做组件的具体的布局渲染工作,渲染树上每个节点都是一个继承自 RenderObject 类的对象,其由 Element 中的 renderObject 或 RenderObjectWidget 中的 createRenderObject 方法生成,该对象内部提供多个属性及方法来帮助框架层中的组件如何布局渲染。RenderObject 用于应用界面的布局和绘制,保存了元素的大小,布局等信息,实例化一个 RenderObject 是非常耗能的。 RenderObject 主要属性和方法如下:

4.2 布局过程

Flutter 中的控件在屏幕上绘制渲染之前需要先进行布局(Layout)操作。其具体可分为两个线性过程:

  1. 从顶部向下传递约束。

这一过程用于传递布局约束。父节点给每个子节点传递约束,这些约束是每个子节点在布局阶段必须要遵守的规则。常见的约束包括规定子节点最大最小宽度或者子节点最大最小的高度。这种约束会向下延伸,子组件也会产生约束传递给自己的孩子,一直到叶子结点。

  1. 从底部向上传递布局信息。

这一过程用来传递具体的布局信息。子节点接受到来自父节点的约束后,会依据它产生自己具体的布局信息,如父节点规定我的最小宽度是 500 的单位像素,子节点按照这个规则可能定义自己的宽度为 500 个像素,或者大于 500 像素的任何一个值。这样,确定好自己的布局信息之后,将这些信息告诉父节点。父节点也会继续此操作向上传递一直到最顶部。 其过程可用下图表示:

Flutter 中有两种主要的布局协议:Box 盒子协议和 Sliver 滑动协议。 在RenderBox 中,有个size属性用来保存控件的宽和高。RenderBox的layout是通过在组件树中从上往下传递BoxConstraints对象的实现的。BoxConstraints对象可以限制子节点的最大和最小宽高,子节点必须遵守父节点给定的限制条件。在布局阶段,父节点会调用子节点的layout()方法,layout方法需要传入两个参数,第一个为constraints,即 父节点对子节点大小的限制,该值根据父节点的布局逻辑确定。另外一个参数是 parentUsesSize,该值用于确定 relayoutBoundary,该参数表示子节点布局变化是否影响父节点,如果为true,当子节点布局发生变化时父节点都会标记为需要重新布局,如果为false,则子节点布局发生变化后不会影响父节点。

4.3 绘制过程

RenderObject可以通过paint()方法来完成具体绘制逻辑,流程和布局流程相似,子类可以实现paint()方法来完成自身的绘制逻辑,paint()签名如下:

void paint(PaintingContext context, Offset offset) { }

通过context.canvas可以取到Canvas对象,接下来就可以调用Canvas API来实现具体的绘制逻辑。如果节点有子节点,它除了完成自身绘制逻辑之外,还要通过paintChild()方法来调用子节点的绘制方法。如此递归完成整个节点树的绘制,最终调用栈为: paint() > paintChild() > paint() ...

五、为什么需要三棵树?

先说答案:使用三棵树的目的是尽可能复用 Element。

复用 Element 对性能非常重要,因为 Element 拥有两份关键数据:Stateful widget 的状态对象及底层的 RenderObject。当应用的结构很简单时,或许体现不出这种优势,一旦应用复杂起来,构成页面的元素越来越多,重新创建 3 棵树的代价是很高的,所以需要最小化更新操作。当 Flutter 能够复用 Element 时,用户界面的逻辑状态信息是不变的,并且可以重用之前计算的布局信息,避免遍历整棵树。

参考:

https://book.flutterchina.club/

https://juejin.cn/post/6844903837858283528

https://zhuanlan.zhihu.com/p/128469011

https://www.jianshu.com/p/096a38a24a49

上一篇下一篇

猜你喜欢

热点阅读