众所周知,官方提供了好几个办法来让我们在开发 Flutter app 的过程中可以使用查看 fps等性能数据,如devtools ,具体见文档 Debugging Flutter apps 、Flutter performance profiling 等。
但是这些工具统计到的数据充其量只能算开发过程中的“试验室”数据,假如需要统计app 在线上在用户手机上的运行情况,该如何在 flutter 端代码里自己计算性能数据,比如 fps
这个值呢?
经过阅读源码,发现其实很简单,给window
对象注册 onReportTimings
即可,去看api文档 。
1 2 3 4 5 6 7 8 void main() { runApp(...); window .onReportTimings = _onReportTimings; } void _onReportTimings(List <FrameTiming> timings) { }
注意 :当升级到Flutter 1.12.x 之后,onReportTimings
应该改成SchedulerBinding
的addTimingsCallback
1 2 3 4 5 6 7 8 9 10 11 void start() { SchedulerBinding.instance.addTimingsCallback(_onReportTimings); } void stop() { SchedulerBinding.instance.removeTimingsCallback(_onReportTimings); } void _onReportTimings(List <FrameTiming> timings) { }
代码给你看 考虑计算fps
,只需要保留最近 N
个FrameTiming
来计算即可,最好用类似stack的数据结构存起来,参考了文档,我们选用 Queue ,N 指定为 100
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const maxframes = 100 ; final lastFrames = ListQueue<FrameTiming>(maxframes);void _onReportTimings(List <FrameTiming> timings) { for (FrameTiming timing in timings) { lastFrames.addFirst(timing); } while (lastFrames.length > maxframes) { lastFrames.removeLast(); } }
1 2 3 4 5 6 7 8 9 +---------------------------------------------------------------+ + + +----------+ +-----+ +---------+ CPU | | | | | | +----------+ +-----+ ... +---------+ +-------+ +-------+ +--------+ GPU | | | | | | +-------+ +-------+ +--------+
lastFrames
的头就是最后一帧,尾是队伍里最开始的一帧,现在你可以计算 FPS 了:
1 2 3 4 5 6 7 8 double get fps { int frames = lastFrames.length; var start = lastFrames.last.timestampInMicroseconds(FramePhase.buildStart); var end = lastFrames.first.timestampInMicroseconds(FramePhase.rasterFinish); var duration = (end - start) / Duration .microsecondsPerMillisecond; return frames * Duration .millisecondsPerSecond / duration; }
但,你会发现,这样算出来和官方工具算的对不上,而且错的离谱。
why ??
代码再给你看一下 其实,window.onReportTimings
只会在有帧被绘制时才有数据回调,换句话说,你没有和app发生交互、界面状态没有变化(setState)、没有定时刷布局(动画)等等没有新的帧产生,所以lastFrames
里存的可能是分属不同“绘制时间段”的帧信息。
1 2 3 4 5 6 7 8 9 10 11 12 +------------------------------------------------------------------------------------------------------+ + + +----------+ +-----+ +---------+ +----------+ +---------+ CPU | | | | | | | | | | +----------+ +-----+ +---------+ +----------+ +---------+ +-------+ +-------+ +--------+ +-------+ +--------+ GPU | | | | | | | | | | +-------+ +-------+ +--------+ +-------+ +--------+ + + + + +-----------------frameset1---------------------------+ +---------------frameset2-------------+
假设 一秒最多绘制 60 帧,每帧消耗的时间 frameInterval
为:
1 2 const REFRESH_RATE = 60 ;const frameInterval = const Duration (microseconds: Duration .microsecondsPerSecond ~/ REFRESH_RATE);
flutter 引擎应该就是根据该frameInterval
按计划绘制每一帧,即如果一帧绘制时间少于 frameInterval
则下一帧会等待一点点时间,而当一帧绘制时间超过frameInterval
则下一帧会因为“原计划时间”被占用而紧接着执行绘制(我没看源码猜的,如果跟你认知的不一样,可以评论一下)。
可以认为 lastFrames
里的相邻两帧如果开始结束相差时间过大,比如大于 frameInterval * 2
,是不同绘制时间段产生的。那么我们采取最后一个时间段来计算 FPS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 double get fps { var lastFramesSet = <FrameTiming>[]; for (FrameTiming timing in lastFrames) { if (lastFramesSet.isEmpty) { lastFramesSet.add(timing); } else { var lastStart = lastFramesSet.last.timestampInMicroseconds(FramePhase.buildStart); if (lastStart - timing.timestampInMicroseconds(FramePhase.rasterFinish) > ( frameInterval.inMicroseconds * 2 )) { break ; } lastFramesSet.add(timing); } } var frameCount = lastFramesSet.length; var duration = ( lastFramesSet.first.timestampInMicroseconds(FramePhase.rasterFinish) - lastFramesSet.last.timestampInMicroseconds(FramePhase.buildStart) ) ~/ Duration .microsecondsPerMillisecond; return frameCount * Duration .millisecondsPerSecond / duration; }
此时又会发现,算出来的 FPS 有时会超过 60。
why ??
代码给你看,真的 因为上面的 duration
其实算的不对。
前面提到,flutter 是按 frameInterval
计划绘制的,所以 duration
算出来偏小,进而导致计算结果超过 60。
换个思路,遵循之前的按计划绘制的逻辑,如果一帧绘制时长超过frameInterval
,那么它必然会占用下一个帧的绘制计划,这个被挤占的帧即为丢帧
,显然,一秒内总绘制的帧数加上总丢掉帧数应该约等于 60。
所以,FPS ≈ REFRESH_RATE * framesCount / (framesCount + droppedCount) = REFRESH_RATE * framesCount / costCount
修改之前的代码,套入公式即为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 double get fps { var lastFramesSet = <FrameTiming>[]; for (FrameTiming timing in lastFrames) { if (lastFramesSet.isEmpty) { lastFramesSet.add(timing); } else { var lastStart = lastFramesSet.last.timestampInMicroseconds(FramePhase.buildStart); if (lastStart - timing.timestampInMicroseconds(FramePhase.rasterFinish) > ( _frameInterval.inMicroseconds * 2 )) { break ; } lastFramesSet.add(timing); } } var framesCount = lastFramesSet.length; var costCount = lastFramesSet.map((t) { return (t.totalSpan.inMicroseconds ~/ frameInterval.inMicroseconds) + 1 ; }).fold(0 , (a, b)=> a + b); return framesCount * REFRESH_RATE / costCount; }
好了,现在你会发现,算出来和Flutter Performance
中看到的几乎一模一样:
但是,不要忘了前提是你的手机屏幕刷新率 REFRESH_RATE
恰好是我一开始假设的 60,那刷新率不是 60 怎么办?就留给你自己思考了<(▰˘◡˘▰)>
还有几行关键代码 对于 Android 通过 WindowManager 获取刷新率:
1 2 WindowManager windowManager = (WindowManager) applicationContext.getSystemService(Context.WINDOW_SERVICE)) float fps = windowManager.getDefaultDisplay().getRefreshRate();
对于iOS 从 CADisplayLink 获取刷新率:
1 2 3 4 auto preferredFPS = displayLink.preferredFPS; if (preferredFPS == 0 ) { float fps = [UIScreen mainScreen].maximumFramesPerSecond; }
本文完。
ps. 完整代码见gist