帧率与抖动的基本概念:FPS、Jank、Frame Time、掉帧阈值
做性能优化这么多年,我见过太多新手一上来就盯着FPS看数字。嗯,30fps稳了就觉得没问题?其实这里面坑挺多的。今天咱们就把这几个基础概念掰扯清楚:FPS、Jank、Frame Time、还有到底掉多少帧才算“掉帧”。
核心一句话:帧率是结果,Frame Time才是原因。Jank是“不连贯”的主观感受,掉帧阈值是客观的量化标准。
1. FPS:最熟悉的陌生人
FPS(Frame Per Second),每秒传输帧数。说白了就是一秒钟内屏幕刷新了多少张画面。
- 60fps —— 主流流畅基准,每帧16.67ms
- 120fps —— 高刷屏常见,每帧8.33ms
- 30fps —— 勉强可用,但滑动已经能感到卡顿
但我要提醒你:FPS是平均值,它掩盖了抖动。你想想看,假如一秒钟内前15帧每帧2ms,后45帧每帧30ms,平均FPS还是60,实际体验却卡得要死。我在项目中遇到过好几次,测试报告写着“平均59.8fps”,用户却投诉掉帧。后来我就养成了习惯——先看Frame Time分布,再看FPS。
避坑指南:千万不要只看FPS平均值。我曾经被58fps的假象骗过,结果单帧耗时飙到42ms,Jank率100%。Systrace里一定要打开Frame Timeline视图。
2. Jank:主观与客观的差距
Jank这个词,你可以在很多地方看到定义。Google官方给过一个公式:Jank = 当前帧耗时 > 相邻两帧平均耗时的 2 倍。但我个人觉得,定义归定义,实际优化时更关注的是用户能不能感知到卡顿。
举个例子:你从A点滑到B点,动画持续了500ms,按理说应该生成30帧(60fps)。但中间有几帧耗时特别长,导致画面跳了一下。用户不会说“这里发生了一次Jank”,而是直接说“卡”。
所以我们在Perfetto里经常这么看:
- 打开
Choreographer#doFrame切片 - 统计单帧耗时超过16.67ms的次数
- 再按“连续掉帧数”分组——掉1帧不算大问题,连续掉3帧以上用户就能明显感知
个人经验:我通常把“连续掉帧≥3帧”定义为一次严重Jank事件。例如连续3帧都超过33ms,那用户看到的画面就会“冻住”100ms左右,非常难受。
3. Frame Time:真正的数据真相
Frame Time就是每一帧从开始渲染到提交显示的耗时。它比FPS敏感得多,直接暴露性能瓶颈。
| 场景 | FPS | Frame Time分布 | 体验 |
|---|---|---|---|
| 理想状态 | 60 | 所有帧 ≤ 16.67ms | 丝滑 |
| 轻微波动 | 55~60 | 大部分帧 ≤ 16.67ms,少数帧 20~30ms | 偶尔微卡 |
| 严重抖动 | 40~50 | 频繁出现 30~50ms 帧 | 明显掉帧 |
| 卡死状态 | <30 | 大量帧超过 50ms | 无法使用 |
在Systrace里怎么看Frame Time?找到SurfaceFlinger进程,展开onFrameAvailable或doFrame,每个蓝色条就是一次帧绘制。条越长,耗时越大。我习惯用Perfetto的Chrome Trace Viewer模式,按M键标记,然后看Statistics面板,直接得到最小、最大、平均、中位数的Frame Time。
4. 掉帧阈值:该以什么标准卡线?
掉帧阈值,就是超过多少毫秒算掉帧。不同刷新率对应的硬阈值:
- 60Hz:阈值 16.67ms,超过即算掉1帧
- 90Hz:阈值 11.11ms
- 120Hz:阈值 8.33ms
- 144Hz:阈值 6.94ms
不过,实战中我往往会使用更严格的内部标准。比如60Hz屏幕,超过14ms我就会拉警报。为什么?因为你还要给vsync的延迟留余量。我曾经在一个项目中,Frame Time中位数只有12ms,但最大帧达到了28ms,导致Jank率超过5%。最后定位到是GPU的一个submit命令卡了。
注意:掉帧阈值千万不能一概而论。游戏场景和UI滑动场景要分开。游戏里掉帧可能因为GPU负载,UI里掉帧通常是因为主线程做了耗时操作。我在做Launcher优化时,把阈值设到16ms内,但如果是视频播放场景,允许单帧20ms以内,因为人眼对视频卡顿的容忍度更高。
5. FPS与Frame Time的关系公式
很多人以为FPS=1000/Frame Time。其实不对。
FPS = 1000 / (总帧数 / 总耗时),也就是平均每帧耗时。但帧率抖动的核心在于帧与帧之间的耗时方差。
举个例子:
- 场景A:10帧耗时分别为 [16,16,16,16,16,16,16,16,16,16] → 平均16ms,FPS=62.5,无抖动
- 场景B:10帧耗时分别为 [2,2,2,2,2,2,2,2,2,74] → 平均也是16ms,FPS=62.5,但实际上有1帧卡了74ms,用户感觉很明显
你想想看,FPS完全一样,体验天差地别。这就是为什么我反复强调:别只看FPS,一定要打开Perfetto的帧耗时分布图。
// 如果要从Perfetto命令行导出帧耗时数据:
adb shell perfetto --txt -c /data/local/tmp/perfetto.config --out /data/local/tmp/trace.perfetto
# 然后打开 trace_processor,执行 SQL 查询
SELECT ts, dur FROM slice WHERE name = 'Choreographer#doFrame' ORDER BY ts
这个SQL会返回每一帧的开始时间戳和耗时(dur)。你拿到数据后可以手动算一下:有多少帧的dur超过了16.67ms?连续超过的帧数最大值是多少?这些都是评估Jank严重性的关键指标。
6. 知识体系图:帧率与抖动的核心概念
下面这张SVG图把FPS、Jank、Frame Time、掉帧阈值这几个概念串了起来。你从“用户体验”出发,分别落到“主观”和“客观”两个维度,最终所有问题都归结到Frame Time这条主线上。
7. 实战总结:三个必须记住的数字
记住这三个阈值:
- 16.67ms — 60Hz的生死线,超过即掉帧
- 33ms — 连续两帧超时,用户开始皱眉
- 50ms — 连续三帧超时,用户肯定要骂娘了
我自己在优化时,把“最大帧耗时 < 20ms”作为目标,哪怕FPS降到58,只要没有突发大块帧,用户反馈反而更好。因为人眼对“平均慢”的容忍度高于“突然一跳”。你下次调性能的时候可以试试这个思路。
好了,帧率与抖动的基本概念就聊到这。记住:FPS是个参考,Jank是个感觉,Frame Time才是真凭实据。下回你在Systrace里看到一堆蓝条,别慌,按咱们讲的办法一个个拆开看,哪里超了16.67ms,哪里连续超了,一目了然。