资讯专栏INFORMATION COLUMN

Flutter之SchedulerBinding简析

BlackMass / 2399人阅读

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

开始

在原生开发中(例如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 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;
    }
  }

很简单遍历_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 localPostFrameCallbacks =
          new List.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会像游戏一样疯狂消耗电量,消耗电量表现应该跟原生没有多大差别。

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

转载请注明本文地址: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元查看
<