如果要搞清楚flutter的渲染流程,必须要知道flutter中最重要的三棵树 widget three
element three
以及render tree
。他们之间的关系,以及存在的意义。其实渲染的流程也可以说是这三棵树节点对象创建的流程。
一. 三棵树之间的关系
从上图可以看的出来 widget tree
和 element tree
是一一对应的关系,但是Render tree
并没有和他们对应。这是因为只有继承自RenderObjectWidget
的widget
才会最终被渲染到屏幕上。我们经常用到的一些组件比如Container
或者是 Text
这类属于组合widget
因为他们并不是继承自RenderObjectWidget
。Container
内部其实是由 Padding
ColoredBox
Align
等组合而成,而Text
内部其实RichText
。
Widget
用来描述View的最终展示形态,比如尺寸多大,是什么颜色的 距离屏幕边缘距离是多少等等Element
与widget一一对应 为了保持树的稳定形态,同时持有widget
和renderObject
Render
渲染树对象,由他最终提供渲染方法 比如markNeedsLayout
performLayout
markNeedsPaint
paint
等方法,渲染在屏幕上,与element
并不是一一对应。
二. 对象创建的过程
1. Widget
对象的创建
Widget
对象的创建非常简单其实就是你在调用构造函数的时候,比如下面的代码
void main() {
runApp(MyApp());
}
复制代码
MyApp()
就是这个Widget
对象创建,非常简单。
2. Element
对象的创建
跟进源码我们会发现每一个Widget
类都有一个createElement()
方法,那么这个方法的调用时机就是 Element
对象创建的时候。我们再runApp()
这个方法里面发现有这么一段代码,为了方便了解这个过程我把一些无关的代码省略掉
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {
......
element = createElement();
.....
element!.mount(null, null);
.....
return element!;
}
复制代码
所以在这个方法中调用了createElement()
,根Element
创建成功。紧接着调用mount
方法将这个Element
插入到对应的Element tree
中。然后调用 inflateWidget()
方法创建 在这个方法里面会调用子element
的createElement()
方法,然后子element
继续调用mount
方法,以此类推,element tree
最终形成。
简单的梳理一下流程
attachToRenderTree()
方法调用createElement()
创建出rootElement
rootElement
对象调用mount()
方法将rootElement
插入到树的节点中,并调用inflateWidget()
- 在
inflateWidget()
方法中子widget
调用createElement()
和mount()
重复以上步骤,直至element tree
创建完成
Element inflateWidget(Widget newWidget, dynamic newSlot) {
....
final Element newChild = newWidget.createElement();
.....
newChild._activateWithParent(this, newSlot);
....
newChild.mount(this, newSlot);
....
return newChild;
}
复制代码
3. Render
对象的创建
和 Element
一样,RenderObject
类有一个很重要的方法createRenderObject()
,也就是说调用这个方法的时机就是创建RenderObject
对象的时候。前面说过只有继承自RenderObject
的widget
最终才会被渲染。才会创建RenderObject
对象。flutter
中大多数widget
的类型都有与之对应的element
如下图
所以真正继承RenderObjectElement
的对象才会真正的去调用createRenderObject()
。
跟踪源码我们发现其实是在element
对象插入到树节点的时候也就是调用mount
的时候创建了对应的renderObject
对象。
void mount(Element? parent, dynamic newSlot) {
super.mount(parent, newSlot);
......
_renderObject = widget.createRenderObject(this);
......
attachRenderObject(newSlot);
_dirty = false;
}
复制代码
三. build
方法的执行流程
在element
中有一个很重要的方法markNeedsBuild()
这个方法主要做了两件事
- 标记
element
为dirty
- 把当前的
element
对象放到全局_dirtyElements
中
执行流程
- 当
element
插入到对应的树节点之后,当前element
被标记为active
状态,然后执行activate()
- 在
activate()
方法中执行markNeedsBuild()
将该对象标记为dirty
- 之后系统会调用
_handleBuildScheduled()
scheduleWarmUpFrame()
scheduleFrame()
window.scheduleFrame();
等一系列方法,其目的是为了注册vsync
信号,注册完成等待下一次vsync
的到来 - 当下一次
vsync
到来以后会触发WidgetsBinding.drawFrame
buildScope
在buildScope()
这个方法中会获取到刚才被标记为dirty
的列表,也就是_dirtyElements
对该列表逐一进行rebuild()
- 在
rebuild()
方法中,调用对应的performRebuild()
,最终调用Widget build() => widget.build(this);
流程结束
在上面的流程中,有个细节就是下一次的vsync
什么时候到来?
我们一般用于评判UI是否卡顿,使用了一个叫做 fps(Frames Per Second)的指标,每秒需要传输的帧数。我们一般用 60fps来衡量,如果低于60 那就会产生卡顿,越低卡顿就越明显。这是因为大多数设备的刷新频率就是 60fps,也就是每一秒处理60帧的数据,所以一帧数据处理的时间大概就是 1/60s 也就是0.0166s。这个vsync
就是开始渲染第一帧的时候设备发出的信号。所以如果设备的刷新频率是 60fps,那么理想情况下,下一次vsync
信号就是0.0166
秒之后到来。
四. 总结
了解了widget
element
render
对象创建的时机以及build()
方法的执行流程以后,我们把所有的点都穿起来可以得出下面的流程
- 首先创建
widget
对象,然后在attachToRenderTree
方法中创建rootElement
rootElement
插入到element tree
,并创建renderObject
对象,紧接着执行markNeedsBuild()
方法- 通过
build
方法获取到下一个子节点的widget,然后执行inflateWidget()
创建element
对象 - 执行
mount()
方法,插入element tree
中,创建对应的renderObject
对象,然后再执行这个element
的markNeedsBuild()
方法,重复上面的步骤,直至完成三棵树的创建。