SchedulerBinding原理

192 阅读9分钟

SchedulerBinding 是 Flutter 框架中负责协调帧调度的核心组件,它管理着应用程序的渲染时机和各种回调的执行顺序。下面我将详细分析 SchedulerBinding 的调度原理和状态切换机制。

核心状态:SchedulerPhase

SchedulerBinding 通过 SchedulerPhase 枚举定义了调度过程中的不同阶段:

enum SchedulerPhase {
  idle,                // 空闲状态,没有正在处理的帧
  transientCallbacks,  // 处理临时回调(如动画)
  midFrameMicrotasks,  // 处理微任务
  persistentCallbacks, // 处理持久回调(如布局和绘制)
  postFrameCallbacks,  // 处理帧后回调
}

这些状态按顺序切换,代表了一帧处理的完整生命周期。

调度原理

1. 帧请求机制

SchedulerBinding 通过以下方法请求新帧:

void scheduleFrame() {
  if (_hasScheduledFrame || !_framesEnabled)
    return;
  ensureVisualUpdate();
}

void ensureVisualUpdate() {
  if (_hasScheduledFrame)
    return;
  _hasScheduledFrame = true;
  window.scheduleFrame();
}

当调用 scheduleFrame() 时,SchedulerBinding 会通过 window.scheduleFrame() 向底层引擎请求在下一个 VSync 信号到来时开始一个新帧。

2. 回调注册系统

SchedulerBinding 维护了三种主要的回调列表:

1.临时帧回调(Transient Frame Callbacks)

final Map<int, _FrameCallbackEntry> _transientCallbacks = <int, _FrameCallbackEntry>{};

用于动画等需要在特定帧执行一次的回调。

2.持久帧回调(Persistent Frame Callbacks)

final List<FrameCallback> _persistentCallbacks = <FrameCallback>[];

用于布局和绘制等每帧都需要执行的回调。

3.帧后回调(Post-Frame Callbacks)

final List<FrameCallback> _postFrameCallbacks = <FrameCallback>[];

用于在当前帧完成后执行的回调

3. 帧处理流程

当 VSync 信号到来时,Flutter 引擎会调用 SchedulerBinding 的两个关键方法:

3.1 handleBeginFrame
void handleBeginFrame(Duration rawTimeStamp) {
  _hasScheduledFrame = false;
  _currentFrameTimeStamp = rawTimeStamp;
  
  // 处理临时回调
  _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();
  
  // 切换到微任务阶段
  _schedulerPhase = SchedulerPhase.midFrameMicrotasks;
}

这个方法处理帧开始时的工作,主要执行临时回调(如 Ticker 的回调)。

3.2 handleDrawFrame
void handleDrawFrame() {
  // 处理持久回调
  _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!);
  
  // 返回空闲状态
  _schedulerPhase = SchedulerPhase.idle;
  _currentFrameTimeStamp = null;
}

这个方法处理帧的绘制工作,执行持久回调和帧后回调。

4. 性能模式管理

SchedulerBinding 还支持不同的性能模式,通过 requestPerformanceMode 方法实现:

PerformanceModeRequestHandle? requestPerformanceMode(DartPerformanceMode mode) {
  // 冲突请求不允许
  if (_performanceMode != null && _performanceMode != mode) {
    return null;
  }

  if (_performanceMode == mode) {
    assert(_numPerformanceModeRequests > 0);
    _numPerformanceModeRequests++;
  } else if (_performanceMode == null) {
    assert(_numPerformanceModeRequests == 0);
    _performanceMode = mode;
    _numPerformanceModeRequests = 1;
  }

  return PerformanceModeRequestHandle._(_disposePerformanceModeRequest);
}

这允许应用程序请求特定的性能模式(如 latency 或 throughput),以优化特定场景下的性能。

状态切换机制

SchedulerBinding 的状态切换遵循严格的顺序,通过 _schedulerPhase 字段跟踪当前阶段:

  1. idle → transientCallbacks:当 handleBeginFrame 被调用时
  2. transientCallbacks → midFrameMicrotasks:当所有临时回调执行完毕时
  3. midFrameMicrotasks → persistentCallbacks:当 handleDrawFrame 被调用时
  4. persistentCallbacks → postFrameCallbacks:当所有持久回调执行完毕时
  5. postFrameCallbacks → idle:当所有帧后回调执行完毕时

这种状态机设计确保了回调的执行顺序符合预期,例如:

  • 动画更新(transientCallbacks)在布局和绘制(persistentCallbacks)之前执行
  • 布局和绘制完成后才执行帧后回调(postFrameCallbacks

生命周期状态管理

除了帧内的状态切换,SchedulerBinding 还管理应用程序的生命周期状态:

void handleAppLifecycleStateChanged(AppLifecycleState state) {
  _lifecycleState = state;
  switch (state) {
    case AppLifecycleState.resumed:
    case AppLifecycleState.inactive:
      _framesEnabled = true;
      break;
    case AppLifecycleState.paused:
    case AppLifecycleState.detached:
      _framesEnabled = false;
      break;
  }
}

当应用进入后台(paused)或分离(detached)状态时,SchedulerBinding 会禁用帧调度(_framesEnabled = false),避免不必要的计算和电池消耗。

时间膨胀(Time Dilation)

SchedulerBinding 支持时间膨胀功能,可以放慢动画速度以便调试:

double get timeDilation => _timeDilation;
double _timeDilation = 1.0;
set timeDilation(double value) {
  assert(value > 0.0);
  if (_timeDilation == value)
    return;
  _timeDilation = value;
  if (_currentFrameTimeStamp != null) {
    _adjustForTimeDilation();
  }
}

通过调整时间戳,可以实现动画的慢动作效果,这在调试复杂动画时非常有用。

优先级管理

SchedulerBinding 通过 Priority 类管理任务的优先级:

class Priority {
  static const int animation = 100000;
  static const int touch = 200000;
  // ...
}

这些优先级值用于确定任务的执行顺序,特别是在使用 scheduleTask 方法时。

总结

SchedulerBinding 通过精心设计的状态机和回调系统,实现了 Flutter 应用程序的高效帧调度:

  1. 状态驱动:通过 SchedulerPhase 明确定义帧处理的各个阶段
  2. 回调分类:将回调分为临时、持久和帧后三类,确保正确的执行顺序
  3. 生命周期感知:根据应用程序的生命周期状态调整帧调度行为
  4. 性能优化:支持不同的性能模式和任务优先级

这种设计使得 Flutter 能够在保持高性能的同时,提供流畅的用户体验和灵活的开发模型。理解 SchedulerBinding 的调度原理和状态切换机制,对于开发高性能的 Flutter 应用和解决性能问题至关重要。

Flutter 中的持久回调(Persistent Callbacks)详解

持久回调的定义

在 Flutter 的 SchedulerBinding 中,"持久回调"(Persistent Callbacks)是指那些需要在每一帧都执行的回调函数。与临时回调(Transient Callbacks)不同,持久回调一旦注册,就会在每一帧渲染时被调用,直到应用程序结束或者这些回调被明确移除。

持久的时间范围

持久回调的"持久"并不是指某个特定的时间长度,而是指它们的生命周期特性:

  1. 跨帧持久:一旦注册,持久回调会在每一帧都被调用,而不是像临时回调那样只在特定帧执行一次。

  2. 应用生命周期:持久回调通常与特定子系统(如渲染管道)的生命周期绑定,可能持续整个应用程序的生命周期。

  3. 直到明确移除:持久回调会一直存在,直到通过 removeFrameCallback 方法明确移除。

持久回调的作用

持久回调在 Flutter 框架中扮演着关键角色:

  1. 驱动渲染管道:最重要的用途是驱动 Flutter 的渲染管道,确保视图在每一帧都得到正确更新。

  2. 布局和绘制:负责执行布局计算和实际绘制操作,将虚拟视图树转换为屏幕上的像素。

  3. 保持视图一致性:确保用户界面与应用程序状态保持同步,反映最新的数据和交互状态。

  4. 处理系统级更新:响应系统级事件(如屏幕旋转、键盘显示/隐藏等),更新界面布局。

持久回调的应用场景

1. 渲染引擎

最主要的应用场景是在 RendererBinding 中,它注册了一个持久回调来驱动整个渲染管道:

void initInstances() {
  super.initInstances();
  _instance = this;
  
  // 注册持久回调来驱动渲染管道
  addPersistentFrameCallback(_handlePersistentFrameCallback);
}

void _handlePersistentFrameCallback(Duration timeStamp) {
  drawFrame();  // 执行实际的绘制工作
}

这个持久回调确保每帧都会触发渲染管道的执行,包括布局、绘制和合成等步骤。

2. 小部件绑定

WidgetsBinding 使用持久回调来管理小部件树的构建和更新:

void drawFrame() {
  buildOwner!.buildScope(renderViewElement!);  // 构建小部件
  super.drawFrame();  // 执行渲染
  buildOwner!.finalizeTree();  // 清理
}

3. 自定义渲染组件

开发者可以创建自定义的渲染组件,这些组件可能需要在每帧更新:

class CustomRenderObject extends RenderObject {
  CustomRenderObject() {
    SchedulerBinding.instance.addPersistentFrameCallback(_update);
  }
  
  void _update(Duration timeStamp) {
    // 执行每帧所需的更新
    markNeedsPaint();
  }
  
  @override
  void dispose() {
    SchedulerBinding.instance.removeFrameCallback(_update);
    super.dispose();
  }
}

4. 性能监控系统

Flutter 的性能监控工具使用持久回调来收集每帧的性能数据:

void initPerformanceMonitoring() {
  SchedulerBinding.instance.addPersistentFrameCallback((Duration timeStamp) {
    // 记录帧开始时间
    _frameStartTime = DateTime.now();
  });
  
  // 在帧后回调中记录帧结束时间
  SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
    final Duration frameDuration = DateTime.now().difference(_frameStartTime);
    _recordFrameMetrics(frameDuration);
  });
}

总结

持久回调是 Flutter 框架中确保视图持续更新的关键机制。它们:

  1. 在每一帧都会执行,而不是一次性的
  2. 主要用于驱动渲染管道和布局系统
  3. 在临时回调(如动画)执行后运行,确保动画状态能够正确反映在屏幕上
  4. 与应用程序或特定子系统的生命周期绑定,而不是特定时间长度

理解持久回调的工作原理,对于深入掌握 Flutter 的渲染机制和开发高性能应用非常重要。特别是在需要自定义渲染行为或优化渲染性能时,了解持久回调的角色和执行时机可以帮助开发者做出更明智的设计决策。

FrameTiming

/// Time-related performance metrics of a frame.
///
/// If you're using the whole Flutter framework, please use
/// [SchedulerBinding.addTimingsCallback] to get this. It's preferred over using
/// [PlatformDispatcher.onReportTimings] directly because
/// [SchedulerBinding.addTimingsCallback] allows multiple callbacks. If
/// [SchedulerBinding] is unavailable, then see [PlatformDispatcher.onReportTimings]
/// for how to get this.
///
/// The metrics in debug mode (`flutter run` without any flags) may be very
/// different from those in profile and release modes due to the debug overhead.
/// Therefore it's recommended to only monitor and analyze performance metrics
/// in profile and release modes.
/// 帧的时间相关性能指标 
/// 如果使用完整的Flutter框架,请通过[SchedulerBinding.addTimingsCallback]获取这些指标。 
/// 这比直接使用[PlatformDispatcher.onReportTimings]更推荐,因为[SchedulerBinding.addTimingsCallback] /// 支持多个回调。如果[SchedulerBinding]不可用,请参考[PlatformDispatcher.onReportTimings]的用法。
/// 调试模式下的指标(直接运行`flutter run`)可能与profile/release模式有显著差异,
/// 因此建议仅在profile/release模式下进行性能监控和分析。

class FrameTiming {
  /// Construct [FrameTiming] with raw timestamps in microseconds.
  ///
  /// This constructor is used for unit test only. Real [FrameTiming]s should
  /// be retrieved from [PlatformDispatcher.onReportTimings].
  ///
  /// If the [frameNumber] is not provided, it defaults to `-1`.
  factory FrameTiming({
    required int vsyncStart,
    required int buildStart,
    required int buildFinish,
    required int rasterStart,
    required int rasterFinish,
    required int rasterFinishWallTime,
    int layerCacheCount = 0,
    int layerCacheBytes = 0,
    int pictureCacheCount = 0,
    int pictureCacheBytes = 0,
    int frameNumber = -1,
  }) {
    return FrameTiming._(<int>[
      vsyncStart,
      buildStart,
      buildFinish,
      rasterStart,
      rasterFinish,
      rasterFinishWallTime,
      layerCacheCount,
      layerCacheBytes,
      pictureCacheCount,
      pictureCacheBytes,
      frameNumber,
    ]);
  }

  /// Construct [FrameTiming] with raw timestamps in microseconds.
  ///
  /// List [timestamps] must have the same number of elements as
  /// [FramePhase.values].
  ///
  /// This constructor is usually only called by the Flutter engine, or a test.
  /// To get the [FrameTiming] of your app, see [PlatformDispatcher.onReportTimings].
  FrameTiming._(this._data) : assert(_data.length == _dataLength);

  static final int _dataLength = FramePhase.values.length + _FrameTimingInfo.values.length;

  /// This is a raw timestamp in microseconds from some epoch. The epoch in all
  /// [FrameTiming] is the same, but it may not match [DateTime]'s epoch.
  int timestampInMicroseconds(FramePhase phase) => _data[phase.index];

  Duration _rawDuration(FramePhase phase) => Duration(microseconds: _data[phase.index]);

  int _rawInfo(_FrameTimingInfo info) => _data[FramePhase.values.length + info.index];

  /// The duration to build the frame on the UI thread.
  ///
  /// The build starts approximately when [PlatformDispatcher.onBeginFrame] is
  /// called. The [Duration] in the [PlatformDispatcher.onBeginFrame] callback
  /// is exactly the `Duration(microseconds:
  /// timestampInMicroseconds(FramePhase.buildStart))`.
  ///
  /// The build finishes when [FlutterView.render] is called.
  ///
  /// {@template dart.ui.FrameTiming.fps_smoothness_milliseconds}
  /// To ensure smooth animations of X fps, this should not exceed 1000/X
  /// milliseconds.
  /// {@endtemplate}
  /// {@template dart.ui.FrameTiming.fps_milliseconds}
  /// That's about 16ms for 60fps, and 8ms for 120fps.
  /// {@endtemplate}
  Duration get buildDuration => _rawDuration(FramePhase.buildFinish) - _rawDuration(FramePhase.buildStart);

  /// The duration to rasterize the frame on the raster thread.
  ///
  /// {@macro dart.ui.FrameTiming.fps_smoothness_milliseconds}
  /// {@macro dart.ui.FrameTiming.fps_milliseconds}
  Duration get rasterDuration => _rawDuration(FramePhase.rasterFinish) - _rawDuration(FramePhase.rasterStart);

  /// The duration between receiving the vsync signal and starting building the
  /// frame.
  Duration get vsyncOverhead => _rawDuration(FramePhase.buildStart) - _rawDuration(FramePhase.vsyncStart);

  /// The timespan between vsync start and raster finish.
  ///
  /// To achieve the lowest latency on an X fps display, this should not exceed
  /// 1000/X milliseconds.
  /// {@macro dart.ui.FrameTiming.fps_milliseconds}
  ///
  /// See also [vsyncOverhead], [buildDuration] and [rasterDuration].
  Duration get totalSpan => _rawDuration(FramePhase.rasterFinish) - _rawDuration(FramePhase.vsyncStart);

  /// The number of layers stored in the raster cache during the frame.
  ///
  /// See also [layerCacheBytes], [pictureCacheCount] and [pictureCacheBytes].
  int get layerCacheCount => _rawInfo(_FrameTimingInfo.layerCacheCount);

  /// The number of bytes of image data used to cache layers during the frame.
  ///
  /// See also [layerCacheCount], [layerCacheMegabytes], [pictureCacheCount] and [pictureCacheBytes].
  int get layerCacheBytes => _rawInfo(_FrameTimingInfo.layerCacheBytes);

  /// The number of megabytes of image data used to cache layers during the frame.
  ///
  /// See also [layerCacheCount], [layerCacheBytes], [pictureCacheCount] and [pictureCacheBytes].
  double get layerCacheMegabytes => layerCacheBytes / 1024.0 / 1024.0;

  /// The number of pictures stored in the raster cache during the frame.
  ///
  /// See also [layerCacheCount], [layerCacheBytes] and [pictureCacheBytes].
  int get pictureCacheCount => _rawInfo(_FrameTimingInfo.pictureCacheCount);

  /// The number of bytes of image data used to cache pictures during the frame.
  ///
  /// See also [layerCacheCount], [layerCacheBytes], [pictureCacheCount] and [pictureCacheMegabytes].
  int get pictureCacheBytes => _rawInfo(_FrameTimingInfo.pictureCacheBytes);

  /// The number of megabytes of image data used to cache pictures during the frame.
  ///
  /// See also [layerCacheCount], [layerCacheBytes], [pictureCacheCount] and [pictureCacheBytes].
  double get pictureCacheMegabytes => pictureCacheBytes / 1024.0 / 1024.0;

  /// The frame key associated with this frame measurement.
  int get frameNumber => _data.last;

  final List<int> _data; // some elements in microseconds, some in bytes, some are counts

  String _formatMS(Duration duration) => '${duration.inMicroseconds * 0.001}ms';

  @override
  String toString() {
    return '$runtimeType(buildDuration: ${_formatMS(buildDuration)}, '
        'rasterDuration: ${_formatMS(rasterDuration)}, '
        'vsyncOverhead: ${_formatMS(vsyncOverhead)}, '
        'totalSpan: ${_formatMS(totalSpan)}, '
        'layerCacheCount: $layerCacheCount, '
        'layerCacheBytes: $layerCacheBytes, '
        'pictureCacheCount: $pictureCacheCount, '
        'pictureCacheBytes: $pictureCacheBytes, '
        'frameNumber: ${_data.last})';
  }
}