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
💡 我的习惯:拿到新设备的第一件事,就是查它的刷新率。很多“千元机”虽然标了 120Hz,但实际系统会动态降频到 60Hz,不查你根本不知道。

2.3 帧率与刷新率的关系 —— 合拍才能流畅

帧率和刷新率必须“合拍”,体验才顺滑。我总结了三种典型情况:

帧率(FPS) 刷新率(Hz) 用户感知 原因
60 60 ✅ 流畅 帧率与刷新率完全同步,每一帧都能被及时显示。
30 60 ⚠️ 掉帧/卡顿 刷新率是帧率的 2 倍,屏幕有一半的刷新周期在显示重复画面,形成视觉停顿。
90 60 ❌ 画面撕裂 帧率超过刷新率,VSync 失效时,屏幕会在一次刷新内显示两帧的不同部分,出现撕裂。
120 120 ✅ 极流畅 高帧率与高刷新率匹配,每一帧几乎无延迟呈现。

说白了,帧率低于刷新率,浪费了硬件的显示能力;帧率高于刷新率,又超出了硬件的处理范围。只有两者匹配,才不浪费、不卡顿。我曾经在一个视频播放项目里,发现播放 24 FPS 的内容时,屏幕用 60Hz 刷新,每 2.5 个刷新周期才换一帧,结果画面总有一种微弱的“顿挫感”。后来用 VSync 锁帧才解决。

帧率、刷新率与 VSync 关系图 应用输入层 同步与对比层 结果输出层 应用渲染帧率 (FPS) 屏幕刷新率 (Hz) VSync 同步信号 帧率 vs 刷新率 比较 ✅ 匹配 → 流畅体验 ❌ 不匹配 → 掉帧/撕裂 📌 VSync 信号协调应用帧率与屏幕刷新率:匹配则流畅,不匹配则产生掉帧或画面撕裂。

2.4 垂直同步(VSync)—— 让“画”和“显”步调一致

为什么需要 VSync?没有它,帧率和刷新率就是两条并行的车道,各跑各的。当 App 在屏幕刚刷新到一半时提交了新帧,屏幕会在一帧内同时显示新旧两帧的画面——这就是 撕裂。

VSync 的作用,说白了就是给 App 和硬件之间加了一个“交通信号灯”。每次屏幕开始刷新前,会发出一个 VSync 脉冲信号,告诉 App:“嘿,你可以开始画下一帧了。” App 必须在下一个 VSync 到来之前画完。如果没画完,那就只能等下一个 VSync,中间这个刷新周期就被跳过了——这就是 掉帧。

在 Android 里,Choreographer 就是监听 VSync 信号的管家。它收到 VSync 后,依次执行 Input(事件处理)、Animation(动画计算)、Traversal(布局&绘制),最后提交给渲染管线。我见过不少开发者直接在 UI 线程做耗时操作,结果错过了 VSync 窗口期,导致连续掉帧。嗯,这就是为什么我们常说要避免在主线程做重活。

⚠️ 避坑指南:我曾经在一个自定义 View 里直接 new 了大量对象,结果每次绘制都触发 GC,GC 期间线程暂停,完美错过 VSync。掉帧率直接飙升到 40%。记住:不要在 draw 里做分配。

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。