目录介绍
- 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 中。
例如默认的计数器应用的结构如下图:
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 三棵树架构关系
三棵树架构图
总结的关系
- 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 三者创建关系图
创建关系图
用文字描述三者创建关系
- 首先是 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经过这三个步骤后的状态
总结一下三棵树结构
- 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中的配置,然后继续向下遍历。