Flutter三棵树理解

目录介绍

  • 01.Flutter三棵树背景
  • 02.Flutter中的三棵树
  • 03.Flutter三棵树关系
  • 04.运行时三棵树结构
  • 05.三棵树的作用介绍

Flutter三棵树背景

1.1 先思考一些问题

  • Widget与Element是什么关系?它们是一一对应的还是怎么理解?
  • createState 方法在什么时候调用?state 里面为啥可以直接获取到 widget 对象?
  • Widget 频繁更改创建是否会影响性能?复用和更新机制是什么样的?
  • Widget、Element、RenderObject 三棵树之间的关系是怎样的?

1.2 Flutter中Dom树

  • 如何理解 DOM 树这个概念

    • 它由页面中每一个控件组成,这些控件所形成的一种天然的嵌套关系使其可以表示为 “树” 结构,可以将这个概念应用在 Flutter 中。
  • 例如默认的计数器应用的结构如下图:

    • Flutter三棵树理解

02.Flutter中的三棵树

  • 即Widget树、Element树和RenderObject树。

    • Widget树:控件的配置信息,不涉及渲染,更新代价极低。
    • RenderObject树:真正的UI渲染树,负责渲染UI,更新代价极大。
    • Element树:Widget树和RenderObject树之间的粘合剂,负责将Widget树的变更以最低的代价映射到RenderObject树上。
  • Widget 树

    • 我们平时用 Widget 使用声明式的形式写出来的界面,可以理解为 Widget 树,这是要介绍的第一棵树。
    • Widget的功能是“描述一个UI元素的配置数据”,它就是说,Widget其实并不是表示最终绘制在设备屏幕上的显示元素,而它只是描述显示元素的一个配置数据。
  • RenderObject 树

    • Flutter 引擎需要把我们写的 Widget 树的信息都渲染到界面上,这样人眼才能看到,跟渲染有关的当然有一颗渲染树 RenderObject tree,这是第二颗树,渲染树节点叫做 RenderObject,这个节点里面处理布局、绘制相关的事情。
    • 这两个树的节点并不是一一对应的关系,有些 Widget是要显示的,有些 Widget ,比如那些继承自 StatelessWidget & StatefulWidget 的 Widget 只是将其他 Widget 做一个组合,这些 Widget 本身并不需要显示,因此在 RenderObject 树上并没有相对应的节点。
  • Element 树

    • Widget 树是非常不稳定的,动不动就执行 build方法,一旦调用 build 方法意味着这个 Widget 依赖的所有其他 Widget 都会重新创建,如果 Flutter 直接解析 Widget树,将其转化为 RenderObject 树来直接进行渲染,那么将会是一个非常消耗性能的过程,那对应的肯定有一个东西来消化这些变化中的不便,来做cache。
    • 因此,这里就有另外一棵树 Element 树。Element 树这一层将 Widget 树的变化(类似 React 虚拟 DOM diff)做了抽象,可以只将真正需要修改的部分同步到真实的 RenderObject 树中,最大程度降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个渲染视图树重建。

03.Flutter三棵树关系

3.1 三棵树架构关系

  • 三棵树架构图

    • Flutter三棵树理解
  • 总结的关系

    • widget 树和 Element 树节点是一一对应关系,每一个 Widget 都会有其对应的 Element,但是 RenderObject 树则不然,只有需要渲染的 Widget 才会有对应的节点。
    • Element 树相当于一个中间层,大管家,它对 Widget 和 RenderObject 都有引用。
    • 当 Widget 不断变化的时候,将新 Widget 拿到 Element 来进行对比,看一下和之前保留的 Widget 类型和 Key 是否相同,如果都一样,那完全没有必要重新创建 Element 和 RenderObject,只需要更新里面的一些属性即可,这样可以以最小的开销更新 RenderObject,引擎在解析 RenderObject 的时候,发现只有属性修改了,那么也可以以最小的开销来做渲染。
  • 简单总结一下

    • Widget 树就是配置信息的树,我们平时写代码写的就是这棵树。
    • RenderObject 树是渲染树,负责计算布局,绘制,Flutter 引擎就是根据这棵树来进行渲染的。
    • Element 树作为中间者,管理着将 Widget 生成 RenderObject和一些更新操作。
  • 举个通俗例子

    • UI 渲染就像盖一栋大楼,Widget 代表图纸,表示我们想造怎样的大楼,RenderObject 是根据图纸干活的工人,而 Element 是监工,负责协调各方资源,统一调配,外部人员有事需要先找这个监工。

3.2 三者创建关系图

  • 创建关系图

    • Flutter三棵树理解
  • 用文字描述三者创建关系

    • 首先是 Widget 通过调用其 createElement 方法创建出 Element 对象。
    • Element 继续调用其持有 Widget 对象(Stateless)或 State 对象(Stateful)的 build 方法创建其子 widget 对象。往复循环,继续创建子Element,子 Element 持有父 Element 的引用,因此最终会形成出一颗 Element 树。
    • 对于有 layout/paint 的能力控件,会创建 RenderObjectElement,在该 Element 的 mount 阶段会创建其对应的 RenderObject 对象。

04.运行时三棵树结构

4.1 三棵树结构

  • 认识了三棵树之后,那Flutter是如何创建布局的?以及三棵树之间他们是如何协同的呢?

    • 接下来就让我们通过一个简单的例子来剖析下它们内在的协同关系:
    class Tree extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Container(
          color: Colors.brown,
          child: Row(
            children: [
              new Image.network(
                "https://p1.ssl.qhmsg.com/dr/220__/t01d5ccfbf9d4500c75.jpg",
                width: 100,
                height: 100,
              ),
              new Text(
                  "从网络加载图片",
                style: TextStyle(
                  fontSize: 16
                ),
              ),
            ],
          ),
        );
      }
    }
  • 当runApp()被调用时,第一时间会在后台发生以下事件:

    • Flutter会构建包含Widget(Container,Row,Image,Text)的Widgets树;
    • Flutter遍历Widget树,然后根据其中的Widget调用createElement()来创建相应的Element对象,最后将这些对象组建成Element树;
    • 接下来会创建第三个树,这个树中包含了与Widget对应的Element通过createRenderObject()创建的RenderObject;
  • 具体Flutter经过这三个步骤后的状态

    • Flutter三棵树理解
  • 总结一下三棵树结构

    • Widget Tree: Widget 是 Flutter 面向开发者的上层接口,我们通过 widget 的层层嵌套,会形成一颗 Widget 树,一个 Widget 可在多个位置复用。Flutter Framework 层为我们提供了一些常用的包装或者容器的 Widget,比如 Container,其内部继续嵌套了其他 Widget,如 Padding、Align 等等。所以,开发者编写的 Widget 树和实际生成的 Widget 树都会略有差别。如图中虚线圆形标注的 ColorBox、RawImage 等。
    • Element Tree :每一个 Widget 都会对应一个 Element,只不过 Element 分类不同。
    • RenderObject Tree:RenderObject 只负责最终的测量、布局和绘制,因此最终的 RenderObject Tree 是 Element Tree 剔除掉哪些包装,最后组织而成的 Tree。

4.2 为何搞这多树

  • 分层:开发只关注widget

    • Framework 将复杂的内部设计、渲染逻辑与开发接口隔离开,应用层只需关注 Widget 开发即可。
  • 高效:提交绘制效率

    • Tree 最大的共同特点就是快取,因为 Element、RenderObject 销毁重建成本很高,一旦可以复用 ,那么快取可以大幅减少这种开销。
    • 比如:当 Element 不需要重建时,更新 Widget 的引用就可以了;Layer Tree 的设计是将绘制图层分开,方便提取和合成,合成层中的 transform 和 opacity 效果,都只是几何变换、透明度变换等,不会触发 layout 和 paint,直接由 GPU 完成即可。

05.三棵树的作用介绍

5.1 简单了解更新操作

  • 简而言之是为了性能,为了复用Element从而减少频繁创建和销毁RenderObject。

    • 因为实例化一个RenderObject的成本是很高的,频繁的实例化和销毁RenderObject对性能的影响比较大,所以当Widget树改变的时候,Flutter使用Element树来比较新的Widget树和原来的Widget树:
    //framework.dart
     @protected
      Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
        if (newWidget == null) {
          if (child != null)
            deactivateChild(child);
          return null;
        }
        Element newChild;
        if (child != null) {
          assert(() {
            final int oldElementClass = Element._debugConcreteSubtype(child);
            final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
            hasSameSuperclass = oldElementClass == newWidgetClass;
            return true;
          }());
          if (hasSameSuperclass && child.widget == newWidget) {
            if (child.slot != newSlot)
              updateSlotForChild(child, newSlot);
            newChild = child;
          } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
            if (child.slot != newSlot)
              updateSlotForChild(child, newSlot);
            child.update(newWidget);
            assert(child.widget == newWidget);
            assert(() {
              child.owner._debugElementWasRebuilt(child);
              return true;
            }());
            newChild = child;
          } else {
            deactivateChild(child);
            assert(child._parent == null);
            newChild = inflateWidget(newWidget, newSlot);
          }
        } else {
          newChild = inflateWidget(newWidget, newSlot);
        }
    
        assert(() {
          if (child != null)
            _debugRemoveGlobalKeyReservation(child);
          final Key key = newWidget?.key;
          if (key is GlobalKey) {
            key._debugReserveFor(this, newChild);
          }
          return true;
        }());
    
        return newChild;
      }
    ...
      static bool canUpdate(Widget oldWidget, Widget newWidget) {
        return oldWidget.runtimeType == newWidget.runtimeType
            && oldWidget.key == newWidget.key;
      }
  • 如果某一个位置的Widget和新Widget不一致,才需要重新创建Element;
  • 如果某一个位置的Widget和新Widget一致时(两个widget相等或runtimeType与key相等),则只需要修改RenderObject的配置,不用进行耗费性能的RenderObject的实例化工作了;

    • 因为Widget是非常轻量级的,实例化耗费的性能很少,所以它是描述APP的状态(也就是configuration)的最好工具;
    • 重量级的RenderObject(创建十分耗费性能)则需要尽可能少的创建,并尽可能的复用;

5.2 更新时三棵树操作

  • 因为Widget是不可变的,当某个Widget的配置改变的时候,整个Widget树都需要被重建。

    • 例如当我们改变一个Text文本的时候,框架就会触发一个重建整个Widget树的动作。
    • 因为有了Element的存在,Flutter会比较新的Widget树中的第一个Widget和之前的Widget。
    • 接下来比较Widget树中之后Widget和之前Widget,以此类推,直到Widget树比较完成。
      @override
      Widget build(BuildContext context) {
        return Container(
          color: Colors.brown,
          height: double.infinity,
          child: Row(
            children: [
              new Image.network(
                "https://p1.ssl.qhmsg.com/dr/220__/t01d5ccfbf9d4500c75.jpg",
                width: 100,
                height: 100,
              ),
              new Text(
                  "改变UI",
                style: TextStyle(
                  fontSize: 16
                ),
              ),
            ],
          ),
        );
      }
  • Flutter遵循一个最基本的原则:判断新的Widget和老的Widget是否是同一个类型:

    • 如果不是同一个类型,那就把Widget、Element、RenderObject分别从它们的树(包括它们的子树)上移除,然后创建新的对象;
    • 如果是一个类型,那就仅仅修改RenderObject中的配置,然后继续向下遍历。

推荐:https://github.com/yangchong211/YCFlutterUtils

上一篇:03.视频播放器Api说明


下一篇:Widget原理分析