帧率基础:FPS、帧时间与卡顿概念
讲性能优化,绕不开帧率。很多同学一上来就盯着 FPS 看,觉得只要 60 就万事大吉。说实话,我早期也犯过这个错误。后来踩了几次坑才发现——FPS 只是个表象,真正要揪的是「帧时间」。今天咱们就把最基础的概念掰开揉碎,讲清楚了再往下走。
1. FPS:每秒多少帧?别被数字骗了
FPS(Frames Per Second)就是画面每秒钟刷新的次数。60 FPS 代表一秒内渲染了 60 帧画面,理论上每帧耗时 16.67 毫秒。这个数字越大,用户感知到的流畅度越高。
但有个问题:FPS 是一个「平均指标」。如果一秒内前 500ms 跑了 59 帧,后 500ms 只跑了 1 帧,FPS 算下来还是 60。你想想看,用户眼里的实际体验就是突变的卡顿。所以我个人习惯——不只看 FPS 的平均值,更关注它的「抖动」。
| FPS 数值 | 用户体验 | 常见场景 |
|---|---|---|
| ≥ 60 | 非常流畅 | 静态页面、简单列表滑动 |
| 30 ~ 59 | 可感知掉帧 | 复杂布局、大量图片加载 |
| 15 ~ 29 | 明显卡顿 | GC 频繁、主线程耗时操作 |
| < 15 | 几乎不可用 | 内存抖动、过度绘制严重 |
2. 帧时间:真正该盯的死线
什么叫帧时间?就是一帧从开始绘制到最终显示到屏幕上的耗时。Android 目前主流屏幕刷新率是 60Hz,那么每一帧的生命周期就是 16.67ms。你所有的 UI 工作——布局、测量、绘制、合成——都得在这个时间内完成。超了?画面就跳过去了,下一帧才能补上。
我在项目中遇到过这样的事:一个列表页加载了 20 张高清图,不做任何压缩。Profiler 一看,单帧时间飙到 80ms,FPS 却还有 45 左右。为什么?因为掉帧 3~4 帧之后,数据又被平均上去了。听我的,以后看帧率先把帧时间曲线拉出来。比 FPS 准得多。
// 你可以用 FrameMetrics 来获取精确帧时间
window.addOnFrameMetricsAvailableListener { _, frameMetrics, _ ->
val frameTimeNanos = frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION)
// 转换成毫秒
val frameTimeMs = frameTimeNanos / 1_000_000f
if (frameTimeMs > 16.67f) {
Log.w("FrameTracer", "掉帧!耗时:$frameTimeMs ms")
}
}
3. 卡顿的本质:掉帧与 jank
卡顿就是用户感受到的画面停顿或跳跃。它的根源只有一个:帧时间超出了屏幕刷新周期。比如 60Hz 的屏幕上,某帧处理了 50ms。那这 50ms 里本该有 3 次刷新机会,结果只显示了 1 帧。肉眼就看到了「跳一下」的效果。
Android 社区管这种丢帧叫 jank。Profiler 里会统计 jank 数和 jank 率。我建议你关注这两个指标:
- Jank 次数:在 1 秒内发生掉帧的帧数量。
- Jank 率:jank 帧数 / 总帧数。超过 5% 就需要警惕。
4. 核心关系图:一目了然
下面这张图把 FPS、帧时间和卡顿的关系串起来了。记住这三个核心节点,后面的实战才有方向。
5. 实战中的取舍:你该关注哪个指标?
我的习惯是分层级看:
- 第一层(快速筛查):看 FPS 曲线是否长期低于 50。如果是,直接进入帧时间分析。
- 第二层(精确定位):打开 Profiler 的 Frame Timeline,找到那些红色或黄色的帧(表示超时)。记下它们的帧时间分布。
- 第三层(根因定位):结合 CPU/GPU 曲线,看是主线程阻塞了,还是 GPU 渲染超负荷。后面几章我会专门讲怎么读这些曲线。
好了,基础概念就到这。下次我们用 Profiler 跑一遍真实 App,亲手抓一帧超时的「犯罪现场」。