前言
前面几篇文章介绍了Flutter框架的渲染流水线,window
,初始化以及Widget
,Element
和RenderObject
体系。其中对Widget
,Element
和RenderObject
的介绍主要是一些静态的说明,了解了以上这些技术点之后,在这篇文章里我们会通过动态运行的方式来介绍一下Flutter框架是如何运行的。
从之前介绍的渲染流水线可以知道,这个过程大致可以分为两段操作。第一段是从State.setState()
到去engine那里请求一帧,第二段就是Vsync信号到来以后渲染流水线开始重建新的一帧最后送入engine去显示。我们先来看第一段Flutter框架都做了什么。
调度之前
先看一下State.setState()
void setState(VoidCallback fn) {
final dynamic result = fn() as dynamic;
_element.markNeedsBuild();
}
这里会调用到Element
的markNeedsBuild()
函数。
void markNeedsBuild() {
if (!_active)
return;
if (dirty)
return;
_dirty = true;
owner.scheduleBuildFor(this);
}
Element
首先看自己是不是active的状态,不是的话就直接返回了,如果是“脏”(dirty)的状态也直接返回,不是的话会置上这个状态然后调用BuildOwner
的scheduleBuildFor()
函数,这个BuildOwner
我们之前介绍过,它的实例是在WidgetsBinding
初始化的时候构建的。每个Element
的都会持有BuildOwner
的引用。由其父Element
在mount
的时候设置。
void scheduleBuildFor(Element element) {
if (element._inDirtyList) {
_dirtyElementsNeedsResorting = true;
return;
}
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled();
}
_dirtyElements.add(element);
element._inDirtyList = true;
}
BuildOwner
会维护一个_dirtyElements
列表,所有被标记为“脏”(dirty)的element
都会被添加进去。在此之前会调用onBuildScheduled()
。这个函数是WidgetsBinding
初始化的时候设置给BuildOwner
的,对应的是WidgetsBinding._handleBuildScheduled()
。
void _handleBuildScheduled() {
ensureVisualUpdate();
}
这里会调用到ensureVisualUpdate()
。这个函数定义在SchedulerBinding
里的
void ensureVisualUpdate() {
switch (schedulerPhase) {
case SchedulerPhase.idle:
case SchedulerPhase.postFrameCallbacks:
scheduleFrame();
return;
case SchedulerPhase.transientCallbacks:
case SchedulerPhase.midFrameMicrotasks:
case SchedulerPhase.persistentCallbacks:
return;
}
}
函数ensureVisualUpdate()
会判断当前调度所处的状态,如果是在idle
(空闲)或者postFrameCallbacks
运行状态则调用scheduleFrame()
。其他状态则直接返回。下面这三个状态正是渲染流水线运行的时候。
void scheduleFrame() {
if (_hasScheduledFrame || !_framesEnabled)
return;
window.scheduleFrame();
_hasScheduledFrame = true;
}
在函数scheduleFrame()
里我们看到了熟悉的window
。这里就是通知engine去调度一帧的地方了。调度之后会置上_hasScheduledFrame
标志位,避免重复请求。另外一个标志位_framesEnabled
是代表当前app的状态,或者说其所处的生命周期是否允许刷新界面。这个状态有四种:resumed
,inactive
,paused
和suspending
。
-
resumed
:app可见且可以响应用户输入。 -
inactive
:app不能响应用户输入,例如在Android上弹出系统对话框。 -
paused
:app对用户不可见。 -
suspending
:app挂起??这个状态貌似Android和iOS都没有上报。
_framesEnabled
只有在resumed
和inactive
状态下才为true
。也就是说,只有在这两个状态下Flutter框架才会刷新页面。
至此第一阶段,也就是调度之前的工作做完了。看起来比较简单,主要就是把需要重建的Element
放入_dirtyElements
列表。接下来Flutter框架会等待Vsync信号到来以后engine回调框架,这就是第二段要做的事情了。
Vsync到来之后
我们之前说过Vsync信号到来之后,engin会按顺序回调window
的两个回调函数:onBeginFrame()
和onDrawFrame()
。这两个回调是SchedulerBinding
初始化的时候设置给window
的。对应的是SchedulerBinding.handleBeginFrame()
和SchedulerBinding.handleDrawFrame()
。
onBeginFrame
这个回调会直接走到SchedulerBinding.handleBeginFrame()
。
void handleBeginFrame(Duration rawTimeStamp) {
...
_hasScheduledFrame = false;
try {
// TRANSIENT FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.transientCallbacks;
final Map callbacks = _transientCallbacks;
_transientCallbacks = {};
callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
if (!_removedIds.contains(id))
_invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack);
});
_removedIds.clear();
} finally {
_schedulerPhase = SchedulerPhase.midFrameMicrotasks;
}
}
这个函数主要是在依次回调“Transient”回调函数,这些回调函数是在调度之前设置在SchedulerBinding
里的,这里的“Transient”意思是临时的,或者说是一次性的。原因是这些回调函数只会被调用一次。注意看代码里_transientCallbacks
被置为空Map
了。如果想在下一帧再次调用的话需要提前重新设置回调。这些回调主要和动画有关系。也就是渲染流水线里的第一阶段,动画(Animate)阶段。关于动画后续我会再写文章从框架角度分析一下动画的机制。
在运行回调之前_schedulerPhase
的状态被设置为SchedulerPhase.transientCallbacks
。回调处理完以后状态更新至SchedulerPhase.midFrameMicrotasks
意思是接下来会处理微任务队列。处理完微任务以后,engine会接着回调onDrawFrame()
。
onDrawFrame
这个回调会直接走到SchedulerBinding.handleDrawFrame()
。
void handleDrawFrame() {
try {
// PERSISTENT FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
// POST-FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.postFrameCallbacks;
final List localPostFrameCallbacks =
List.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
} finally {
_schedulerPhase = SchedulerPhase.idle;
_currentFrameTimeStamp = null;
}
}
在handleDrawFrame
里按顺序处理了两类回调,一类叫“Persistent”回调,另一类叫“Post-Frame”回调。
“Persistent”字面意思是永久的。这类回调一旦注册以后是不能取消的。主要用来驱动渲染流水线。渲染流水线的构建(build),布局(layout)和绘制(paint)阶段都是在其中一个回调里的。
“Post-Frame”回调主要是在新帧渲染完成以后的一类调用,此类回调只会被调用一次。
在运行“Persistent”回调之前_schedulerPhase
状态变为SchedulerPhase.persistentCallbacks
。在运行“Post-Frame”回调之前_schedulerPhase
状态变为SchedulerPhase.postFrameCallbacks
。最终状态变为SchedulerPhase.idle
。
这里我们主要关注一个“Persistent”回调:WidgetsBinding.drawFrame()
。这个函数是在RendererBinding
初始化的时候加入到“Persistent”回调的。
void drawFrame() {
try {
if (renderViewElement != null)
buildOwner.buildScope(renderViewElement);
super.drawFrame();
buildOwner.finalizeTree();
} finally {
...
}
}
这里首先会调用buildOwner.buildScope(renderViewElement)
。其入参renderViewElement
是element tree的根节点。此时渲染流水线就进入了构建(build)阶段。接下来调用了super.drawFrame()
。这个函数定义在RendererBinding
中。
void drawFrame() {
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
可以看出渲染流水线的接力棒传到了pipelineOwner
的手里,渲染流水线就进入了布局(layout)阶段和绘制(paint)阶段。关于最后这两个阶段本篇不做详细介绍。这里大家只要知道绘制完成以后Flutter框架最终会调用window.render(scene)
将新帧的数据送入engine显示到屏幕。
最后调用buildOwner.finalizeTree();
。这个函数的作用是清理不再需要的Element
节点。在element tree更新以后可能有些节点就不再需要挂载在树上了,在finalizeTree()
的时候会将这些节点及其子节点unmount。
构建(build)阶段
void buildScope(Element context, [VoidCallback callback]) {
try {
_scheduledFlushDirtyElements = true;
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
int dirtyCount = _dirtyElements.length;
int index = 0;
while (index < dirtyCount) {
try {
_dirtyElements[index].rebuild();
} catch (e, stack) {
...
}
index += 1;
}
} finally {
for (Element element in _dirtyElements) {
element._inDirtyList = false;
}
_dirtyElements.clear();
_scheduledFlushDirtyElements = false;
_dirtyElementsNeedsResorting = null;
}
}
还记得在调度帧之前会把需要更新的Element
标记为“脏”(dirty)并放入BuildOwner
的_dirtyElements
列表。这里Flutter会先按照深度给这个列表排个序。因为Element
在重建的时候其子节点也都会重建,这样如果父节点和子节点都为“脏”的话,先重建父节点就避免了子节点的重复重建。
排完序就是遍历_dirtyElements
列表。依次调用Element.rebuild()
。这个函数又会调用到Element.performRebuild()
。我们之前介绍Element
的时候说过performRebuild()
由其子类实现。
我们之前的出发点是State.setState()
。那就先看看StatefulElement
如何做的。它的performRebuild()
是在其父类ComponentElement
里:
void performRebuild() {
Widget built;
built = build();
try {
_child = updateChild(_child, built, slot);
} catch (e, stack) {
...
}
}
回忆一下ComponentElement
。这个build()
函数最终会调用到State.build()
了。返回的就是我们自己实例化的Widget
。拿到这个新Widget
就去调用updateChild()
。之前在讲Element
的时候我们介绍过updateChild()
这个函数。由增,删,改这么几种情况,对于MyWidget
,从State.setState()
过来是属于改的情况。此时会调用child.update(newWidget);
。这个update()
函数又是由各个Element
子类实现的。这里我们只列举几个比较典型的。
StatefulElement
和StatelessElement
的update()
函数最终都会调用基类Element
的rebuild()
函数。好像在兜圈圈的感觉。。。
RenderObjectElement
的update()
函数就比较简单了
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
更新只是调用了一下RenderObjectWidget.updateRenderObject()
。这个函数我们之前介绍过,只是把新的配置设置到现有的RenderObject
上。
回到上面那个兜圈圈的问题。理清这里的调用关系的关键就是要搞清楚是此时的Element
是在对自己进行操作还是对孩子进行操作。假设我们有这样的一个三层element tree进行更新重建。
父(StatefulElement)
子(StatefulElement)
孙(LeafRenderObjectElement)
那么从父节点开始,调用顺序如下:
父.rebuild()--->父.performRebuild()--->父.updateChild()--->子.update()--->子.rebuild()--->子.performRebuild()--->子.updateChild()--->孙.update()。
可见构建(build)过程是从需要重建的Element
节点开始一层层向下逐个更新子节点。直到遇到叶子节点为止。
至此渲染流水线的构建(build)阶段就跑完了。接下来就由pipelineOwner
驱动开始布局(layout)和绘制(paint)阶段了。这两个阶段留待以后再给大家介绍一下。
总结
本篇文章从我们熟悉的State.setState()
函数出发,大致介绍了Flutter框架是如何运行渲染流水线的。总体来说其运行时分为两个阶段,向engine调度帧之前和Vsync信号到来engine回调Flutter框架之后。剩余篇幅则是以更新Element
为例介绍了一下渲染流水线的构建(build)阶段都做了一些什么事情。限于篇幅,没有更多涉及Element
的新增和删除步骤。大家感兴趣的话可以直接看源码来了解相关信息。