深色模式
FrameTiming
概述
FrameTiming 是 dart:ui 提供的只读帧性能数据。每个对象对应引擎已经完成 Raster 的一帧,记录这帧从 VSync、UI 阶段到 Raster 阶段的关键时间点,以及帧编号和 Raster Cache 状态。本文以原生 Flutter 应用为准,Web 端应使用 Chrome DevTools 的 Performance 面板分析。
它适合回答“这一帧的 UI、Raster 或整体延迟是多少”,但不直接包含 Widget、路由、业务操作和调用栈。理解本文前,建议先阅读 Flutter 帧流水线。线上采集、聚合和定位方法见 FrameTiming 数据采集与分析。
数据结构
FrameTiming 的数据可以分为四组:
| 数据 | 相关 API | 作用 |
|---|---|---|
| 原始时间点 | timestampInMicroseconds()、FramePhase | 还原一帧的时间轴 |
| 派生耗时 | vsyncOverhead、buildDuration、rasterDuration、totalSpan | 直接读取主要阶段耗时 |
| 帧标识 | frameNumber | 关联同一次 Engine 会话内的帧 |
| Raster Cache | layerCache*、pictureCache* | 查看该帧对应的缓存数量和占用 |
这些属性没有 setter。应用通常不需要自行构造 FrameTiming,而是接收引擎报告的实例。
时间点
FramePhase 定义了一帧生命周期中的六个时间点:
| 时间点 | 含义 |
|---|---|
vsyncStart | 操作系统给出 VSync 信号的时间 |
buildStart | UI 任务运行器开始生成这一帧的时间 |
buildFinish | UI 阶段提交 Scene 的时间 |
rasterStart | Raster 任务运行器开始栅格化的时间 |
rasterFinish | Raster 阶段完成的时间 |
rasterFinishWallTime | 使用系统墙上时钟表示的 Raster 完成时间 |
前五个时间点用于计算同一时间基准上的间隔。rasterFinishWallTime 用于和日志、系统 Trace 等墙上时钟数据关联,不参与帧内耗时计算。
可以通过 timestampInMicroseconds() 读取原始值:
dart
final vsyncStartUs = timing.timestampInMicroseconds(
FramePhase.vsyncStart,
);
final buildStartUs = timing.timestampInMicroseconds(
FramePhase.buildStart,
);
final rasterFinishUs = timing.timestampInMicroseconds(
FramePhase.rasterFinish,
);除 rasterFinishWallTime 外,不要把这些原始值直接交给 DateTime.fromMicrosecondsSinceEpoch()。它们的纪元不保证与 DateTime 相同,适合相减求间隔或与其他 FrameTiming 排序。
耗时指标
四个派生耗时都由原始时间点相减得到:
text
vsyncOverhead = buildStart - vsyncStart
buildDuration = buildFinish - buildStart
rasterDuration = rasterFinish - rasterStart
totalSpan = rasterFinish - vsyncStart它们分别表示:
| 指标 | 含义 |
|---|---|
vsyncOverhead | VSync 到达后,等待 UI 阶段开始的时间 |
buildDuration | UI 任务运行器准备并提交一帧 Scene 的时间 |
rasterDuration | Raster 任务运行器栅格化这一帧的时间 |
totalSpan | 从 VSync 开始到 Raster 完成的总跨度 |
buildDuration 中的 Build 不是某个 Widget 的 build()。它大致从 PlatformDispatcher.onBeginFrame 开始,到 FlutterView.render() 提交 Scene 为止,覆盖动画回调、Widget 重建、Layout、Paint 和场景合成等工作。
totalSpan 也不等于另外三个指标简单相加。buildFinish 与 rasterStart 之间可能存在排队和流水线等待:
text
totalSpan =
vsyncOverhead
+ buildDuration
+ (rasterStart - buildFinish)
+ rasterDurationUI 和 Raster 运行在不同的任务运行器上,还可能与相邻帧重叠。因此,buildDuration + rasterDuration 既不是这一帧的墙上耗时,也不能代替 totalSpan。
帧编号
frameNumber 是这份帧数据的关联键。它在一次 Flutter Engine 会话中单调递增,但不保证从 0 或 1 开始;未提供有效编号时值为 -1。
当前正在生成的帧号可以从 PlatformDispatcher.frameData.frameNumber 读取:
dart
final currentFrameNumber = WidgetsBinding
.instance
.platformDispatcher
.frameData
.frameNumber;只有在帧回调或帧生成阶段读取时,它才表示当前帧。在普通点击回调、Timer 或网络回调中读取,拿到的可能仍是上一帧编号。
frameNumber 不是已显示帧数,也不是掉帧数。它只能用于关联同一次 Engine 会话中的数据,跨会话使用时还需要额外的会话标识。
缓存指标
FrameTiming 同时携带 Raster Cache 的快照:
dart
final layerCount = timing.layerCacheCount;
final layerBytes = timing.layerCacheBytes;
final layerMb = timing.layerCacheMegabytes;
final pictureCount = timing.pictureCacheCount;
final pictureBytes = timing.pictureCacheBytes;
final pictureMb = timing.pictureCacheMegabytes;Layer 指标表示缓存的图层数量和图片数据占用,Picture 指标表示缓存的 Picture 数量和占用。这些值可以观察缓存规模变化,但不能单独证明缓存命中率,也不能直接证明某个 Widget 是 Raster 卡顿的根因。
产生原理
FrameTiming 在一帧完成 Raster 之后由 Flutter Engine 生成。完整关系如下:
FrameTiming 描述的是最近完成 Raster 的帧,不是 UI 阶段结束时立即产生的数据。回调收到它时,对应帧可能早已结束,应用状态也可能已经发生变化。
回调机制
使用完整 Flutter Framework 时,应通过 SchedulerBinding.addTimingsCallback() 注册监听。它在底层使用 PlatformDispatcher.onReportTimings,但允许多个库各自注册回调。
引擎会批量报告数据,以降低 Release 模式下的监控开销:
- Release 模式大约每秒报告一次。
- Debug 和 Profile 模式大约每 100 ms 报告一次。
- 第一帧的 Timing 会立即报告,不参与首次批处理。
- 每批
List<FrameTiming>按时间从早到晚排列。 - 即使后续没有新帧,已完成帧的数据也会在对应批处理周期内送达。
回调不是“每完成一帧就同步调用一次”。代码必须遍历列表,也不能用收到回调时的当前页面状态直接代表慢帧现场。
基础用法
下面的观察器展示注册、读取和移除回调的完整过程:
dart
import 'dart:ui';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
class FrameTimingObserver {
late final TimingsCallback _callback = _onTimings;
bool _started = false;
void start() {
if (_started) {
return;
}
_started = true;
SchedulerBinding.instance.addTimingsCallback(_callback);
}
void stop() {
if (!_started) {
return;
}
SchedulerBinding.instance.removeTimingsCallback(_callback);
_started = false;
}
void _onTimings(List<FrameTiming> timings) {
for (final timing in timings) {
print(
'frame=${timing.frameNumber}, '
'build=${timing.buildDuration.inMicroseconds}us, '
'raster=${timing.rasterDuration.inMicroseconds}us, '
'overhead=${timing.vsyncOverhead.inMicroseconds}us, '
'total=${timing.totalSpan.inMicroseconds}us',
);
}
}
}监听器可以在 runApp() 前启动,以接收第一帧数据:
dart
final frameTimingObserver = FrameTimingObserver();
void main() {
WidgetsFlutterBinding.ensureInitialized();
frameTimingObserver.start();
runApp(const MyApp());
}移除监听时必须传入先前注册的同一个回调对象。重复添加同一回调会执行多次,因此观察器需要避免重复注册。
示例中的 print() 只适合学习和临时验证。正式采集不应在回调里大量输出日志、编码 JSON 或发起网络请求,具体实现见 FrameTiming 数据采集与分析。
底层接口
不使用 Flutter Framework 时,可以直接设置 PlatformDispatcher.onReportTimings:
dart
PlatformDispatcher.instance.onReportTimings = (
List<FrameTiming> timings,
) {
// 处理最近完成 Raster 的帧。
};这个属性只能保存一个回调,新赋值会覆盖旧值。普通 Flutter 应用应优先使用 SchedulerBinding.addTimingsCallback(),避免不同库相互覆盖监听器。
使用模式
FrameTiming 在 Debug、Profile 和 Release 模式都可以获得,但 Debug 模式包含断言和调试开销,数据不能代表正式版本性能:
- 本地分析使用真机 Profile 模式。
- 线上监控使用 Release 模式。
- Debug 模式只适合验证回调和字段读取是否正确。
对于目标帧率为 X FPS 的场景,一个目标周期约为 1000 / X ms。buildDuration 或 rasterDuration 超过该预算,说明对应阶段可能无法维持目标帧率;totalSpan 超过预算表示这帧没有达到最低延迟目标。这个预算是性能目标,不是 FrameTiming 携带的逐帧系统 Deadline;动态刷新率下的判定边界见数据采集文章。
能力边界
FrameTiming 能提供阶段级时间数据,但不能单独还原完整用户体验:
- 它只报告已经完成 Raster 的 Flutter 帧。
- 它不能覆盖请求目标帧之前发生的所有 Dart 长任务。
rasterFinish不等于画面已经被系统合成器显示。- 它不包含 Widget、RenderObject、路由、交互名称或调用栈。
- Cache 字段只提供规模快照,不提供具体缓存对象和命中归因。
- 它没有公开的 View 标识,多窗口应用不能只靠对象本身可靠关联显示设备。
- 它不能完整描述 Platform View、原生页面、GPU 驱动和系统显示链路。
因此,FrameTiming 适合定位问题发生在哪个帧阶段。要回答“哪个页面、哪个交互、哪段代码导致问题”,还需要上下文埋点、Timeline、CPU Profiler 和平台侧工具。
