Flutter之SchedulerBinding简析

开始

在原生开发中(例如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实例化的时候:

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会像游戏一样疯狂消耗电量,消耗电量表现应该跟原生没有多大差别。

    原文作者:tain335
    原文地址: https://segmentfault.com/a/1190000011935445
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞