帧率抖动基础:帧率、抖动、卡顿的定义与关系

做性能优化,绕不开三个词:帧率、抖动、卡顿。很多新手容易把它们混为一谈,觉得帧率低就是卡顿,卡顿就是因为帧率低。其实不是这样。

我个人习惯先把概念拆清楚。举个例子——一个 App 跑在 60fps,按理说每帧间隔 16.67ms。但你真拿 Perfetto 抓一把 trace 看看,很少有帧能卡得那么准。多半是 14ms、17ms、20ms 来回跳。这种跳,就是抖动。而如果某一帧突然崩到 100ms,那就是卡顿。

所以想用 Perfetto 监控后台调度导致的帧率问题,先得把这几个概念的关系吃透。

帧率(FPS)——每秒显示的帧数

帧率是大部分人最先接触的指标。60fps 意味着渲染线程每秒提交 60 帧,每帧可用时间约 16.67ms。但注意,帧率只是一个平均值,它把那些忽快忽慢的帧间隔全都平均掉了。我曾经见过一个项目,FPS 测出来是 58,看上去挺稳,但实际上每 3 秒就有一个 150ms 的大掉帧——用户感知到的就是卡,但平均帧率根本没反映出来。

这就是帧率的局限:它告诉你“量”,但不告诉你“均匀度”。

帧率每帧间隔典型场景
30 fps33.33 ms低端设备、省电模式
60 fps16.67 ms主流流畅体验
90 fps11.11 ms高刷新率屏幕
120 fps8.33 ms游戏/专业应用
我的习惯:先看平均帧率,但绝不能只看它。我会用 Perfetto 的 FrameTiming 切片抓每一帧的起止时间,然后算变异系数。如果变异系数超过 10%,哪怕帧率是 59,也值得深挖。

抖动(Jitter)——帧间隔的偏差

抖动是个更精细的指标。它描述的是帧间隔时间偏离理想值的程度。比如理想间隔 16.67ms,实际帧间隔序列是 [16, 17, 15, 20, 16] ms,最大偏差 3.33ms,这就是抖动。

抖动有两种常见量化方式:

  • 最大抖动(Peak Jitter): 最大帧间隔减去最小帧间隔。简单直观,但对单次异常敏感。
  • 均方根抖动(RMS Jitter): 计算所有帧间隔与理想值差值的均方根。更能反映整体稳定性。

我在项目中遇到过一个典型案例:某款手机在抖音滑动时,帧间隔总是在 14ms 到 20ms 之间跳,幅度不大但频率很高。用户反馈“说不出的涩”,这就是典型的抖动问题,而不是卡顿。

注意:抖动不是卡顿。抖动是高频小幅波动,卡顿是低频大幅延迟。在 Perfetto 的 SurfaceFlinger 的 onFrameCommitted 下,如果看到连续几个帧间隔突然翻倍,那就要用卡顿去分析了。

卡顿(Stutter)——明显的拖慢

卡顿指的是某一帧或多帧的渲染时间严重超过预期,导致用户明显感知到画面停顿或跳帧。通常,超过下一帧的 vsync 边界就算一次小卡顿,超过两三个 vsync 就算明显卡顿。

不同系统对卡顿的判定标准不同:

  • Android 官方用「掉帧数」衡量(比如超过 16.67ms 的部分累计为掉帧)。
  • iOS 用「帧间隔超过 32ms 的次数」来统计。
  • Perfetto 里可以看 FrameTimeline 里的 expectedVsync 和 actualVsync 的差值,差值越大卡顿越严重。

我曾优化过一个后台调度导致的卡顿:某个线程被调度到小核上,而且时不时被其它进程抢占,导致它处理完一帧需要 40~60ms。每次抢占了主线程的时间,屏幕就冻一下。这种卡顿从帧率上看只掉到 45fps,但用户的感知是“一顿一顿的”。

三者的关系:一图说清

下面这张图概括了帧率、抖动、卡顿的关系。你可以把它当作一个判断框架:

帧率、抖动、卡顿关系图 帧率·抖动·卡顿 关系示意图 理想帧间隔 (如 16.67ms) 偏离 实际帧间隔 (14ms, 17ms, 20ms...) 抖动 Jitter 小幅频变 感知平滑 可接受 卡顿 Stutter 大幅延迟 感知卡顿 明显顿挫 核心判断: 抖动小 → 帧间隔均匀 → 即使帧率稍低也感觉流畅

量化指标实战:用 Perfetto 抓住抖动

在 Perfetto 里,我们可以用几个关键切片来量化:

  • FrameTimeline: 展示每一帧的预期时间与结束时间,直接看差值。
  • SurfaceFlinger::onFrameCommitted: 记录每一帧提交的时间点,可以用来算帧间隔。
  • Choreographer: 在 Android 上可以抓 Choreographer.doFrame 看每一帧的绘制耗时。
// 在 Perfetto SQL 里计算帧间隔抖动
SELECT 
  ts - lag(ts) OVER (ORDER BY ts) AS frame_duration_ns
FROM 
  slice
WHERE 
  name = 'onFrameCommitted'
ORDER BY ts
// 然后看标准差和最大值

我建议你抓一次 trace 后,直接拉出 FrameTimeline 的视图——它会用不同颜色标出哪一帧超时了。黄色是轻微超时,红色是严重超时。一眼就能看出抖动模式和卡顿分布。

一个经验值:当帧间隔的标准差超过 3ms 时,用户就能感知到“不流畅”。当最大帧间隔大于 50ms 时,用户会明确说“卡”。你可以在自己的项目里调一下这个阈值。

常见误区避坑

我曾经踩过的一个坑:某次优化后台调度,我以为只要把平均帧率提到 59 以上就行了。结果用户反馈反而更差。后来一看 trace,发现帧率虽然高了,但抖动从 2ms 飙到了 7ms——后台线程被调度到小核上频繁抢占前台。所以帧率不是万能的,抖动和分布比均值重要得多。

总结三句话:

  • 帧率是整体平均数,评估性能天花板。
  • 抖动是细节偏差,决定流畅细腻度。
  • 卡顿是极端异常,破坏交互连贯性。

下节课我们会具体用 Perfetto 抓一次 trace,看看后台调度怎么导致帧率抖动。这里先打好概念基础——工具是手段,理解定义和关系才是分析的起点。