区长

Flutter 如何更加准确地获取 FPS

如果我们需要对比Flutter与Native的性能数据,那么我们就需要获取Flutter的一部分性能数据,FPS就是其中的一个衡量标准。

至于Flutter的FPS的计算方式,可以参考这篇文章,里面讲的比较细以及为什么这么计算。

总体思路就是设置window.onReportTimings回调,获取每帧的数据,但这里需要注意一下,v1.9.1可以通过设置window.onReportTimings实现,如

1
2
3
4
5
6
var orginalCallback = window.onReportTimings;

window.onReportTimings = (timings) {
if (orginalCallback != null) orginalCallback(timings);
// ...
}

不过在v1.12.13上,你不能再使用这方式,你得改成如下方式

1
2
3
4
5
import 'package:flutter/scheduler.dart';

SchedulerBinding.instance.addTimingsCallback((List<FrameTiming> timings) {
//...
});

设置完回调后过滤掉无效的帧数据后根据计算公式获得对应的FPS。对应的公式如下:

FPS / 60 ≈ drawFramesCount / (drawFramesCount + droppedCount)

根据公式推导出:

FPS ≈ 60 * drawFramesCount / (drawFramesCount + droppedCount)

假设过滤后的有效的帧数据为变量framesSet,那么

  • drawFramesCount则是我们绘制的帧数,这个可以直接通过framesSet.length获取
  • droppedCount 则是丢帧数,可以通过判断绘制的时间是否大于每帧绘制时间获取
  • 每帧绘制的时间可以用1000ms/刷新频率获取,即1000/60 ≈ 16ms

假如我们将drawFramesCount + droppedCount赋值为costCount,那么 FPS 就可以通过如下代码获取得到:

1
2
3
4
5
6
7
8
const frameInterval = const Duration(microseconds: Duration.microsecondsPerSecond ~/ 60);

var framesCount = framesSet.length;
var costCount = framesSet.map((t) {
// 耗时超过 frameInterval 会导致丢帧
return (t.totalSpan.inMicroseconds ~/ frameInterval.inMicroseconds) + 1;
}).fold(0, (a, b)=> a + b);
double fps = framesCount * 60 / costCount;

这里看起来很完美,其实有个致命的问题,那就是真的每个手机的每秒绘制的最大帧数是60帧吗,也就是16ms绘制一帧,显然不是的。可以参考下如下文章

可以看到,部分手机肯定不是60Hz的刷新频率,可能存在90Hz,如OnePlus 7 Pro,甚至出现了120Hz,如Redmi K30,而Flutter的目标就是在60Hz的手机上达到每秒60帧的绘制,在120Hz的手机上达到每秒120帧的绘制。如果我们上述公式写死用60进行计算,那么在这些手机上计算出来的FPS就会偏小。

所以为了让上述计算公式更准确,我们需要获取到手机屏幕的刷新频率。

通过查看Flutter Engine的源码,我们会发现Vsync的刷新频率是通过Platform的API获取到的

首先来看下Android是如何获取的

engine/src/flutter/shell/platform/android/vsync_waiter_android.cc

1
2
3
4
5
6
7
8
9
10
11
12
float VsyncWaiterAndroid::GetDisplayRefreshRate() const {
JNIEnv* env = fml::jni::AttachCurrentThread();
if (g_vsync_waiter_class == nullptr) {
return kUnknownRefreshRateFPS;
}
jclass clazz = g_vsync_waiter_class->obj();
if (clazz == nullptr) {
return kUnknownRefreshRateFPS;
}
jfieldID fid = env->GetStaticFieldID(clazz, "refreshRateFPS", "F");
return env->GetStaticFloatField(clazz, fid);
}

通过JNI调用Java类FlutterJNI的一个静态字段refreshRateFPS获取,而该字段是在VsyncWaiter类中获取到的,其代码路径为
engine/src/flutter/shell/platform/android/io/flutter/view/VsyncWaiter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class VsyncWaiter {
private static VsyncWaiter instance;

@NonNull
public static VsyncWaiter getInstance(@NonNull WindowManager windowManager) {
if (instance == null) {
instance = new VsyncWaiter(windowManager);
}
return instance;
}

@NonNull
private final WindowManager windowManager;

private final FlutterJNI.AsyncWaitForVsyncDelegate asyncWaitForVsyncDelegate = new FlutterJNI.AsyncWaitForVsyncDelegate() {
@Override
public void asyncWaitForVsync(long cookie) {
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
float fps = windowManager.getDefaultDisplay().getRefreshRate();
long refreshPeriodNanos = (long) (1000000000.0 / fps);
FlutterJNI.nativeOnVsync(frameTimeNanos, frameTimeNanos + refreshPeriodNanos, cookie);
}
});
}
};

private VsyncWaiter(@NonNull WindowManager windowManager) {
this.windowManager = windowManager;
}

public void init() {
FlutterJNI.setAsyncWaitForVsyncDelegate(asyncWaitForVsyncDelegate);

// TODO(mattcarroll): look into moving FPS reporting to a plugin
float fps = windowManager.getDefaultDisplay().getRefreshRate();
FlutterJNI.setRefreshRateFPS(fps);
}
}

从代码看出Android的屏幕刷新频率可以通过WindowManager的API获取到

1
2
3
4
5
6
7
8
public double getRefreshRate(Context context) {
try {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
return windowManager.getDefaultDisplay().getRefreshRate();
} catch (Exception e) {
}
return 60.0;
}

我们再来看看iOS如何获取到屏幕刷新频率,其关键代码位于
engine/src/flutter/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (float)displayRefreshRate {
if (@available(iOS 10.3, *)) {
auto preferredFPS = display_link_.get().preferredFramesPerSecond; // iOS 10.0

// From Docs:
// The default value for preferredFramesPerSecond is 0. When this value is 0, the preferred
// frame rate is equal to the maximum refresh rate of the display, as indicated by the
// maximumFramesPerSecond property.

if (preferredFPS != 0) {
return preferredFPS;
}

return [UIScreen mainScreen].maximumFramesPerSecond; // iOS 10.3
} else {
return 60.0;
}
}

也就是说可以通过CADisplayLink的displayLinkWithTarget函数获取到,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (double)displayRefreshRate:(CADisplayLink *)link {
if (@available(iOS 10.3, *)) {
NSInteger preferredFPS = link.preferredFramesPerSecond; // iOS 10.0

// From Docs:
// The default value for preferredFramesPerSecond is 0. When this value is 0, the preferred
// frame rate is equal to the maximum refresh rate of the display, as indicated by the
// maximumFramesPerSecond property.

if (preferredFPS != 0) {
return @(preferredFPS).doubleValue;
}

return @([UIScreen mainScreen].maximumFramesPerSecond).doubleValue; // iOS 10.3
} else {
return 60.0;
}
}

- (void)onDisplayLink:(CADisplayLink *)link {
NSLog(@"preferredFramesPerSecond:%lf", [self displayRefreshRate:link]);
}

- (double)getRefreshRate:(NSDictionary *)arguments {
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(onDisplayLink:)];
return [self displayRefreshRate:link];
}

最后,通过channel让dart层可以调用如上函数获取到对应的屏幕刷新频率即可

fps.dart

1
2
3
4
5
static const MethodChannel _channel = MethodChannel('fps_plugin');

static Future<double> getRefreshRate() async {
return _channel.invokeMethod("getRefreshRate");
}

对应Android和iOS实现channel,调用上面的函数获取refreshRate即可

最终FPS的计算公式就会变成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
double _refreshRate;
Duration _frameInterval;

if (_refreshRate == null) {
_refreshRate = (await getRefreshRate()) ?? 60;
}
if (_frameInterval == null) {
_frameInterval = Duration(
microseconds:
Duration.microsecondsPerSecond ~/ _refreshRate); //每帧消耗的时间,单位微秒
}

var framesCount = framesSet.length;
var costCount = framesSet.map((t) {
// 耗时超过 _frameInterval 会导致丢帧
return (t.totalSpan.inMicroseconds ~/ _frameInterval.inMicroseconds) + 1;
}).fold(0, (a, b)=> a + b);
double fps = framesCount * _frameInterval / costCount;

改进后的方法将会在屏幕刷新频率为90Hz和120Hz的手机上更加准确。

坚持原创技术分享,您的支持将鼓励我继续创作!
区长 WeChat Pay

微信打赏