Flutter中三棵树的理解
Widget、Element和RenderObject
Widget
Widget 是用户页面的描述,表示了Element的配置信息,Flutter页面都是由各种各样的Widget组合声明成的。Widget本身是不可变的immutable,注解如下:
@immutable
abstractclassWidgetextendsDiagnosticableTree{///...}
这也就意味着,所有它直接声明或继承的变量都必须为final类型的。如果想给widget关联一个可变的状态,考虑使用StatefulWidget,它会通过[StatefulWidget.createState]创建一个State对象,然后,每当它转化成一个element时会合并到树上。
子类:

StatelessWidget、StatefulWidget我们很熟悉是用来编写页面和组件的,那另外三个都是做什么用的呢?
- RenderObjectWidget,从名字上就能看出它是一个Widget,然后和实际渲染对象RenderObject有撇不清的关系。它提供了RenderObjectElement的配置信息,其中包装了RenderObject。也就是从页面上编写的StatelessWidget和StatefulWidget在递归的build过程中,会最终返回实际可渲染的Widget对象,也就是RenderObjectWidget,那么这个转化关系是一一对应的吗,其实不是的,后边再具体分析
- PreferredSizeWidget,一个返回它自身想要大小的组件,如果它在布局过程中是不受限制的,例如,AppBar和TabBar
- ProxyWidget,代理组件,提供一个子组件,而不是自己创建,例如,InheritedWidget和ParentDataWidget
Element
元素树,是Widget在具体位置的实例化,它负责控制Widget的生命周期,持有了widget实例和renderObject实例,它和Widget继承自同一个类,DiagnosticableTree可诊断树,并且实现了BuildContext类。

Element有两种基本类型:
- ComponentElement,其他elements的宿主,它本身不包含RenderObject,而由它持有的element节点包含,像StatelessWidget 和StatefulWidget 中分别创建的StatelessElement和StatefulElement都是继承自ComponentElement
- RenderObjectElement,参与layout或者绘制阶段的元素
RenderObject
渲染树中的每个节点基类是RenderObject,它定义了布局和绘制的抽象模型。每一个RenderObject有一个parent,和一个parentData,父级的RenderObject可以在其中存储孩子的具体数据,例如,child的位置信息。

- RenderObject 仅实现了基本的布局和绘制,没有具体的布局绘制模型,相当于ViewGroup,其子类RenderBox使用了笛卡尔坐标系,它的一些子类是真正的渲染树上的节点。大多数情况下,当我们想自定义一个渲染对象时,直接继承RenderObject有些过重overkill,更好的选择是继承RenderBox,除非你不想使用笛卡尔坐标系统。
- RenderView,通常情况下是Flutter渲染树的根节点,可以理解为DecorView,它只有一个子节点,必须是RenderBox类型的。
对应关系
从Widget构建Element
看这段简单的代码片段,显示了widget树形结构
Container(
color:Colors.blue,
child:Row(
children:[
Image.network('https://www.example.com/1.png'),
constText('A'),
],
),
);
当Flutter要渲染这个Container到页面时,会调用它的build()方法,返回一个widget的子树,包含它的child树Row及其children的子树,还有一些其它的树的节点,看下它的build()函数:
classContainerextendsStatelessWidget{
///创建一个结合常用的绘画、定位和控制大小的组件
Container({
Key?key,
this.alignment,
this.padding,
this.color,
this.decoration,
this.foregroundDecoration,
double?width,
double?height,
BoxConstraints?constraints,
this.margin,
this.transform,
this.transformAlignment,
this.child,
this.clipBehavior=Clip.none,
})://...
@override
Widgetbuild(BuildContextcontext){
Widget?current=child;
//...
if(alignment!=null)
current=Align(alignment:alignment!,child:current);
//...
if(effectivePadding!=null)
current=Padding(padding:effectivePadding,child:current);
if(color!=null)
current=ColoredBox(color:color!,child:current);
//...
if(decoration!=null)
current=DecoratedBox(decoration:decoration!,child:current);
returncurrent!;
}
}
可以看到,Container的一些属性,都代表插入一个控制该属性的新节点widget,所以它本身就是一个封装,替我们组合了大量小部件,减轻了开发工作量。我们设置了color属性,它会插入一个ColoredBox节点,显示它的颜色。
相应的,Image和Text在build期间也可能插入子节点比如RawImage和RichText,所以widget树的层级结构可能比代码展示的更深

在构建阶段,Flutter将上述的widget转换成相应的element tree ,一一对应,树的层级结构上的每个元素代表了一个具体位置的widget实例。
这里的一一对应其实是framework层的经过转化后的widget,并不是代码层的用户编写的widget跟element的对应,比如一个Container在设置属性后被转化成多个子widget,同时对应了多个element节点。

上边提到了Element实现了BuildContext,任何widget的element可以通过build()方法中传入的BuildContext参数访问到,它是widget在树上操作的句柄。例如,可以调用Theme.of(context),查找widget树上最近的主题,如果widget定义了单独的主题就返回它,如果没有返回app的主题
///An[Element]thatusesa[StatelessWidget]asitsconfiguration.
classStatelessElementextendsComponentElement{
///Createsanelementthatusesthegivenwidgetasitsconfiguration.
StatelessElement(StatelessWidgetwidget):super(widget);
@override
StatelessWidgetgetwidget=>super.widgetasStatelessWidget;
@override
Widgetbuild()=>widget.build(this);
@override
voidupdate(StatelessWidgetnewWidget){
super.update(newWidget);
assert(widget==newWidget);
_dirty=true;
rebuild();
}
}
可以看到,StatelessElement元素在构建的时候调用build方法,会调用StatelessWidget的build方法,传入BuildContext为this。
因为widgets是immutable的,包括节点之间的父/子关系,对widget树的任何修改(比如,Text('A') to Text('B'))会导致一系列新的widget对象的被重建。但这并不意味下层必须被重建,element tree可能在界面刷新时是持久的(persistent),因此对性能起着关键作用,因为Flutter缓存了底层表示,使它表现的可以像完全丢弃上层的widget层一样。通过遍历widgets的修改,可以做到只重新构建一部分的element tree。
Element到RenderObject
只绘制单个的widget的应用是很少见的,所以,任何的UI框架的一个重要的部分就是能够高效的布局一个层级结构的widget,确定它们的大小、位置然后绘制到屏幕上。
渲染树上的每个节点的基类型是RenderObject,在构建阶段,Flutter仅将element tree中的RenderObjectElement对象生成可渲染的对象,不同的Render对象渲染不同类型,RenderParagraph
渲染text,RenderImage
渲染image

Flutter中多数widgets的渲染对象是继承自RenderBox的,它使用了笛卡尔坐标系在2D空间,它提供了一个盒子约束模型,限制了widget的最小和最大宽度和高度。
layout期间,Flutter会以深度优先遍历渲染树,并将constraints约束传递给child,用来确定child的大小,然后将结果传递给parent的size变量。
///子类不应该直接重写[layout]方法,而应该重写[performResize]and/or[performLayout],[layout]方法
///代理它的工作放在[performResize]and[performLayout]
///parent's的[performLayout]方法应该无条件的调用所有它的child的[layout]
voidlayout(Constraintsconstraints,{boolparentUsesSize=false}){
///...
try{
performLayout();
markNeedsSemanticsUpdate();
}catch(e,stack){
_debugReportException('performLayout',e,stack);
}
///...
_needsLayout=false;
markNeedsPaint();
}
///空实现,由子类重写
@protected
voidperformLayout();
举例,看下RenderPadding的performLayout方法:
@override
voidperformLayout(){
///第一步,拿到constraints
finalBoxConstraintsconstraints=this.constraints;
//...
///第二步,根据parent的constraints,计算自己内部的constraints
finalBoxConstraintsinnerConstraints=constraints.deflate(_resolvedPadding!);
///第三步,继续向下遍历layout
child!.layout(innerConstraints,parentUsesSize:true);
finalBoxParentDatachildParentData=child!.parentData!asBoxParentData;
childParentData.offset=Offset(_resolvedPadding!.left,_resolvedPadding!.top);
///第四步,根据constraints生成size
size=constraints.constrain(Size(
_resolvedPadding!.left+child!.size.width+_resolvedPadding!.right,
_resolvedPadding!.top+child!.size.height+_resolvedPadding!.bottom,
));
}
这样就完成了树的深度遍历过程

盒子约束模型是一种很强大的布局对象的方式,时间复杂度为O(n)
所有RenderObjects的根节点是RenderView,它代表了整个渲染树的输出。当平台需要渲染新的帧时(例如,一个vsync信号触发,或者texture的解压/上传完成)会调用RenderView对象中的compositeFrame()方法,它创建了一个SceneBuilder触发屏幕的更新。当更新完成时,RenderView会传递这个压缩的scene到dart:ui包中的Window.render()方法,该方法控制GPU将它渲染。
是一一对应的关系吗
从上面图中可以轻松看出,并不是。
类型 Widget Element RenderObject 说明 组合型 StatelessWidget ComponentElement NA 组合节点,不对应RenderObject StatefulWidget NA 代理型 ProxyWidget NA 代理组件,数据传递 展示型 RenderObjectWidget RenderObjectElement RenderObject 实际渲染对象 表中仅列出了常用Widget和对应关系,并不代表全部
所以说widget和element和renderObject是一一对应是有语境的,在展示型这一行的情况下是没问题的,但是在全局范围这么说,是不准确的。
建立过程
上面粗略的看了三颗树的转化过程,那么在代码层面,他们是如何经过方法的调用串联起来的呢?可以主要分为两个过程:
根view的attachRootWidget
初始化Widget树Element树和RenderObject树的root节点,分别是RenderObjectToWidgetAdapter、RenderObjectToWidgetElement、RenderView。
然后在WidgetsBinding.attachRootWidget方法中,将runApp传入的rootWidget添加到widget树根RenderObjectToWidgetAdapter实例的child上,调用它的attachToRenderTree,将element关联到RenderTree上,调用了element的mount方法。
///Takesawidgetandattachesittothe[renderViewElement],creatingitif
///necessary.
///Thisiscalledby[runApp]toconfigurethewidgettree.
///*[RenderObjectToWidgetAdapter.attachToRenderTree],whichinflatesa
///widgetandattachesittotherendertree.
voidattachRootWidget(WidgetrootWidget){
finalboolisBootstrapFrame=renderViewElement==null;
_readyToProduceFrames=true;
_renderViewElement=RenderObjectToWidgetAdapter<RenderBox>(
container:renderView,
debugShortDescription:'[root]',
child:rootWidget,
).attachToRenderTree(buildOwner!,renderViewElementasRenderObjectToWidgetElement<RenderBox>?);
if(isBootstrapFrame){
SchedulerBinding.instance!.ensureVisualUpdate();
}
}
其中的renderView就是RenderObject tree上的根节点,它是在RendererBinding类中被初始化的
///ThegluebetweentherendertreeandtheFlutterengine.
///rendertree和Flutterengine之间的胶水
mixinRendererBindingonBindingBase,ServicesBinding,SchedulerBinding,GestureBinding,SemanticsBinding,HitTestable{
@override
voidinitInstances(){
super.initInstances();
///...
initRenderView();
///...
}
voidinitRenderView(){
///...
renderView=RenderView(configuration:createViewConfiguration(),window:window);
renderView.prepareInitialFrame();
}
}
attachToRenderTree方法
///Usedby[runApp]tobootstrapapplications.
///供runApp使用来引导程序
classRenderObjectToWidgetAdapter<TextendsRenderObject>extendsRenderObjectWidget{
///Usedby[runApp]tobootstrapapplications.
RenderObjectToWidgetElement<T>attachToRenderTree(BuildOwnerowner,[RenderObjectToWidgetElement<T>?element]){
if(element==null){
owner.lockState((){
element=createElement();
assert(element!=null);
element!.assignOwner(owner);
});
owner.buildScope(element!,(){
element!.mount(null,null);
});
}else{
element._newWidget=this;
element.markNeedsBuild();
}
returnelement!;
}
RenderObjectToWidgetElement<T>createElement()=>RenderObjectToWidgetElement<T>(this);
}
这里element为空,所以创建了RenderObjectToWidgetElement的实例,然后mount。
子view的attachToRenderTree
element的mount方法中,这里触发了挂载element到Element tree,判断是包含渲染对象的RenderObjectElement就创建RenderObject,调用attachRenderObject挂载到RenderObject tree上。然后_rebuild→updateChild→inflateWidget→newWidget.createElement→newChild.mount(this, newSlot)触发了树的深度遍历,时序图如下(粗略)

关键的一点是,newChild.mount方法会调用Element的子类型主要是两个SingleChildRenderObjectElement和MultiChildRenderObjectElement,名字起的很明显,一个孩子或者多个孩子的Element。mount方法如下
classSingleChildRenderObjectElementextendsRenderObjectElement{
@override
voidmount(Element?parent,Object?newSlot){
super.mount(parent,newSlot);
_child=updateChild(_child,widget.child,null);
}
}
classMultiChildRenderObjectElementextendsRenderObjectElement{
@override
voidmount(Element?parent,Object?newSlot){
super.mount(parent,newSlot);
finalList<Element>children=List<Element>.filled(widget.children.length,_NullElement.instance,growable:false);
Element?previousChild;
for(inti=0;i<children.length;i+=1){
finalElementnewChild=inflateWidget(widget.children[i],IndexedSlot<Element?>(i,previousChild));
children[i]=newChild;
previousChild=newChild;
}
_children=children;
}
}
可见它们都做了两件事:
- 调用super.mount(),挂载element到Element tree,createRenderObject,attachRenderObject,挂载_renderObject到RenderObject tree
- updateChild,传入widget.child,继续下一层级的widget树的转换,这里slot分别传的为null,和IndexedSlot对象
如果Element节点是ComponentElement类型,mount方法如下
abstractclassComponentElementextendsElement{
@override
voidmount(Element?parent,Object?newSlot){
super.mount(parent,newSlot);
///...
_firstBuild();
assert(_child!=null);
}
///最终会调到performRebuild
@override
voidperformRebuild(){
Widget?built;
try{
///我们经常在代码中重写的build()函数,就是这里
built=build();
}catch(e,stack){
///构建错误页面ErrorWidget,我们看的到错误红色页面
built=ErrorWidget.builder(
_debugReportException(
ErrorDescription('building$this'),
e,
stack,
informationCollector:()sync*{
yieldDiagnosticsDebugCreator(DebugCreator(this));
},
),
);
}
///更新widget,继续循环
_child=updateChild(_child,built,slot);
}
///在StatelessWidget/StafulWidget中重写的方法
@protected
Widgetbuild();
}
Slot对象
updateChild传入的slot对象是干什么用的呢?一句话总结就是,为了标记RenderObject挂载到RenderObject tree上的位置。
首先,每一个Element都会最终包裹一个RenderObject,最终挂载到RenderObject tree上,不管是自身包裹,或者是它的子孙包裹。所以,当Element的直接child不包含RenderObject时,例如StatelessElement/StatefulElement,它就要标记下一个RenderObject对象要挂载到RenderObject tree上的哪个节点。所以,在它们的父类ComponentElement的updateChild方法中传的slot值就是要挂载的位置。比如这样的element节点,会一直向下传递slot直到是RenderObjectElement节点。

那么这个值什么情况下会初始化并往下传递呢?SingleChildRenderObjectElement往下传递的是null,看来它并不需要插槽,看下attachRenderObject方法
@override
voidattachRenderObject(Object?newSlot){
assert(_ancestorRenderObjectElement==null);
_slot=newSlot;
///找到是RenderObjectElement对象的祖先节点
_ancestorRenderObjectElement=_findAncestorRenderObjectElement();
///根据newSlot插槽,插入renderObject到渲染树
_ancestorRenderObjectElement?.insertRenderObjectChild(renderObject,newSlot);
finalParentDataElement<ParentData>?parentDataElement=_findAncestorParentDataElement();
if(parentDataElement!=null)
_updateParentData(parentDataElement.widget);
}
RenderObjectElement?_findAncestorRenderObjectElement(){
Element?ancestor=_parent;
///循环向上找到第一个RenderObjectElement的对象,其实就是为了找到RenderObject的父节点
while(ancestor!=null&&ancestoris!RenderObjectElement)
ancestor=ancestor._parent;
returnancestorasRenderObjectElement?;
}
所以单个孩子的SingleChildRenderObjectElement不需要slot,因为总能找到 ancestor挂载点。而MultiChildRenderObjectElement,由于多个孩子都找到同一个ancestor节点,所以就有了slot将兄弟节点按顺序排列起来,生成IndexedSlot<Element?>(i, previousChild)的slot,这就有了初始的slot往下传递,所以slot是从MultiChildRenderObjectElement这样的节点开始分化的
这里排除了刚开始建立渲染树的根节点_rootChildSlot

这样就完成了,Element tree,和RenderObject tree的父子节点/兄弟节点之间的错落有致的树型结构。RenderObjectElement在整个过程中,占据核心的功能,同时负责控制widget向下更新,和RenderObject生成,挂载到Render tree的正确节点上。
总结
本篇为三棵树理解的第一篇,重点分析了三棵树的建立过程,下一篇我们继续分析三棵树的刷新过程,以及为什么要设计三棵树,以及理解了三棵树的概念,对我们开发中有哪些指导或者注意的点。
文中难免有个人理解,有偏差的地方,请大家批评指正,多谢!
奇舞团是360集团最大的前端团队,欢迎大家关注~