likes
comments
collection
share

Flutter 必知必会系列 —— 从 SchedulerBinding 中看 Flutter 帧调度

作者站长头像
站长
· 阅读数 13

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

前面我们介绍了 GestureBinding,知道了 Flutter 的手势处理流程,这一篇我们就从 SchedulerBinding 的代码中看帧调度过程。

往期精彩:

Flutter 必知必会系列 —— runApp 做了啥

Flutter 必知必会系列 —— mixin 和 BindingBase 的巧妙配合

Flutter 必知必会系列 —— 从 GestureBinding 中看 Flutter 手势处理过程

Flutter 帧的阶段

Flutter 把一帧分为了 6 个阶段,每个阶段做不同的事情,并且把帧阶段包装在枚举类 SchedulerPhase 中,作为前期准备,我们认识一下这个类和这几个阶段是啥。

idle —— 空闲

这个阶段没有帧任务执行,在这个阶段执行的代码是:SchedulerBinding.scheduleTask 发布的 TaskCallbackscheduleMicrotask 注册的微任务、Timer 声明的任务、用户的手势处理器、FutureStream 的任务。

这里大家需要先看一下 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 必知必会系列 —— 从 SchedulerBinding 中看 Flutter 帧调度

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;
}

就干了两件事:第一:确保 windowonBeginFrameonDrawFrame 回调已经设置了。 第二:调用 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';

那么 windowonBeginFrameonDrawFrame 便是帧调度执行的内容了。

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 是一个 Mapkey 是帧的 idvalue 是这一帧在 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:请调度我吧,我的回调已经准备好了!分别是 onBeginFrameonDrawFrame

总结

帧调度就说完啦,和我们平时写的代码一样,把一个大任务分成几个阶段,每个阶段对应一个回调数组,从开始到结束依次是:动画、布局、合成、绘制、收尾

Flutter 必知必会系列 —— 从 SchedulerBinding 中看 Flutter 帧调度