12. 帧率控制与VSync:动态帧率调整、VSync对齐、减少帧丢失

帧率控制这件事,说白了就是「让相机干活别太累,也别偷懒」。我见过不少项目,一上来就把帧率拉到满,结果功耗飙上去,画面却没变流畅多少。你想想看,用户看的是画面效果,不是帧率数字。

VSync 对齐就更关键了。我早期调一个预览卡顿的问题,折腾了两周,最后发现是帧率和屏幕刷新率没对齐,白白丢了一半的帧。嗯,这坑我踩过,今天咱们就把它填平。

12.1 动态帧率调整:别让相机一直跑满速

相机预览的帧率,真的需要一直 30fps 吗?不一定。场景暗的时候,每帧曝光时间都长,你硬要 30fps,结果就是每帧都欠曝,噪点还多。我建议的做法是:根据场景亮度动态调整目标帧率

核心思路:帧率 = min(传感器能力, 应用需求, 场景限制)

具体怎么调?我一般分三步走:

  1. 亮度检测:从每一帧的 AE 统计信息里拿到当前 lux 值。
  2. 帧率映射:亮度低 -> 降帧率(比如 15fps),亮度高 -> 恢复满帧率。
  3. 平滑切换:不要一下子从 30 跳到 15,中间加个渐变,避免用户感知到卡顿。
// 伪代码示例:动态帧率调整逻辑
int getTargetFps(float lux) {
    if (lux > 200.0f) {
        return 30;  // 光线充足,满帧跑
    } else if (lux > 50.0f) {
        return 24;  // 室内光线,降一点
    } else if (lux > 10.0f) {
        return 15;  // 暗光,降帧保画质
    } else {
        return 10;  // 极暗,能看就行
    }
}

我的经验:动态帧率调整一定要和 AE 算法配合好。我曾经遇到一个 case,帧率降下去了,AE 却还在按 30fps 算曝光时间,结果画面忽明忽暗。后来我把帧率信息同步给 AE 模块,问题就解决了。

12.2 VSync 对齐:让每一帧都踩在点上

VSync 是什么?简单说就是屏幕刷新的「节拍器」。屏幕每 16.6ms(60Hz)刷新一次,你的相机帧就得在这个时间窗口内准备好。如果没对齐,就会出现「帧准备好了,屏幕刚刷过去」的情况——这一帧就丢了。

我习惯把 VSync 对齐分成两个层面来看:

  • 应用层对齐:Camera 应用的 Surface 要注册到 Choreographer 的 VSync 回调上。
  • HAL 层对齐:Sensor 的帧同步信号要和 Display 的 VSync 做硬件级对齐。

应用层对齐的代码大概长这样:

// 注册 Choreographer 回调,实现 VSync 对齐
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
    @Override
    public void doFrame(long frameTimeNanos) {
        // frameTimeNanos 就是下一次 VSync 的时间点
        // 在这里提交新的预览帧到 Surface
        previewSurface.queueBuffer(buffer, ...);
        
        // 继续注册下一帧回调
        Choreographer.getInstance().postFrameCallback(this);
    }
});

注意:Choreographer 的回调是在 UI 线程执行的。如果你的帧处理逻辑太重,会卡住 UI。我建议把帧处理放到单独的 HandlerThread 里,回调只做「提交」操作。

12.3 减少帧丢失:从源头堵住漏洞

帧丢失的原因很多,我总结下来,最常见的有三个:

原因 表现 解决方案
Buffer 不足 Surface 队列满了,新帧被丢弃 增加 Buffer 数量,或降低帧率
处理超时 ISP/HAL 处理一帧超过 33ms 优化算法,或降帧率
VSync 失配 帧准备好时屏幕刚刷完 做 VSync 对齐,或使用 Triple Buffer

我曾经在一个项目里,预览帧率标称 30fps,实际测出来只有 22fps。查了半天,发现是 HAL 层用了 4 个 Buffer,但应用层只消费了 3 个,第 4 个 Buffer 永远在排队。嗯,这种「Buffer 死锁」问题,说白了就是生产者和消费者没协调好。

减少帧丢失的 checklist:

  • ✅ 确认 Buffer 数量 >= 3(推荐 4 个)
  • ✅ 确认每一帧的处理时间 < 帧间隔(30fps 就是 33ms)
  • ✅ 确认 VSync 信号已对齐(用 systrace 看 SurfaceFlinger 的 vsync-app)
  • ✅ 确认没有 Buffer 泄漏(用 dumpsy SurfaceFlinger 检查)

12.4 知识体系图:帧率控制与 VSync 的核心逻辑

帧率控制与 VSync 核心逻辑 场景亮度 / 用户需求 动态帧率决策 亮度低→降帧率 | 亮度高→满帧率 VSync 对齐 Choreographer 回调 + HAL 同步 减少帧丢失 Buffer 管理 + 处理时间优化 稳定帧率 + 低功耗 核心目标:在画质和功耗之间找到最佳平衡点

12.5 实战避坑指南

最后,分享几个我踩过的坑:

  • 不要盲目用 Triple Buffer:Triple Buffer 能减少 VSync 失配,但会增加内存和延迟。我建议只在 60fps 高帧率场景下启用。
  • 注意帧率切换的平滑性:从 30fps 降到 15fps,中间最好过渡一两秒。我曾经直接硬切,结果用户反馈「画面一顿一顿的」。
  • 用 systrace 看 VSync 对齐:在 systrace 里找到 SurfaceFlinger 的 vsync-app 和相机线程的 dequeueBuffer,看它们的时间戳是否对齐。这是最直接的验证方法。

我的小工具:我写了一个简单的帧率监控脚本,每 5 秒统计一次实际帧率,如果低于目标帧率的 80%,就自动 dump 一次 systrace。这样能快速定位帧丢失的原因。

帧率控制这件事,说到底就是「该快的时候快,该慢的时候慢」。VSync 对齐是基本功,减少帧丢失是细节活。把这两件事做好了,功耗和体验就都稳了。


公众号:蓝海资料掘金营,微信deep3321