Flutter 帧率(FPS)实时监控
背景
在Debug
模式,我们可以很容易使用Flutter
提供的DevTools
实时知道页面的FPS
, 不过如果我们需要做线上帧率监控,就需要使用代码来处理了,Flutter
提供了addTimingsCallback
回调,来得到帧调度序列,通过这个调度序列的原始数据,来更加精确和及时获取FPS
,就是本文的重点。
当然也有其他博主提供了别的思路,不过我对比下来,一来精确度不太高,二来感觉做了不少没必要的计算。
需求理解的知识点
如何监听
Flutter 中通过如下方式监听帧率
WidgetsBinding.instance.addTimingsCallback((timings) { });
结合下面的官方文档注释,我们来说说上面回调函数的参数timings
。timings
的类型是List<FrameTiming>
。
- 引擎层触发这个回调有两个必要条件
- 并不是实时的,必须是用页面或者说屏幕发送了变化,或者说用户点击了页面;
- 性能考虑,做了防抖处理,debug模式100ms; release模式1s;
List<FrameTiming>
则表示一系列实时帧信息。
List<FrameTiming>
中0的位置是第一帧,last 是最新一帧。 最新的帧永远在最后面。
再来说说FrameTiming类
里面的这几个属性,我用下面的图,大家应该一看就明白了。
- ④-① =
totalSpan
:同步信号开始到栅格化时间 - ②-① =
vsyncOverhead
:同步信号接受后到 ui 构建之间延迟。 - ③-② =
buildDuration
:ui 构建过程总时间。 - ④-③ =
rasterDuration
:栅格化过程总时间。
核心实现逻辑
基本信息了解完成,下面到我们的重点了,如何计算
-
Vsync时间间隔
我们先假设设备的帧率是60;我们得到_frameInterval
,大概是16.66ms,表明正常情况下,屏幕刷新时间间隔,就是上图中两个垂直同步信号Vsync
之间的时间间隔。
目前有部分安卓和iOS手机帧率为90或者120,逻辑一样的。
final _frameInterval =
const Duration(microseconds: Duration.microsecondsPerSecond ~/ 60);
-
定义一个丢帧数组
frameMapGlobal
表
默认长度600,每项默认赋值0;这个其实可以理解为一个二维数组.。
丢帧表: 数组索引表示丢了几帧,索引对应的值表示发生的次数
至于长度为什么定义为600;600*16.6ms ≈ 10s。是一个比较大的数了,考虑一个页面正不至于卡10s。
frameMapGlobal = List<int>.filled(600, 0, growable: false);
-
处理回调
下面代码中的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。这就会导致丢帧。
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;
}
测试数据
-
可能遇到一些问题的解决思路
好了,到这基本上完成了FPS的检测的核心实现逻辑。但是还有几个问题
- 目前假定设备刷新屏幕频率是60,这个正常需要通过Flutter Plugin向原生获取。方法很多,不表。
- 上面的丢帧表
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;
}
}
转载自:https://juejin.cn/post/7249647793899257916