资讯专栏INFORMATION COLUMN

Flutter之SchedulerBinding简析

BlackMass / 2751人阅读

摘要:但是接下来并不是讨论单线程如何方便开发,而是要深入的调度器,看一下是如何安排任务,调度工作。总结在大部分情况下,其实并不用担心会像游戏一样疯狂消耗电量,消耗电量表现应该跟原生没有多大差别。

开始

在原生开发中(例如Android)都会强调不能阻塞主线程,但是开发中经常会遇到发送请求或者操作数据库等,这些操作都会阻塞主线程,几乎唯一办法就是用多线程处理这些工作;而在Flutter中就像跟在前端一样,Dart也是单线程IO异步,刚才所说的这些操作既不会阻塞主线程也不会打断你的代码逻辑,所以在Flutter上开发有相当高的效率。
但是接下来并不是讨论单线程IO如何方便开发,而是要深入Flutter的Scheduler(调度器),看一下Flutter是如何安排任务,调度工作。

调度阶段

在Flutter中有几个调度阶段:

transientCallbacks
主要处理动画计算,动画状态的更新

midFrameMicrotasks
处理transientCallbacks阶段触发的Microtasks,啥是Microtasks?传送门

persistentCallbacks
主要处理build/layout/paint

postFrameCallbacks
主要在下一帧之前,做一些清理工作或者准备工作

idle
不产生Frame的空闲期,可以处理Tasks(由SchedulerBinding.scheduleTask触发),microtasks(由scheduleMicrotask触发),定时器的回调,响应事件处理(例如:用户的输入)

分析

这个几个阶段是如何定义出来的尼?
在SchedulerBinding实例化的时候:

</>复制代码

  1. void initInstances() {
  2. super.initInstances();
  3. _instance = this;
  4. ui.window.onBeginFrame = handleBeginFrame;
  5. ui.window.onDrawFrame = handleDrawFrame;
  6. }

可以看到底层暴露了两个阶段beginFrame和drawFrame,它们都是由底层触发的,一般跟屏幕的刷新速率一致,如果是60帧就是每16.7毫秒回调一次,而onDrawFrame回调是紧接着onBeginFrame回调的,因为刚才所提到Flutter有一个midFrameMicrotasks调度阶段然后结合Dart的消息循环机制,可以推断底层在Event队列中连续创建了两个Event,暂且称作:beginFrame事件和drawFrame事件。
在handleBeginFrame处理中:

</>复制代码

  1. void handleBeginFrame(Duration rawTimeStamp) {
  2. ...
  3. try {
  4. // TRANSIENT FRAME CALLBACKS
  5. Timeline.startSync("Animate", arguments: timelineWhitelistArguments);
  6. _schedulerPhase = SchedulerPhase.transientCallbacks;
  7. final Map callbacks = _transientCallbacks;
  8. _transientCallbacks = {};
  9. callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
  10. if (!_removedIds.contains(id))
  11. _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack);
  12. });
  13. _removedIds.clear();
  14. } finally {
  15. _schedulerPhase = SchedulerPhase.midFrameMicrotasks;
  16. }
  17. }

很简单遍历_transientCallbacks列表,然后回调,最后就转入midFrameMicrotasks阶段;而把回调加入_transientCallbacks列表的方法,跟前端的requestAnimationFrame方法几乎一样,调用scheduleFrameCallback方法然后会返回一个id,你也可以使用cancelFrameCallbackWithId来取消这次回调。
接着进入handleDrawFrame方法:

</>复制代码

  1. void handleDrawFrame() {
  2. Timeline.finishSync(); // end the "Animate" phase
  3. try {
  4. // PERSISTENT FRAME CALLBACKS
  5. _schedulerPhase = SchedulerPhase.persistentCallbacks;
  6. for (FrameCallback callback in _persistentCallbacks)
  7. _invokeFrameCallback(callback, _currentFrameTimeStamp);
  8. // POST-FRAME CALLBACKS
  9. _schedulerPhase = SchedulerPhase.postFrameCallbacks;
  10. final List localPostFrameCallbacks =
  11. new List.from(_postFrameCallbacks);
  12. _postFrameCallbacks.clear();
  13. for (FrameCallback callback in localPostFrameCallbacks)
  14. _invokeFrameCallback(callback, _currentFrameTimeStamp);
  15. } finally {
  16. _schedulerPhase = SchedulerPhase.idle;
  17. Timeline.finishSync(); // end the Frame
  18. _currentFrameTimeStamp = null;
  19. }
  20. // All frame-related callbacks have been executed. Run lower-priority tasks.
  21. _runTasks();
  22. }

直接进入persistentCallbacks阶段,drawFrame方法会在这里回调(build/layout/paint),然后在布局绘制完成后紧接着就进入postFrameCallbacks阶段,在这个阶段我们基本可以拿到最新的布局信息了,就像Vue的$nextTick方法一样,最后就是idle阶段,这里的默认处理就有点意思了。
直接来到_runTask方法:

</>复制代码

  1. void _runTasks() {
  2. if (_taskQueue.isEmpty || locked)
  3. return;
  4. final _TaskEntry entry = _taskQueue.first;
  5. if (schedulingStrategy(priority: entry.priority, scheduler: this)) {
  6. try {
  7. (_taskQueue.removeFirst().task)();
  8. } finally {
  9. if (_taskQueue.isNotEmpty)
  10. _ensureEventLoopCallback();
  11. }
  12. } else {
  13. scheduleFrame();
  14. }
  15. }

刚才也提到可以使用SchedulerBinding.scheduleTask加入一个task,但是task执行前想要执行首先要判断优先级,默认的判断是这样的:

</>复制代码

  1. bool defaultSchedulingStrategy({ int priority, SchedulerBinding scheduler }) {
  2. if (scheduler.transientCallbackCount > 0)
  3. return priority >= Priority.animation.value;
  4. return true;
  5. }

也就是transientCallback存在,而且task的优先级不大于animation的优先级,那么task就不会执行了。其实目标应该是为了保证动画足够流畅,因为transientCallback一般都是处理动画的,如果存在transientCallback一般就是当前有正在播放的动画,所以_runTasks方法会立马进行第二帧的调度,动画得以流畅进行。
大部分时候,等动画播放完再处理一些耗时的操作其实也并不是问题,问题是如果存在循环播放的动画就有点尴尬了,这样task就会永远都没机会执行,这是一个值得注意的地方,要么就是修改默认的调度策略,要么把安排第二次播放动画的代码放到addPostFrameCallback里面并使用scheduleMicrotask触发,这样的话在处理完一个Task之后,又可以触发第二次动画,把影响降到最低。

在schedulingStrategy方法之后,就是_ensureEventLoopCallback:

</>复制代码

  1. void _ensureEventLoopCallback() {
  2. assert(!locked);
  3. if (_hasRequestedAnEventLoopCallback)
  4. return;
  5. Timer.run(handleEventLoopCallback);
  6. _hasRequestedAnEventLoopCallback = true;
  7. }

主要驱动事件循环,其实在scheduleTask方法里面也会调用这个方法,保证task队列里面的task都可以得到处理:

</>复制代码

  1. void scheduleTask(VoidCallback task, Priority priority) {
  2. final bool isFirstTask = _taskQueue.isEmpty;
  3. _taskQueue.add(new _TaskEntry(task, priority.value));
  4. if (isFirstTask && !locked)
  5. _ensureEventLoopCallback();
  6. }

这里可以得知Flutter并不是都在以每16.7毫秒产生一帧来布局绘制界面,当没有动画,或者我们不调起setState方法,又或者说不调起ScheduleBinding.scheduleFrame有关联的方法,Flutter并不会进行布局绘制和刷新界面,这样的情况下就不能靠onBeginFrame和onDrawFrame来驱动处理task,只能靠dart自身的事件循环,这也是_ensureEventLoopCallback方法存在的必要性。

总结

在大部分情况下,其实并不用担心Flutter会像游戏一样疯狂消耗电量,消耗电量表现应该跟原生没有多大差别。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/89633.html

相关文章

  • Flutter样式和布局控件简析(一)

    摘要:但是好像反其道而行之,样式糅合在结构里面,这样究竟有啥意思尼首先应该是一个性能的考虑,浏览器解析其实也是一个性能消耗点,没有解析自然也可以加快页面的显示。 开始 搞前端的同学可能都习惯了CSS局部的思维,过去也出现过一些跟布局或者样式相关的标签,例如:big, center, font, s, strike, tt, u;但是目前也被CSS所代替,已经不推荐使用。但是在Flutter里...

    BoYang 评论0 收藏0
  • Flutter样式和布局控件简析(一)

    摘要:但是好像反其道而行之,样式糅合在结构里面,这样究竟有啥意思尼首先应该是一个性能的考虑,浏览器解析其实也是一个性能消耗点,没有解析自然也可以加快页面的显示。 开始 搞前端的同学可能都习惯了CSS局部的思维,过去也出现过一些跟布局或者样式相关的标签,例如:big, center, font, s, strike, tt, u;但是目前也被CSS所代替,已经不推荐使用。但是在Flutter里...

    wangxinarhat 评论0 收藏0
  • Flutter样式和布局控件简析(二)

    摘要:开始继续接着分析相关的样式和布局控件,但是这次内容难度感觉比较高,怕有分析不到位的地方,所以这次仅仅当做一个参考,大家最好可以自己阅读一下代码,应该会有更深的体会。关于属性,指前一个组件的布局区域和绘制区域重叠了。 开始 继续接着分析Flutter相关的样式和布局控件,但是这次内容难度感觉比较高,怕有分析不到位的地方,所以这次仅仅当做一个参考,大家最好可以自己阅读一下代码,应该会有更深...

    yck 评论0 收藏0
  • Flutter样式和布局控件简析(二)

    摘要:开始继续接着分析相关的样式和布局控件,但是这次内容难度感觉比较高,怕有分析不到位的地方,所以这次仅仅当做一个参考,大家最好可以自己阅读一下代码,应该会有更深的体会。关于属性,指前一个组件的布局区域和绘制区域重叠了。 开始 继续接着分析Flutter相关的样式和布局控件,但是这次内容难度感觉比较高,怕有分析不到位的地方,所以这次仅仅当做一个参考,大家最好可以自己阅读一下代码,应该会有更深...

    leanxi 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<