简介
性能数据要能回答“哪一帧、哪个阶段、哪类内存”出了问题。Flutter 的 SchedulerBinding.addTimingsCallback 接收引擎报告的 FrameTiming,适合观察 build、raster 和 total span;递归注册 addPostFrameCallback 统计回调次数既不能可靠反映丢帧,也容易遗漏停止和销毁逻辑。
1. 采集帧耗时
以下监控器暴露帧耗时流,并在停止时注销回调。FPS 不是它的输出;如需按时间窗口计算 FPS,应先明确统计窗口和刷新率语义。
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
class FrameSample {
const FrameSample({
required this.build,
required this.raster,
required this.total,
});
final Duration build;
final Duration raster;
final Duration total;
factory FrameSample.fromTiming(FrameTiming timing) {
return FrameSample(
build: timing.buildDuration,
raster: timing.rasterDuration,
total: timing.totalSpan,
);
}
}
class PerformanceMonitor {
final _frames = StreamController<FrameSample>.broadcast();
Timer? _memoryTimer;
bool _started = false;
bool _disposed = false;
Stream<FrameSample> get frames => _frames.stream;
void start({
Duration memoryInterval = const Duration(seconds: 5),
}) {
if (_disposed) {
throw StateError('PerformanceMonitor is disposed');
}
if (_started) return;
SchedulerBinding.instance.addTimingsCallback(_onTimings);
if (kDebugMode) {
_memoryTimer = Timer.periodic(memoryInterval, _logRss);
}
_started = true;
}
void _onTimings(List<FrameTiming> timings) {
for (final timing in timings) {
_frames.add(FrameSample.fromTiming(timing));
}
}
void _logRss(Timer timer) {
if (kDebugMode) {
final rssMb = ProcessInfo.currentRss / (1024 * 1024);
debugPrint('Process RSS: ${rssMb.toStringAsFixed(1)} MB');
}
}
void stop() {
if (!_started) return;
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
_memoryTimer?.cancel();
_memoryTimer = null;
_started = false;
}
Future<void> dispose() async {
if (_disposed) return;
stop();
_disposed = true;
await _frames.close();
}
}
ProcessInfo.currentRss 来自 dart:io,仅适用于支持该 API 的原生运行环境,不适用于 Flutter Web。RSS 是进程级观测值,不等同于 Dart heap,也不能单独证明发生泄漏。
在拥有该监控器的 State 中启动和释放:
late final PerformanceMonitor _monitor;
@override
void initState() {
super.initState();
_monitor = PerformanceMonitor()..start();
}
@override
void dispose() {
unawaited(_monitor.dispose());
super.dispose();
}
addTimingsCallback 在 debug/profile 下通常按批次报告帧数据,在 release 下批次间隔更长;采样频率和采集开销应纳入测试条件。使用 FrameTiming.buildDuration、rasterDuration 及 totalSpan 找到可疑阶段后,再用 DevTools Performance 视图检查具体调用。
2. 观察 RSS 与内存对象
上面的 Timer 由 PerformanceMonitor 持有,在 stop() / dispose() 时取消。不要丢弃 Timer.periodic 的返回值,否则无法停止周期任务。
进程 RSS 包含 Dart 和原生等多类内存,不能把 RSS 曲线等同于 Dart heap。出现持续增长时,使用 DevTools Memory 视图比较交互前后的 Diff Snapshots,查看实例数量和 retaining path;曲线短时间没有回落本身不足以断定泄漏。
3. 测量页面阶段
页面耗时需要先定义起止事件。例如,记录一次用户操作的异步阶段:
final stopwatch = Stopwatch()..start();
await loadPageData();
stopwatch.stop();
debugPrint('loadPageData: ${stopwatch.elapsedMilliseconds}ms');
这段时间只覆盖 loadPageData,不代表完整首帧时间。不要将网络耗时、widget build 和 raster 时长混成一个指标。
4. 工具与采集边界
- 用 DevTools Performance 视图关联 UI / Raster 线程耗时和具体帧。
- 用 Flutter Inspector 检查布局、重绘和过大图片。
- 在 profile 模式及目标设备测量;debug 模式的开销不代表用户体验。
- release 采集应控制采样频率、数据量和隐私,避免每帧写日志或同步上报。
- FrameTiming 提供帧耗时,不是通用的业务性能 SDK;上报前定义聚合窗口、设备信息和采样率。
实战记录
本文原稿没有留下可复现的设备、Flutter 版本、采样代码 commit 和基线数据,因此这里不填造性能数字。实际项目记录可包含:设备型号、系统版本、Flutter commit、构建模式、刷新率、场景步骤、build/raster 分位数和采样时长。
