Flutter之SchedulerBinding简析
Posted tain335
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter之SchedulerBinding简析相关的知识,希望对你有一定的参考价值。
开始
在原生开发中(例如android)都会强调不能阻塞主线程,但是开发中经常会遇到发送请求或者操作数据库等,这些操作都会阻塞主线程,几乎唯一办法就是用多线程处理这些工作;而在Flutter中就像跟在前端一样,Dart也是单线程IO异步,刚才所说的这些操作既不会阻塞主线程也不会打断你的代码逻辑,所以在Flutter上开发有相当高的效率。
但是接下来并不是讨论单线程IO如何方便开发,而是要深入Flutter的Scheduler(调度器),看一下Flutter是如何安排任务,调度工作。
调度阶段
在Flutter中有几个调度阶段:
transientCallbacks
主要处理动画计算,动画状态的更新midFrameMicrotasks
处理transientCallbacks阶段触发的Microtasks,啥是Microtasks?传送门persistentCallbacks
主要处理build/layout/paintpostFrameCallbacks
主要在下一帧之前,做一些清理工作或者准备工作idle
不产生Frame的空闲期,可以处理Tasks(由SchedulerBinding.scheduleTask触发),microtasks(由scheduleMicrotask触发),定时器的回调,响应事件处理(例如:用户的输入)
分析
这个几个阶段是如何定义出来的尼?
在SchedulerBinding实例化的时候:
void initInstances() {
super.initInstances();
_instance = this;
ui.window.onBeginFrame = handleBeginFrame;
ui.window.onDrawFrame = handleDrawFrame;
}
可以看到底层暴露了两个阶段beginFrame和drawFrame,它们都是由底层触发的,一般跟屏幕的刷新速率一致,如果是60帧就是每16.7毫秒回调一次,而onDrawFrame回调是紧接着onBeginFrame回调的,因为刚才所提到Flutter有一个midFrameMicrotasks调度阶段然后结合Dart的消息循环机制,可以推断底层在Event队列中连续创建了两个Event,暂且称作:beginFrame事件和drawFrame事件。
在handleBeginFrame处理中:
void handleBeginFrame(Duration rawTimeStamp) {
...
try {
// TRANSIENT FRAME CALLBACKS
Timeline.startSync(\'Animate\', arguments: timelineWhitelistArguments);
_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列表,然后回调,最后就转入midFrameMicrotasks阶段;而把回调加入_transientCallbacks列表的方法,跟前端的requestAnimationFrame方法几乎一样,调用scheduleFrameCallback方法然后会返回一个id,你也可以使用cancelFrameCallbackWithId来取消这次回调。
接着进入handleDrawFrame方法:
void handleDrawFrame() {
Timeline.finishSync(); // end the "Animate" phase
try {
// PERSISTENT FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
// POST-FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.postFrameCallbacks;
final List<FrameCallback> localPostFrameCallbacks =
new List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
} finally {
_schedulerPhase = SchedulerPhase.idle;
Timeline.finishSync(); // end the Frame
_currentFrameTimeStamp = null;
}
// All frame-related callbacks have been executed. Run lower-priority tasks.
_runTasks();
}
直接进入persistentCallbacks阶段,drawFrame方法会在这里回调(build/layout/paint),然后在布局绘制完成后紧接着就进入postFrameCallbacks阶段,在这个阶段我们基本可以拿到最新的布局信息了,就像Vue的$nextTick方法一样,最后就是idle阶段,这里的默认处理就有点意思了。
直接来到_runTask方法:
void _runTasks() {
if (_taskQueue.isEmpty || locked)
return;
final _TaskEntry entry = _taskQueue.first;
if (schedulingStrategy(priority: entry.priority, scheduler: this)) {
try {
(_taskQueue.removeFirst().task)();
} finally {
if (_taskQueue.isNotEmpty)
_ensureEventLoopCallback();
}
} else {
scheduleFrame();
}
}
刚才也提到可以使用SchedulerBinding.scheduleTask加入一个task,但是task执行前想要执行首先要判断优先级,默认的判断是这样的:
bool defaultSchedulingStrategy({ int priority, SchedulerBinding scheduler }) {
if (scheduler.transientCallbackCount > 0)
return priority >= Priority.animation.value;
return true;
}
也就是transientCallback存在,而且task的优先级不大于animation的优先级,那么task就不会执行了。其实目标应该是为了保证动画足够流畅,因为transientCallback一般都是处理动画的,如果存在transientCallback一般就是当前有正在播放的动画,所以_runTasks方法会立马进行第二帧的调度,动画得以流畅进行。
大部分时候,等动画播放完再处理一些耗时的操作其实也并不是问题,问题是如果存在循环播放的动画就有点尴尬了,这样task就会永远都没机会执行,这是一个值得注意的地方,要么就是修改默认的调度策略,要么把安排第二次播放动画的代码放到addPostFrameCallback里面并使用scheduleMicrotask触发,这样的话在处理完一个Task之后,又可以触发第二次动画,把影响降到最低。
在schedulingStrategy方法之后,就是_ensureEventLoopCallback:
void _ensureEventLoopCallback() {
assert(!locked);
if (_hasRequestedAnEventLoopCallback)
return;
Timer.run(handleEventLoopCallback);
_hasRequestedAnEventLoopCallback = true;
}
主要驱动事件循环,其实在scheduleTask方法里面也会调用这个方法,保证task队列里面的task都可以得到处理:
void scheduleTask(VoidCallback task, Priority priority) {
final bool isFirstTask = _taskQueue.isEmpty;
_taskQueue.add(new _TaskEntry(task, priority.value));
if (isFirstTask && !locked)
_ensureEventLoopCallback();
}
这里可以得知Flutter并不是都在以每16.7毫秒产生一帧来布局绘制界面,当没有动画,或者我们不调起setState方法,又或者说不调起ScheduleBinding.scheduleFrame有关联的方法,Flutter并不会进行布局绘制和刷新界面,这样的情况下就不能靠onBeginFrame和onDrawFrame来驱动处理task,只能靠dart自身的事件循环,这也是_ensureEventLoopCallback方法存在的必要性。
总结
在大部分情况下,其实并不用担心Flutter会像游戏一样疯狂消耗电量,消耗电量表现应该跟原生没有多大差别。
以上是关于Flutter之SchedulerBinding简析的主要内容,如果未能解决你的问题,请参考以下文章