likes
comments
collection
share

Flutter 帧率(FPS)实时监控

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

Flutter 帧率(FPS)实时监控

背景

Debug模式,我们可以很容易使用Flutter 提供的DevTools实时知道页面的FPS, 不过如果我们需要做线上帧率监控,就需要使用代码来处理了,Flutter提供了addTimingsCallback回调,来得到帧调度序列,通过这个调度序列的原始数据,来更加精确和及时获取FPS,就是本文的重点。

当然也有其他博主提供了别的思路,不过我对比下来,一来精确度不太高,二来感觉做了不少没必要的计算。

需求理解的知识点

如何监听

Flutter 中通过如下方式监听帧率

WidgetsBinding.instance.addTimingsCallback((timings) { });

  结合下面的官方文档注释,我们来说说上面回调函数的参数timingstimings的类型是List<FrameTiming>

Flutter 帧率(FPS)实时监控

  1. 引擎层触发这个回调有两个必要条件
  • 并不是实时的,必须是用页面或者说屏幕发送了变化,或者说用户点击了页面;
  • 性能考虑,做了防抖处理,debug模式100ms; release模式1s;
  1. List<FrameTiming> 则表示一系列实时帧信息。
  • List<FrameTiming> 中0的位置是第一帧,last 是最新一帧。 最新的帧永远在最后面

再来说说FrameTiming类

Flutter 帧率(FPS)实时监控

里面的这几个属性,我用下面的图,大家应该一看就明白了。

Flutter 帧率(FPS)实时监控

  • ④-① = totalSpan:同步信号开始到栅格化时间
  • ②-① = vsyncOverhead:同步信号接受后到 ui 构建之间延迟。
  • ③-② = buildDuration:ui 构建过程总时间。
  • ④-③ = rasterDuration:栅格化过程总时间。

核心实现逻辑

基本信息了解完成,下面到我们的重点了,如何计算

  1. Vsync时间间隔

我们先假设设备的帧率是60;我们得到_frameInterval,大概是16.66ms,表明正常情况下,屏幕刷新时间间隔,就是上图中两个垂直同步信号Vsync之间的时间间隔。

目前有部分安卓和iOS手机帧率为90或者120,逻辑一样的。

final _frameInterval =
    const Duration(microseconds: Duration.microsecondsPerSecond ~/ 60);
  1. 定义一个丢帧数组frameMapGlobal

默认长度600,每项默认赋值0;这个其实可以理解为一个二维数组.。

丢帧表: 数组索引表示丢了几帧,索引对应的值表示发生的次数

至于长度为什么定义为600;600*16.6ms ≈ 10s。是一个比较大的数了,考虑一个页面正不至于卡10s。

frameMapGlobal = List<int>.filled(600, 0, growable: false);
  1. 处理回调

下面代码中的skippedFrameCount为每帧的绘制消耗总时长整除正常一帧的时长。如果等于0,说明没有丢帧,等于1说明该帧绘制横跨两个垂直信号,那下一帧的就丢失了,来不及绘制。

把这些数据都记录到丢帧表中。

void onTimings(List<FrameTiming> timings) {
  for(FrameTiming timing in timings){
    final int skippedFrameCount =
        timing.totalSpan.inMilliseconds ~/ _frameInterval.inMilliseconds;
    if (skippedFrameCount < frameMapGlobal.length) {
      frameMapGlobal[skippedFrameCount]++;
    }
  }
}

可能还是有点难理解,我把调试信息打了出来,如下图。

我们发现index = 0的值为1499;说明这个1499帧的绘制很完美,没有丢帧,都是值16.6ms内完成了。

index = 1 的值为16; 说明该页面有16帧的绘制超过了16.6ms但是小于16.6ms*2。这就会导致丢帧。

Flutter 帧率(FPS)实时监控

4. 看看如何计算FPS

公式如下:

FPS=60 ∗ 实际绘制帧数 / (实际绘制帧数+跳帧数)FPS = 60 * 实际绘制帧数 / (实际绘制帧数+跳帧数)FPS=60  实际绘制帧数 / (实际绘制帧数+跳帧数)

跳帧数量就是帧数表中的索引大于0中的值 * 索引。

num get uiFps {
  // 跳帧数量
  int skippedCount = 0;
  // 实际绘制帧数
  int drawnCount = 0;

  for (int i = 0; i < frameMapGlobal.length; i++) {
    drawnCount += frameMapGlobal[i];
    skippedCount += i * frameMapGlobal[i];
  }
  final int totalCount = drawnCount + skippedCount;
  if (totalCount > 0) {
    return 60 * drawnCount / totalCount;
  }
  return 60;
}

测试数据

Flutter 帧率(FPS)实时监控

  1. 可能遇到一些问题的解决思路

好了,到这基本上完成了FPS的检测的核心实现逻辑。但是还有几个问题

  1. 目前假定设备刷新屏幕频率是60,这个正常需要通过Flutter Plugin向原生获取。方法很多,不表。
  2. 上面的丢帧表frameMapGlobal,是一个全局的,如果在应用打开就监听,最终的FPS就是项目运行期间的平均FPS

如果想监听某一个时段的FPS,那就每次addTimingsCallback回调new一个丢帧表;

如果想监听某一个页面的平均FPS,那实现也简单,可以在navigatorObservers里面监听页面路由,比如进入页面开始start()监听,退出页面停止stop()监听并返回页面的丢帧表。

源码如下

源码不多,重点是思路。就直接贴出来了。

import 'dart:ui';
import 'package:flutter/material.dart';

class XHFpsMonitor {
  static const int _fpsHz = 60;
  final _frameInterval =
      const Duration(microseconds: Duration.microsecondsPerSecond ~/ _fpsHz);

  static late List<int> frameMapGlobal;

  XHFpsMonitor() {
    frameMapGlobal = List<int>.filled(600, 0, growable: false);
  }

  void start() {
    WidgetsBinding.instance.addTimingsCallback(onTimings);
  }

  void stop() {
    WidgetsBinding.instance.removeTimingsCallback(onTimings);
  }

  void onTimings(List<FrameTiming> timings) {
    for (FrameTiming timing in timings) {
      final int skippedFrameCount =
          timing.totalSpan.inMilliseconds ~/ _frameInterval.inMilliseconds;
      if (skippedFrameCount < frameMapGlobal.length) {
        frameMapGlobal[skippedFrameCount]++;
      }
    }
    print("fps: ${uiFps.toStringAsFixed(0)}");
  }

  num get uiFps {
    // 跳帧数量
    int skippedCount = 0;
    // 实际绘制帧数
    int drawnCount = 0;

    for (int i = 0; i < frameMapGlobal.length; i++) {
      drawnCount += frameMapGlobal[i];
      skippedCount += i * frameMapGlobal[i];
    }
    final int totalCount = drawnCount + skippedCount;
    if (totalCount > 0) {
      return _fpsHz * drawnCount / totalCount;
    }
    return _fpsHz;
  }
}