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 树
三棵树介绍:
- Widget:只是一个配置,里面存储的是有关视图渲染的配置信息,包括布局、渲染属性、事件响应信息等。是不可变的,主要负责描述UI的属性和布局,不负责实际的渲染绘制,所以创建成本很低
- Element 是分离 WidgetTree 和真正的渲染对象的中间层, WidgetTree 用来描述对应的Element 属性,同时持有Widget和RenderObject,存放上下文信息,通过它来遍历视图树,支撑UI结构。
- RenderObject (渲染树)用于应用界面的布局和绘制,负责真正的渲染,保存了元素的大小,布局等信息,实例化一个 RenderObject 是非常耗能的
我们可以把 Widget 当做菜谱,Element 是配菜,RenderObject 是烧菜和出菜。
初次运行时的三棵树:
初步认识了三棵树之后,那Flutter是如何创建布局的?以及三棵树之间他们是如何协同的呢?接下来就 让我们通过一个简单的例子来剖析下它们内在的协同关系:
class ThreeTree extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
child: Container(color: Colors.blue)
);
}
}
上面这个例子很简单,它由三个Widget组成:ThreeTree、Container(red)、Container(blue)。那么当Flutter的 runApp()方法被调用时会发生什么呢?下面在Flutter工程中先来构建这么一个简单的示例:
此时可以打开“Flutter Inspector”:
那第二棵树在哪里呢?此时需要跟一下源码了:
总结一下就是:
当runApp()被调用时,第一时间会在后台发生以下事件:
1)Flutter会构建包含这三个Widget的Widgets树;
2)Flutter遍历Widget树,然后根据其中的Widget调用createElement()来创建相应的Element对象, 最后将这些对象组建成Element树;
3)接下来会创建第三个树,这个树中包含了与Widget对应的Element通过createRenderObject()创建 的RenderObject;
而整个状态过程可以用下图来描述:
从图中可以看出Flutter创建了三个不同的树,一个对应着Widget,一个对应着Element,一个对应着 RenderObject。每一个Element中都有着相对应的Widget和RenderObject的引用。可以说Element是存在于可变Widget树和不可变RenderObject树之间的桥梁。Element擅长比较两个Object,在Flutter里面就是Widget和RenderObject。它的作用是配置好Widget在树中的位置,并且保持对于相对应的 RenderObject和Widget的引用。
三棵树的作用:
那这三棵树有啥意义呢?简而言之是为了性能,为了复用Element从而减少频繁创建和销毁 RenderObject。因为实例化一个RenderObject的成本是很高的,频繁的实例化和销毁RenderObject对 性能的影响比较大,所以当Widget树改变的时候,Flutter使用Element树来比较新的Widget树和原来的 Widget树,接下来从源码中来体会一下:
此时也是只更新对应的element,接下来继续:
总结如下:
1)如果某一个位置的Widget和新Widget不一致,才需要重新创建Element;
2)如果某一个位置的Widget和新Widget一致时(两个widget相等或runtimeType与key相等),则只需要修改RenderObject的配置,不用进行耗费性能的RenderObject的实例化工作了;
3)因为Widget是非常轻量级的,实例化耗费的性能很少,所以它是描述APP的状态(也就是configuration)的最好工具;
4)重量级的RenderObject(创建十分耗费性能)则需要尽可能少的创建,并尽可能的复用;
因为在框架中,Element是被抽离开来的,所以你不需要经常和它们打交道。每个Widget的build (BuildContext context)方法中传递的context就是实现了BuildContext接口的Element。
更新时的三棵树:
那如果此时我们修改一下程序:
因为Widget是不可变的,当某个Widget的配置改变的时候,整个Widget树都需要被重建。例如当我们改变一个Container的颜色为橙色的时候,框架就会触发一个重建整个Widget树的动作。因为有了Element的存在,Flutter会比较 新的Widget树中的第一个Widget和之前的Widget。接下来比较Widget 树中第二个Widget和之前Widget,以此类推,直到Widget树比较完成。
Flutter遵循一个最基本的原则:判断新的Widget和老的Widget是否是同一个类型:
1)如果不是同一个类型,那就把Widget、Element、RenderObject分别从它们的树(包括它们的子 树)上移除,然后创建新的对象;
2)如果是一个类型,那就仅仅修改RenderObject中的配置,然后继续向下遍历;
在我们的例子中,ThreeTree Widget是和原来一样的类型,它的配置也是和原来的ThreeTreeRender一 样的,所以什么都不会发生。下一个节点在Widget树中是Container Widget,它的类型和原来是一样 的,但是它的颜色变化了,所以RenderObject的配置也会发生对应的变化,然后它会重新渲染,其他的 对象都保持不变。
上面这个过程是非常快的,因为Widget的不变性和轻量级使得他能快速的创建,这个过程中那些重量级 的RenderObject则是保持不变的,直到与其相对应类型的Widget从Widget树中被移除。 注意这三个树,配置发生改变之后,Element和RenderObject实例没有发生变化。
当Widget的类型发生改变时:

和刚才流程一样,Flutter会从新Widget树的顶端向下遍历,与原有树中的Widget类型进行对比。
因为FlatButton的类型与Element树中相对应位置的Element的类型不同,Flutter将会从各自的树上删除 这个Element和相对应的ContainerRender,然后Flutter将会重建与FlatButton相对应的Element和 RenderObject。如下:
很明显这个重新创建的过程相对耗时的,但是当新的RenderObject树被重建后将会计算布局,然后绘制 在屏幕上面。Flutter内部使用了很多优化方法和缓存策略来处理,所以你不需要手动来处理这些。以上便是Flutter的整体渲染机制,可以看出Flutter利用了三棵树很巧妙的解决的性能的问题。
三棵树相关知识补充:
Flutter 中存在 Widget
、 Element
、RenderObject
三棵树,其中 Widget
与 Element
是一对多的关系 ,Element
与 RenderObject
是一一对应的关系。
上面说的widget和element一对多的关系指的是实际的关系,widget树的节点比代码段中的widget个数多,因为widget是通过组合其它widget的组合方式进行构建,比如:container如果color属性不为空时,查看源码会引入coloredbox的widget
Element
中持有Widget
和 RenderObject
, 而 Element
与 RenderObject
是一一对应的关系(除去 Element
不存在 RenderObject
的情况,如 ComponentElement
是不具备 RenderObject
),当 RenderObject
的 isRepaintBoundary
为 true
时,那么个区域形成一个 Layer
,所以不是每个 RenderObject
都具有 Layer
的,因为这受 isRepaintBoundary
的影响。
Flutter 中 Widget
不可变,每次保持在一帧,如果发生改变是通过 State
实现跨帧状态保存,而真实完成布局和绘制数组的是 RenderObject
, Element
充当两者的桥梁, State
就是保存在 Element
中。
Flutter 中 RenderObject
在 attch
/layout
之后会通过 markNeedsPaint();
使得页面重绘,流程大概如下:
通过isRepaintBoundary 往上确定了更新区域,通过 requestVisualUpdate 方法触发更新往下绘制。
正常情况 RenderObject
的布局相关方法调用顺序是 : layout
-> performResize
-> performLayout
-> markNeedsPaint
, 但是用户一般不会直接调用 layout
,而是通过 markNeedsLayout
,具体流程如下:
在Flutter的组件体系中,并非所有的Widget都会渲染到最后的页面上,整个Widget大概可以分为三类组合类、代理类、绘制类
平时我们使用到最多的StatelessWidget
和StatefulWidget
其实只是组合类的控件,实际上他并不负责绘制,所有我们在屏幕上看到的UI最终几乎都会通过RenderObjectWidget实现。而RenderObjectWidget中有个createRenderObject()
方法生成RenderObject
对象,RenderObject
实际负责实际的layout()和paint()。例如我们最常使用的Container组件其实只是一个组合类的控件,在其中封装了多个负责绘制的原子组件参考:深入研究Flutter布局原理 - 掘金
从创建到渲染的大体流程是:根据Widget生成Element,然后创建相应的RenderObject并关联到Element.renderObject属性上,最后再通过RenderObject来完成布局排列和绘制。
flutter的ui渲染包含widget,element和renderObject,widget作为配置信息比较轻量级,每一帧渲染的时候就会有一个新的widget对象产生,而element和renderObject却只有在widget类型发生改变或者所持有的key不一致时才会进行重建。
abstract class Widget extends DiagnosticableTree {
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}