帧率与抖动的基本概念: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这条主线上。

帧率与抖动核心概念图谱 用户体验 (流畅/卡顿) 主观感受:Jank 客观指标:FPS 连续掉帧 ≥3 感知阈值 平均值欺骗 方差导致抖动 Frame Time 分布 单帧耗时 中位数/最大 掉帧阈值 vsync边界 优化核心:降低 Frame Time 方差 & 确保所有帧不超掉帧阈值

7. 实战总结:三个必须记住的数字

记住这三个阈值:

  • 16.67ms — 60Hz的生死线,超过即掉帧
  • 33ms — 连续两帧超时,用户开始皱眉
  • 50ms — 连续三帧超时,用户肯定要骂娘了

我自己在优化时,把“最大帧耗时 < 20ms”作为目标,哪怕FPS降到58,只要没有突发大块帧,用户反馈反而更好。因为人眼对“平均慢”的容忍度高于“突然一跳”。你下次调性能的时候可以试试这个思路。

好了,帧率与抖动的基本概念就聊到这。记住:FPS是个参考,Jank是个感觉,Frame Time才是真凭实据。下回你在Systrace里看到一堆蓝条,别慌,按咱们讲的办法一个个拆开看,哪里超了16.67ms,哪里连续超了,一目了然。