第4章:帧率与流畅度:FPS、Jank、帧生成时间分析,60fps vs 120fps策略

各位好,我是老张。今天咱们聊聊座舱图形渲染里最核心也最容易被误解的话题——帧率与流畅度。

很多刚入行的同学觉得,帧率越高就越流畅。嗯,这话对了一半。我见过不少项目,明明跑到了60fps,用户却抱怨卡顿。为什么?因为帧率只是表象,真正的罪魁祸首往往藏在帧生成时间里。

4.1 别被FPS骗了:三个核心指标

我个人习惯,看流畅度从来不看单一指标。你得盯住三个东西:

  • FPS(每秒帧数):最直观,但最不靠谱。60fps意味着每帧16.67ms,但如果有几帧花了50ms,FPS依然能显示60,因为它是平均值。
  • Jank(卡顿次数):这才是用户感知到的「卡」。我定义Jank为:帧生成时间超过目标帧间隔的1.5倍。比如60fps目标下,任何一帧超过25ms就算一次Jank。
  • 帧生成时间(Frame Time):这个最真实。它记录了每一帧从开始到结束的实际耗时。我建议你们用这个指标做性能分析,而不是FPS。

核心观点:FPS是平均值,帧生成时间是瞬时值。用户感知的是瞬时卡顿,不是平均流畅。

4.2 帧生成时间分析:找到真正的瓶颈

我在MTK8676上调试过一个导航地图的滑动卡顿问题。当时FPS显示58,看起来还行。但用户一滑动地图就感觉「粘手」。我抓了帧生成时间曲线,发现每隔10帧左右就有一个峰值,从16ms飙到40ms。

为什么会这样?

后来定位到是地图瓦片加载时触发了CPU的同步IO操作。GPU其实早就画完了,但CPU被IO卡住,导致整帧延迟。你看,如果只看FPS,你永远找不到这个问题。

这里我分享一个实用工具:SurfaceFlinger的dump信息。在MTK平台上,你可以通过adb命令抓取每一帧的提交时间和完成时间:

adb shell dumpsys gfxinfo <package_name> framestats

输出里你会看到类似这样的数据:

Flags,IntendedVsync,Vsync,OldestInputEvent,NewestInputEvent,...
0,1234567890,1234567890,0,0,1234567895,1234567900,...

重点关注FrameCompletedDequeueBufferDuration这两列。前者告诉你帧真正完成的时间,后者告诉你Buffer分配是否卡顿。

我的小技巧:在MTK8676上,我习惯用perfetto抓取整个渲染管道的trace。重点关注「Choreographer」和「SurfaceFlinger」这两个进程的线程状态。如果Choreographer的doFrame回调超过16ms,那说明应用主线程有阻塞。

4.3 60fps vs 120fps:不是简单的数字翻倍

很多产品经理喜欢说「我们要上120fps,体验更好」。但作为工程师,你得算一笔账。

指标 60fps 120fps
帧间隔 16.67ms 8.33ms
GPU渲染负载 基准 约2倍
CPU调度压力 高(每8ms要提交一次)
功耗 基准 增加40%-60%
Jank容忍度 25ms以上算Jank 12.5ms以上算Jank

你看,120fps的帧间隔只有8.33ms。这意味着你的GPU必须在8ms内完成一帧的渲染。对于座舱里的复杂场景——比如3D地图+仪表盘+倒车影像同时渲染——这几乎不可能稳定做到。

我曾经在MTK8676上测试过一个3D仪表盘项目。60fps模式下,GPU负载约65%,偶尔有Jank。强行开启120fps后,GPU负载直接飙到95%,而且因为帧间隔太短,每次垂直同步都赶不上,实际显示效果反而更差——因为掉帧更频繁了。

避坑指南:我曾经犯过一个错误——在120fps模式下用了三重缓冲。结果因为缓冲区太多,帧延迟反而增加了。后来我改成双缓冲+提前一帧提交,才把延迟降下来。记住:120fps下,每一毫秒都很珍贵,别让缓冲机制吃掉你的时间预算。

4.4 动态帧率策略:该省省该花花

那是不是120fps就没用了?当然不是。关键是要动态切换

我建议的策略是这样的:

  1. 静态场景(停车、待机):用30fps甚至更低。省电,而且用户不会操作,看不出区别。
  2. 轻度交互(点击菜单、滑动列表):切到60fps。保证基础流畅度。
  3. 重度交互(3D地图旋转、动画过渡):上120fps。这时候用户手指在动,对流畅度最敏感。
  4. 视频播放:匹配视频源帧率。24fps的电影别硬拉到120fps,反而会有judder(抖动)。

在MTK8676上,我通过监听InputDispatcher的触摸事件来触发帧率切换。检测到触摸事件后,立即把帧率提升到120fps,同时降低GPU的渲染精度(比如关闭MSAA)。等用户停止操作500ms后,再逐步降回60fps。

// 伪代码示例
void onTouchEvent() {
    if (currentFps != 120) {
        setTargetFps(120);
        setGpuQuality(GPU_QUALITY_LOW); // 关闭抗锯齿等
    }
    lastTouchTime = now();
}

void onIdleCheck() {
    if (now() - lastTouchTime > 500ms && currentFps == 120) {
        setTargetFps(60);
        setGpuQuality(GPU_QUALITY_HIGH);
    }
}

经验之谈:动态切换帧率时,要注意帧率变化的平滑性。别从120fps直接跳到30fps,用户会感觉到「突然变慢」。我习惯用渐变策略:120→90→60→30,每步停留200ms左右。这样用户几乎感知不到切换过程。

4.5 总结:流畅度的本质

说了这么多,其实就一句话:流畅度不是看平均帧率,而是看帧生成时间的稳定性

60fps下,如果你的帧生成时间稳定在15-17ms之间,用户会觉得非常流畅。120fps下,如果帧生成时间在8-15ms之间剧烈波动,用户反而会觉得「一顿一顿」的。

所以我的建议是:

  • 先保证60fps的稳定性,再考虑上120fps
  • 用帧生成时间曲线做性能分析,别只看FPS
  • 动态帧率策略比固定高帧率更实用
  • 别忘了功耗——座舱里电池是有限的

下一章我会讲GPU渲染管线的优化,包括如何减少Draw Call、如何用好硬件合成器。到时候咱们再细聊。