Flutter 必知必会系列 —— 从 SchedulerBinding 中看 Flutter 帧调度
持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
前面我们介绍了 GestureBinding
,知道了 Flutter
的手势处理流程,这一篇我们就从 SchedulerBinding
的代码中看帧调度过程。
往期精彩:
Flutter 必知必会系列 —— mixin 和 BindingBase 的巧妙配合
Flutter 必知必会系列 —— 从 GestureBinding 中看 Flutter 手势处理过程
Flutter 帧的阶段
Flutter 把一帧分为了 6 个阶段,每个阶段做不同的事情,并且把帧阶段包装在枚举类 SchedulerPhase
中,作为前期准备,我们认识一下这个类和这几个阶段是啥。
idle —— 空闲
这个阶段没有帧任务执行,在这个阶段执行的代码是:SchedulerBinding.scheduleTask
发布的 TaskCallback
、scheduleMicrotask
注册的微任务、Timer
声明的任务、用户的手势处理器、Future
和 Stream
的任务。
这里大家需要先看一下 Dart 的任务执行顺序,这个链接需要翻墙哦~,了解一下 Dart 是如何处理事件队列、微任务队列的,以便我们写出更好的异步代码。
transientCallbacks —— 瞬时任务阶段
这个阶段执行 SchedulerBinding.scheduleFrameCallback
添加的回调,一般情况下,这些回调执行动画有关的计算,我们在前面的动画介绍中,讲过这个地方,👉
Flutter 动画是这么动起来的
midFrameMicrotasks —— 帧中微任务处理阶段
transientCallbacks 也会产生一些微任务,这些微任务会在这个阶段执行。
persistentCallbacks —— 持续任务处理阶段
这个阶段主要处理 SchedulerBinding.addPersistentFrameCallback
添加的回调,与 transientCallbacks
阶段相对应,这个阶段处理 build/layout/paint
。
postFrameCallbacks —— 帧结束阶段
这个阶段执行 SchedulerBinding.addPostFrameCallback
添加的回调,一般情况下会做一些帧清理工作和发起下一帧。
比如我们举个例子:
void update(TextEditingValue newValue) {
if (_value == newValue)
return;
_value = newValue;
if (SchedulerBinding.instance!.schedulerPhase == SchedulerPhase.persistentCallbacks) { //第一处
SchedulerBinding.instance!.addPostFrameCallback(_markNeedsBuild);
} else {
_markNeedsBuild();
}
}
这一段代码是 EditText
的代码,在更新显示字段的时候,看第一处的显示逻辑。如果当前是绘制阶段,那就在本帧的结尾增加一个发起重新 build 的任务,否则就直接发起重新 build 任务。
小结
帧的调度分为六个阶段,代码体现在 SchedulerPhase 中,帧和队列的执行和关联如下:
Flutter 发起帧调度
帧调度的方式
Flutter 中发起帧调度的方法就下面几个:
方法名 | 作用 |
---|---|
scheduleWarmUpFrame | 调度一帧,并且该帧尽可能快的执行,不需要等待引擎的 "Vsync" 信号 |
scheduleFrame | 调度一帧 |
scheduleForcedFrame | 强行调度一帧,即使是在熄屏的情况下也会执行 |
scheduleFrameCallback | 调度一帧,并且给这一帧设置一个执行回调,回调会在 transient 阶段执行 |
这几个方法大差不差,我们以核心的 scheduleFrame 的为例,看看干了啥事。
void scheduleFrame() {
if (_hasScheduledFrame || !framesEnabled)
return;
ensureFrameCallbacksRegistered();//第一处
window.scheduleFrame();
_hasScheduledFrame = true;
}
void ensureFrameCallbacksRegistered() {
window.onBeginFrame ??= _handleBeginFrame;
window.onDrawFrame ??= _handleDrawFrame;
}
就干了两件事:第一:确保 window
的 onBeginFrame
和 onDrawFrame
回调已经设置了。
第二:调用 window
的发起帧流程。
之前我们提到过,Flutter 和 Native 的交互都是通过回调的方式,window 的发起流程最终会调用到
void scheduleFrame() native 'PlatformConfiguration_scheduleFrame';
这是一个 native 方法,相当于 Flutter 告诉原生,原生请开始绘制吧,我已经准备好了。然后与原生在收到 "Vsync" 信号之后,会调用到第一处注册的两个回调 onBeginFrame 和 onDrawFrame。
我们看:
@pragma('vm:entry-point')
void _beginFrame(int microseconds, int frameNumber) {
PlatformDispatcher.instance._beginFrame(microseconds);
PlatformDispatcher.instance._updateFrameData(frameNumber);
}
@pragma('vm:entry-point')
void _drawFrame() {
PlatformDispatcher.instance._drawFrame();
}
-----------------------------------------------------------------------------
void scheduleFrame() native 'PlatformConfiguration_scheduleFrame';
那么 window
的 onBeginFrame
和 onDrawFrame
便是帧调度执行的内容了。
onBeginFrame 开始响应
我们来看 Flutter 是怎么响应的。
/// ...代码省略
void handleBeginFrame(Duration? rawTimeStamp) {
_hasScheduledFrame = false;
try {
_schedulerPhase = SchedulerPhase.transientCallbacks; //第一处
final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
_transientCallbacks = <int, _FrameCallbackEntry>{};
callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
if (!_removedIds.contains(id))
_invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, callbackEntry.debugStack); //第二处
});
_removedIds.clear();
} finally {
_schedulerPhase = SchedulerPhase.midFrameMicrotasks; // 第三处
}
}
第一处和第三处是修改调度的阶段,先设置为 transientCallbacks 阶段,并且在这个阶段,执行了 _transientCallbacks 中的回调。
_transientCallbacks 中的回调是啥呢? 就是 scheduleFrameCallback 方法参数中添加的。
int scheduleFrameCallback(FrameCallback callback, { bool rescheduling = false }) {
scheduleFrame();
_nextFrameCallbackId += 1;
_transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling);
return _nextFrameCallbackId;
}
\_transientCallbacks
是一个 Map
,key
是帧的 id
,value
是这一帧在 transient
阶段应该执行的回调数组。
在使用 scheduleFrameCallback
方法发起帧任务的时候,需要传递一个 callback
回调,这个回调就是发起帧的下一帧的 transient
阶段执行。
那么谁调用了 scheduleFrameCallback
这个方法呢? 就是动画!所以动画的计算先与布局绘制等。具体可以看这里👉 Flutter 动画是这么动起来的
不管回调会不会异常,都会执行到第三处,第三处就是帧调度进入了 midFrameMicrotasks
阶段。
上面就是 \_handleBeginFrame
,会执行本帧的 \_transientCallbacks
回调,因为动画发起的时候会设置这个回调,所以基本就是动画的计算。
因为动画会是 Future 等的计算,所以在 midFrameMicrotasks 阶段,这些异步的计算依然会执行。
下面我们看 _handleDrawFrame
的执行。
onDrawFrame 绘制任务
/// 代码省略
void handleDrawFrame() {
try {
// PERSISTENT FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.persistentCallbacks; //第一处
for (final FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp!);
// POST-FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.postFrameCallbacks; //第二处
final List<FrameCallback> localPostFrameCallbacks =
List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (final FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp!);
} finally {
_schedulerPhase = SchedulerPhase.idle;
_currentFrameTimeStamp = null;
}
}
上面的代码还是比较清晰的,就是 修改状态、执行回调
。我们仔细看。
首先看第一处的代码,就是从 midFrameMicrotasks
阶段进入到 persistentCallbacks
。
然后执行 _persistentCallbacks
回调,_persistentCallbacks
回调是谁呢?
_persistentCallbacks
是通过 addPersistentFrameCallback
添加的。
void addPersistentFrameCallback(FrameCallback callback) {
_persistentCallbacks.add(callback);
}
那么谁调用了这个方法呢?就是 RendererBinding 的初始化中。
/// 代码省略
mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable {
@override
void initInstances() {
super.initInstances();
addPersistentFrameCallback(_handlePersistentFrameCallback);//第一处
}
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
}
void drawFrame() {
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
}
}
就是第一处的代码, RendererBinding 初始化时,添加了全局的帧 persistent 回调。 回调的任务就是布局(Layout)、合成(CompositingBit)、绘制(Paint)。
所以说,在 persistentCallbacks
阶段,执行的任务就是布局、合成、绘制。
我们继续看,persistentCallbacks
的回调执行完了之后,就会到 postFrameCallbacks
阶段,执行 _postFrameCallbacks
回调。postFrameCallbacks
阶段是帧的末尾阶段,大家可以使用这个方法来做一些收尾的工作。比如获取尺寸等等。和 persistentCallbacks
不同,_postFrameCallbacks
是一次性的。
void handleDrawFrame() {
try {
// PERSISTENT FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (final FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp!); //第一处
_schedulerPhase = SchedulerPhase.postFrameCallbacks;
final List<FrameCallback> localPostFrameCallbacks =
List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear(); //第二处
for (final FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp!);
} finally {
_schedulerPhase = SchedulerPhase.idle;
_currentFrameTimeStamp = null;
}
}
第一处并没清空 _persistentCallbacks
回调,第二处在执行完了之后,会清空 _postFrameCallbacks
回调。
所以大家通过 addPostFrameCallback
添加的回调只会执行一次。
小结
现在我们知道了发起帧调度就是通知 Native:请调度我吧,我的回调已经准备好了!分别是 onBeginFrame
和 onDrawFrame
。
总结
帧调度就说完啦,和我们平时写的代码一样,把一个大任务分成几个阶段,每个阶段对应一个回调数组,从开始到结束依次是:动画、布局、合成、绘制、收尾。