跳转到正文
YGJoon 技术博客

Flutter 性能监控:FrameTiming、RSS 与 DevTools 实战

发布于:
·936 字 · 约 2 分钟·作者:
使用 FrameTiming 观察 build、raster 与总帧耗时,配合可取消的内存采样和 DevTools 定位 Flutter 性能问题。
Flutter 性能监控:FrameTiming、RSS 与 DevTools 实战文章封面
目录

简介

性能数据要能回答“哪一帧、哪个阶段、哪类内存”出了问题。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. 工具与采集边界

实战记录

本文原稿没有留下可复现的设备、Flutter 版本、采样代码 commit 和基线数据,因此这里不填造性能数字。实际项目记录可包含:设备型号、系统版本、Flutter commit、构建模式、刷新率、场景步骤、build/raster 分位数和采样时长。

参考资料


分享这篇文章:


相关文章

上一篇
Flutter 冷启动优化:哪些初始化不该堵在首帧前
下一篇
Flutter 渲染优化详解