2. 帧率与刷新率:理解 Android 渲染的基石
2.1 帧率(FPS)—— 应用渲染的“速度表”
帧率,全称 Frames Per Second,也就是每秒渲染的帧数。它衡量的是你的 App 生成画面的速度。60 FPS 意味着应用每秒钟向屏幕输送 60 帧画面,平均每帧耗时 16.67ms。
我刚开始做性能优化时,以为帧率越高越好。其实不完全是。关键不在于跑多高,而在于 稳不稳。你想想看,如果帧率在 60 和 30 之间反复横跳,体验比稳定在 45 帧还要糟糕。我在项目中遇到过这样一个案例:一款资讯 App 列表滑动时,帧率从 58 瞬间跌到 18,用户反馈“像在泥里滑动”。你看,用户感知到的不是平均帧率,而是 掉帧带来的顿挫感。
- 常见帧率档位:24 FPS(电影)、30 FPS(入门流畅)、60 FPS(标准流畅)、90/120 FPS(高刷流畅)。
- Android 上的计量方式:通过 Choreographer 的 FrameCallback 可以拿到每帧的时间戳,计算差值就能得到实时帧率。
2.2 屏幕刷新率(Hz)—— 硬件的“显示节拍”
屏幕刷新率是硬件参数,表示屏幕每秒能刷新多少次画面。60Hz 就是每秒刷新 60 次,每 16.67ms 刷新一次。它与帧率最大的区别在于:帧率是 App 能画多快,刷新率是屏幕能显多快。两码事。
现在主流设备已经跑到 90Hz、120Hz 甚至 144Hz。我个人习惯在测试前先用 adb 看一眼设备的刷新率:
# 获取屏幕刷新率(单位 Hz)
adb shell dumpsys display | grep "mBaseDisplayInfo"
# 或者直接看
adb shell settings get system peak_refresh_rate
2.3 帧率与刷新率的关系 —— 合拍才能流畅
帧率和刷新率必须“合拍”,体验才顺滑。我总结了三种典型情况:
| 帧率(FPS) | 刷新率(Hz) | 用户感知 | 原因 |
|---|---|---|---|
| 60 | 60 | ✅ 流畅 | 帧率与刷新率完全同步,每一帧都能被及时显示。 |
| 30 | 60 | ⚠️ 掉帧/卡顿 | 刷新率是帧率的 2 倍,屏幕有一半的刷新周期在显示重复画面,形成视觉停顿。 |
| 90 | 60 | ❌ 画面撕裂 | 帧率超过刷新率,VSync 失效时,屏幕会在一次刷新内显示两帧的不同部分,出现撕裂。 |
| 120 | 120 | ✅ 极流畅 | 高帧率与高刷新率匹配,每一帧几乎无延迟呈现。 |
说白了,帧率低于刷新率,浪费了硬件的显示能力;帧率高于刷新率,又超出了硬件的处理范围。只有两者匹配,才不浪费、不卡顿。我曾经在一个视频播放项目里,发现播放 24 FPS 的内容时,屏幕用 60Hz 刷新,每 2.5 个刷新周期才换一帧,结果画面总有一种微弱的“顿挫感”。后来用 VSync 锁帧才解决。
2.4 垂直同步(VSync)—— 让“画”和“显”步调一致
为什么需要 VSync?没有它,帧率和刷新率就是两条并行的车道,各跑各的。当 App 在屏幕刚刷新到一半时提交了新帧,屏幕会在一帧内同时显示新旧两帧的画面——这就是 撕裂。
VSync 的作用,说白了就是给 App 和硬件之间加了一个“交通信号灯”。每次屏幕开始刷新前,会发出一个 VSync 脉冲信号,告诉 App:“嘿,你可以开始画下一帧了。” App 必须在下一个 VSync 到来之前画完。如果没画完,那就只能等下一个 VSync,中间这个刷新周期就被跳过了——这就是 掉帧。
在 Android 里,Choreographer 就是监听 VSync 信号的管家。它收到 VSync 后,依次执行 Input(事件处理)、Animation(动画计算)、Traversal(布局&绘制),最后提交给渲染管线。我见过不少开发者直接在 UI 线程做耗时操作,结果错过了 VSync 窗口期,导致连续掉帧。嗯,这就是为什么我们常说要避免在主线程做重活。
2.5 掉帧的直观表现 —— 你的眼睛不会骗你
掉帧的视觉特征非常明显:滑动列表时画面像“粘住”了一下,然后突然跳过去;动画播放到中途顿一顿再继续。专业点说,就是 帧间隔时间不均匀。正常 60 FPS 下,每帧间隔 16.67ms。如果某帧耗时 33ms,那就意味着中间丢了一帧,人眼就能感知到卡顿。
那怎么量化掉帧?我个人习惯用 Choreographer 的 FrameCallback 来统计:
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
private long lastFrameTimeNanos = 0;
@Override
public void doFrame(long frameTimeNanos) {
if (lastFrameTimeNanos == 0) {
lastFrameTimeNanos = frameTimeNanos;
}
long deltaMs = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000;
if (deltaMs > 17) {
int droppedFrames = (int)(deltaMs / 16.67) - 1;
Log.w("JankDetector", "掉帧 " + droppedFrames + " 帧,耗时 " + deltaMs + "ms");
}
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
});
这段代码能精准抓到每一次掉帧。我在内部工具链里把它做成了一个可配置的监控组件,线上线下一开就能看到每个页面的掉帧率。你会发现,很多平时觉得“还行”的页面,一开监控其实掉帧严重。
• 帧率(FPS)是应用的“绘制速度”,刷新率(Hz)是硬件的“显式速度”。
• VSync 是协调两者的信号灯,Choreographer 是它的 Android 实现。
• 掉帧 = App 没在 VSync 窗口内完成绘制,屏幕被迫重复上一帧。
• 帧间隔 > 16.67ms 就是掉帧,掉的帧数 = 向下取整(帧间隔 / 16.67) - 1。