Flutter 08 三棵树(Widgets、Elements和RenderObjects)

一、Flutter三棵树背景

1.1 先思考一些问题

1. Widget与Element是什么关系?它们是一一对应的还是怎么理解?

2. createState 方法在什么时候调用?state 里面为啥可以直接获取到 widget 对象?

3. Widget 频繁更改创建是否会影响性能?复用和更新机制是什么样的?

4. Widget、Element、RenderObject 三棵树之间的关系是怎样的?

1.2 Flutter中Dom树

如何理解 DOM 树这个概念

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

例如默认的计数器应用的结构如下图:

二、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 树中,最大程度降低 对真实渲染视图的修改,提高渲染效率,而不是销毁整个渲染视图树重建。

三、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 是监工,负责协调各方资源,统一调配,外部人员有事需要先找这个监工。 

三者创建关系图:

用文字描述三者创建关系

首先是 Widget 通过调用其 createElement 方法创建出 Element 对象。

Element 继续调用其持有 Widget 对象(Stateless)或 State 对象(Stateful)的 build 方法创建其子 widget 对象。往复循环,继续创建子Element,子 Element 持有父 Element 的引用,因此最终会形成 出一颗 Element 树。

对于有 layout/paint 的能力控件,会创建 RenderObjectElement,在该 Element 的 mount 阶段会创 建其对应的 RenderObject 对象。 

四、运行时三棵树结构

三棵树结构

认识了三棵树之后,那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()被调用时,第一时间会在后台发生以下事件:

1.Flutter会构建包含Widget(Container,Row,Image,Text)的Widgets树;

2.Flutter遍历Widget树,然后根据其中的Widget调用createElement()来创建相应的Element对 象,最后将这些对象组建成Element树;

3.接下来会创建第三个树,这个树中包含了与Widget对应的Element通过createRenderObject() 创建的RenderObject;

具体Flutter经过这三个步骤后的状态:

总结一下三棵树结构:

1. Widget Tree: Widget 是 Flutter 面向开发者的上层接口,我们通过 widget 的层层嵌套,会形成 一颗 Widget 树,一个 Widget 可在多个位置复用。Flutter Framework 层为我们提供了一些常用 的包装或者容器的 Widget,比如 Container,其内部继续嵌套了其他 Widget,如 Padding、Align 等等。所以,开发者编写的 Widget 树和实际生成的 Widget 树都会略有差别。如图中虚线圆形标 注的 ColorBox、RawImage 等。

2. Element Tree :每一个 Widget 都会对应一个 Element,只不过 Element 分类不同。

3. RenderObject Tree:RenderObject 只负责最终的测量、布局和绘制,因此最终的 RenderObject Tree 是 Element Tree 剔除掉哪些包装,最后组织而成的 Tree。

为何搞这多树?

分层:开发只关注widget

1. Framework 将复杂的内部设计、渲染逻辑与开发接口隔离开,应用层只需关注 Widget 开发即 可。

高效:提交绘制效率

1. Tree 最大的共同特点就是快取,因为 Element、RenderObject 销毁重建成本很高,一旦可以复用 ,那么快取可以大幅减少这种开销。

2. 比如:当 Element 不需要重建时,更新 Widget 的引用就可以了;Layer Tree 的设计是将绘制图层 分开,方便提取和合成,合成层中的 transform 和 opacity 效果,都只是几何变换、透明度变换 等,不会触发 layout 和 paint,直接由 GPU 完成即可。

五、三棵树的作用介绍

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

2. 因为实例化一个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;
}

1. 如果某一个位置的Widget和新Widget不一致,才需要重新创建Element;

2. 如果某一个位置的Widget和新Widget一致时(两个widget相等或runtimeType与key相等),则只需 要修改RenderObject的配置,不用进行耗费性能的RenderObject的实例化工作了;

3. 因为Widget是非常轻量级的,实例化耗费的性能很少,所以它是描述APP的状态(也就是 configuration)的最好工具;

4. 重量级的RenderObject(创建十分耗费性能)则需要尽可能少的创建,并尽可能的复用; 

更新时三棵树操作

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

1. 例如当我们改变一个Text文本的时候,框架就会触发一个重建整个Widget树的动作。

2. 因为有了Element的存在,Flutter会比较新的Widget树中的第一个Widget和之前的 Widget。

3. 接下来比较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中的配置,然后继续向下遍历。 

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sziitjin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值