16、SurfaceFlinger 与显示合成:BufferQueue 机制、HWC(Hardware Composer)、显示帧率控制

各位同学,今天我们来聊聊 Android 显示链路中最核心的一环——SurfaceFlinger。说实话,我做了这么多年多媒体开发,每次调试显示相关的问题,最后十有八九都会回到 SurfaceFlinger 这里。它就像整个显示系统的交通指挥中心,所有 App 要显示的内容,都得经过它来调度和合成。

16.1 BufferQueue:生产与消费的桥梁

我们先从 BufferQueue 说起。这个机制,说白了就是一套「生产者-消费者」模型。App 是生产者,SurfaceFlinger 是消费者,中间通过 BufferQueue 来传递图形缓冲区。

核心要点:BufferQueue 解决了「谁生产、谁消费、什么时候生产、什么时候消费」的问题。

我个人习惯把 BufferQueue 想象成一个环形缓冲区池。App 从池子里取一个 Buffer 来画图,画完了就丢回池子,SurfaceFlinger 再从池子里取走拿去合成显示。嗯,这里要注意,BufferQueue 默认会分配 3 个 Buffer,也就是我们常说的 triple buffering。

为什么会是 3 个?我刚开始做这行时也困惑过。后来在项目中遇到过一次掉帧问题,才真正理解了。如果只有 2 个 Buffer,App 在画下一帧时,上一帧可能还没被 SurfaceFlinger 消费完,App 就得干等着——这就是掉帧的根源。3 个 Buffer 给了系统一个缓冲余地。

// BufferQueue 的核心接口(简化版)
class BufferQueue {
    // App 调用:请求一个 Buffer 来画图
    status_t dequeueBuffer(uint32_t* outSlot, sp<Fence>* outFence);
    
    // App 调用:画完了,把 Buffer 还给队列
    status_t queueBuffer(int slot, const QueueBufferInput& input);
    
    // SurfaceFlinger 调用:获取下一个待显示的 Buffer
    status_t acquireBuffer(BufferItem* outBuffer);
    
    // SurfaceFlinger 调用:显示完了,释放 Buffer
    status_t releaseBuffer(int slot, const sp<Fence>& releaseFence);
};

你看这个流程,App 和 SurfaceFlinger 之间通过 dequeue/queue 和 acquire/release 两对操作来协作。我曾经在优化一个视频播放器的显示延迟时,就是通过调整 BufferQueue 的深度和预取策略,把延迟从 3 帧降到了 1 帧。

避坑指南:我曾经遇到过一个问题——App 端频繁 dequeueBuffer 但 queueBuffer 很慢,导致 BufferQueue 被占满,SurfaceFlinger 拿不到新 Buffer,画面就卡住了。后来发现是 App 的渲染线程被其他任务阻塞了。解决办法是让渲染线程保持高优先级,并且不要做耗时操作。

16.2 HWC:硬件合成器

接下来是 HWC,也就是 Hardware Composer。这个模块是 Android 显示系统的「硬件加速器」。SurfaceFlinger 拿到多个 Layer 的 Buffer 后,需要把它们合成一帧完整的画面。这个合成操作,可以在 GPU 里做,也可以交给 HWC 来做。

你想想看,如果每个 Layer 都用 GPU 合成,那 GPU 的负载会非常大,功耗也高。HWC 的作用就是:能交给硬件做的,就交给硬件做。比如你的手机屏幕显示了一个状态栏、一个桌面壁纸、一个悬浮窗,这三个 Layer 如果互不重叠,HWC 可以直接让显示控制器把它们「叠」在一起,根本不需要 GPU 参与。

关键概念:HWC 的合成策略分为三种:

  • Device Composition(设备合成):全部由 HWC 硬件完成,效率最高
  • Client Composition(客户端合成):全部由 SurfaceFlinger 用 GPU 完成
  • Mixed Composition(混合合成):部分 Layer 由 HWC 处理,部分由 GPU 处理

我记得在做一个双屏显示项目时,副屏的显示一直有撕裂问题。排查了很久,最后发现是 HWC 对副屏的支持不完整,某些 Layer 被错误地分配到了 Device Composition,但硬件其实不支持。解决办法是强制把这些 Layer 走 Client Composition,虽然功耗高了一点,但画面稳定了。

// HWC 的合成决策流程(伪代码)
void SurfaceFlinger::doComposition() {
    // 1. 收集所有可见 Layer
    Vector<Layer> layers = collectVisibleLayers();
    
    // 2. 询问 HWC 哪些 Layer 可以硬件合成
    HWC::setLayerCompositionType(layers);
    
    // 3. 对于 HWC 不处理的 Layer,用 GPU 合成
    for (auto& layer : layers) {
        if (layer.compositionType == CLIENT) {
            renderLayerWithGPU(layer);
        }
    }
    
    // 4. 把最终结果提交给 HWC 显示
    HWC::presentDisplay(layers);
}

注意事项:HWC 的版本差异很大。HWC 1.x 和 HWC 2.x 的接口完全不同。我建议你在做底层开发时,先确认目标设备的 HWC 版本。HWC 2.x 引入了更灵活的合成策略,但也带来了更多的兼容性问题。

16.3 显示帧率控制

最后我们聊聊帧率控制。Android 的显示帧率并不是「App 画多快,屏幕就刷多快」这么简单。中间有 Vsync 信号在协调。

Vsync 是什么?说白了就是一个硬件中断信号,告诉系统「屏幕刷新完了,可以准备下一帧了」。SurfaceFlinger 和 App 的渲染线程都会等待 Vsync 信号,这样就能保证 App 的绘制和屏幕的刷新是同步的。

帧率控制的核心机制:

  • Vsync 信号:由硬件产生,通常是 60Hz 或 120Hz
  • Choreographer:App 端的帧率协调器,等待 Vsync 后触发绘制
  • SurfaceFlinger 的合成周期:也是由 Vsync 驱动,每个 Vsync 周期合成一次
  • 帧率限制:可以通过设置帧率上限来节省功耗

我曾经遇到过一个很有意思的问题:某款手机在播放 30fps 的视频时,画面每隔几秒就会卡一下。用 systrace 抓了数据一看,发现 SurfaceFlinger 的合成周期是 60Hz,但视频源只有 30fps。这就导致每两帧视频之间,SurfaceFlinger 会合成一次「空帧」——也就是重复显示上一帧。按理说这不会卡,但问题出在 BufferQueue 的 acquire 操作上,空帧也会触发一次 Buffer 的 acquire/release 流程,增加了不必要的开销。

解决办法是什么?我让视频播放器在 queueBuffer 时带上正确的帧时间戳,SurfaceFlinger 根据这个时间戳来判断是否需要跳过某些合成周期。嗯,这个优化做完后,卡顿就消失了。

// 帧率控制的配置示例
// 在 SurfaceFlinger 的配置文件中设置帧率上限
<!-- /vendor/etc/surfaceflinger.rc -->
service surfaceflinger /system/bin/surfaceflinger
    class core
    user system
    group graphics drmrpc
    onrestart restart zygote
    // 设置最大帧率为 60fps
    setprop debug.sf.max_frames 60

个人经验:如果你在做游戏或者视频播放器,建议主动设置帧率。比如播放 24fps 的视频时,可以调用 Surface.setFrameRate() 告诉系统「我只需要 24fps」。这样 SurfaceFlinger 就不会以 60fps 的频率去轮询你的 Buffer,能省不少电。我做过测试,在 60Hz 屏幕上播放 24fps 视频,主动设置帧率后,功耗降低了约 15%。

16.4 知识体系总览

为了让你更直观地理解这三者的关系,我画了一张图:

SurfaceFlinger 显示合成核心机制 App(生产者) dequeueBuffer / queueBuffer BufferQueue 3 Buffer 环形池 SurfaceFlinger acquireBuffer / releaseBuffer HWC(硬件合成器) Device / Client / Mixed 显示设备(屏幕) 60Hz / 120Hz Vsync Vsync 信号 帧率控制 Choreographer / 帧率限制 生产者 缓冲区 合成器 硬件加速 帧率控制

从这张图你可以看到,整个显示链路是环环相扣的。App 生产 Buffer,BufferQueue 中转,SurfaceFlinger 合成,HWC 加速,最后送到屏幕显示。而 Vsync 信号就像节拍器,让所有环节都踩在同一个节奏上。

好了,关于 SurfaceFlinger 和显示合成的内容就讲到这里。这些机制看起来复杂,但只要你理解了「生产者-消费者」这个核心模型,再结合 Vsync 的同步机制,就能把整个显示链路串起来。下次遇到掉帧、撕裂、卡顿这些问题,你就知道该从哪里入手了。

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