如果要搞清楚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和renderObjectRender渲染树对象,由他最终提供渲染方法 比如markNeedsLayoutperformLayoutmarkNeedsPaintpaint等方法,渲染在屏幕上,与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()创建出rootElementrootElement对象调用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.drawFramebuildScope在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()方法,重复上面的步骤,直至完成三棵树的创建。