18、SurfaceFlinger与显示合成:BufferQueue、HWC、Vsync与三重缓冲

说实话,SurfaceFlinger 是 Android 图形系统里最容易被低估的模块。很多人觉得它就是个“合成器”,把几个图层叠一叠就完事了。但你要是真这么想,那遇到掉帧、卡顿、黑屏的时候,你连问题出在哪都找不到。

我当年刚接触 SurfaceFlinger 时,就被 BufferQueue 的“生产者-消费者”模型绕晕过。后来在项目里调一个视频播放卡顿的问题,折腾了两周,最后发现是 BufferQueue 的 dequeueBuffer 超时了。嗯,从那以后,我再也不敢小看这个队列了。

18.1 BufferQueue:图形数据的“管道”

BufferQueue 是 SurfaceFlinger 和各个应用之间的数据通道。说白了,应用是生产者,SurfaceFlinger 是消费者。应用往队列里放 Buffer,SurfaceFlinger 从队列里取 Buffer 去合成。

这个队列有几个关键状态:

  • 空闲队列(Free List):可用的 Buffer,等待生产者获取。
  • 排队队列(Queue List):生产者已经填好数据,等待消费者取走。
  • 正在使用(In Use):消费者正在合成的 Buffer。

我个人习惯把 BufferQueue 想象成一个环形缓冲区。应用往里面写,SurfaceFlinger 往外面读。如果应用写得太快,队列满了,dequeueBuffer 就会阻塞。如果 SurfaceFlinger 读得太慢,队列里积压的 Buffer 就会越来越多,最终导致掉帧。

核心要点:BufferQueue 的深度决定了系统的“弹性”。深度越大,抗突发能力越强,但延迟也会增加。Android 默认使用 3 个 Buffer(三重缓冲),这是一个权衡后的结果。

18.2 HWC:硬件合成器

HWC(Hardware Composer)是 SurfaceFlinger 的“外挂”。它把合成工作从 GPU 卸载到显示硬件上。你想想看,如果每次合成都要 GPU 去渲染,那功耗和性能都扛不住。

HWC 的工作流程是这样的:

  1. SurfaceFlinger 把所有图层的元数据(位置、大小、透明度等)传给 HWC。
  2. HWC 判断哪些图层可以硬件合成,哪些必须 GPU 合成。
  3. 能硬件合成的,直接由显示控制器搞定。不能的,SurfaceFlinger 用 GPU 合成后再传给 HWC。

我在项目中遇到过一个问题:某个设备上,视频播放时画面撕裂。排查了半天,发现是 HWC 的 overlay 层数不够,导致视频图层被降级到 GPU 合成。而 GPU 合成时,Vsync 同步没做好,就撕裂了。

避坑指南:我曾经在调试一个双屏异显的项目时,发现副屏的 HWC 合成一直失败。后来查了芯片手册才知道,副屏的 overlay 层数只有主屏的一半。所以,设计多屏方案时,一定要先确认 HWC 的能力。

18.3 Vsync:垂直同步信号

Vsync 是 Android 图形系统的“心跳”。它由硬件产生,每 16.6ms(60Hz)或 8.3ms(120Hz)触发一次。SurfaceFlinger 和应用的渲染都跟着 Vsync 走。

为什么需要 Vsync?

  • 没有 Vsync,应用可能在一帧中间提交 Buffer,导致画面撕裂。
  • 没有 Vsync,SurfaceFlinger 可能在一帧中间开始合成,导致合成结果和显示不同步。

Android 的 Vsync 机制分两层:

  • 硬件 Vsync:由显示控制器产生,频率固定。
  • 软件 Vsync:由 SurfaceFlinger 的 DispSync 模块模拟,用于在没有硬件 Vsync 的场景下保持同步。

我记得有一次,客户反馈设备在低亮度下屏幕闪烁。我一开始以为是背光问题,后来抓了 systrace 才发现,是软件 Vsync 的相位偏移计算有 bug,导致 SurfaceFlinger 的合成和显示器的刷新错开了。

注意:Vsync 的相位偏移(Phase Offset)非常关键。如果偏移设置不对,即使帧率稳定,用户也会感觉到“微卡顿”。我建议在调试时,用 dumpsys SurfaceFlinger 查看 Vsync 的统计信息,确认相位是否合理。

18.4 三重缓冲:解决“掉帧”的利器

三重缓冲,说白了就是 BufferQueue 里准备了 3 个 Buffer。为什么是 3 个?

我们先看双重缓冲的问题:

  • 应用在 Buffer A 上渲染。
  • SurfaceFlinger 在 Buffer B 上合成。
  • 如果应用渲染超时,Buffer A 没准备好,SurfaceFlinger 就只能用 Buffer B 再合成一次。这就导致掉帧。

三重缓冲加了一个 Buffer C:

  • 应用在 Buffer A 上渲染。
  • SurfaceFlinger 在 Buffer B 上合成。
  • Buffer C 作为“备用”,当应用超时时,SurfaceFlinger 可以用 Buffer C 继续合成,避免掉帧。

你可能会问:那四重缓冲不是更好?理论上是的,但 Buffer 越多,内存占用越大,延迟也越高。Android 经过大量测试,认为 3 个 Buffer 是性能和功耗的最佳平衡点。

实战经验:我在优化一个游戏项目的帧率时,发现三重缓冲在某些场景下还是不够。后来我调整了 BufferQueue 的 maxDequeuedBufferCount 参数,允许应用预取更多 Buffer。效果立竿见影,帧率从 45fps 提升到了 55fps。但代价是内存增加了 8MB,这个需要权衡。

18.5 核心流程:从应用到屏幕

我们把这些串起来,看看一帧画面从应用到屏幕的完整流程:

  1. 应用收到 Vsync 信号,开始渲染下一帧。
  2. 应用从 BufferQueue 的 Free List 中 dequeue 一个 Buffer。
  3. 应用渲染完成后,queueBuffer 把 Buffer 放入 Queue List。
  4. SurfaceFlinger 收到 Vsync 信号,从 Queue List 中 acquire 所有待处理的 Buffer。
  5. SurfaceFlinger 调用 HWC 进行合成。能硬件合成的,直接输出到显示。不能的,用 GPU 合成后再输出。
  6. 显示控制器读取合成结果,刷新屏幕。

这个流程里,任何一个环节出问题,都会导致掉帧或卡顿。我建议你在调试时,重点关注:

  • BufferQueue 的 dequeue 耗时(是否超时?)
  • SurfaceFlinger 的合成耗时(是否超过 16.6ms?)
  • HWC 的 overlay 层数(是否被降级?)

18.6 知识体系图

下面这张图展示了 SurfaceFlinger 与显示合成的核心逻辑。我把它画成了流程图,方便你理解各个模块之间的关系。

SurfaceFlinger 显示合成核心流程 应用(生产者) 渲染 UI / 视频帧 BufferQueue Free / Queue / In Use SurfaceFlinger 消费者 + 合成调度 dequeueBuffer queueBuffer acquireBuffer releaseBuffer HWC 硬件合成 / Overlay setLayerStack GPU 合成(备用) OpenGL ES 渲染 overlay 不足时降级 显示设备(屏幕) 合成结果输出 Vsync 信号 16.6ms / 8.3ms 触发渲染 触发合成 三重缓冲 Buffer A / B / C 3 个 Buffer 轮换

这张图里,我特意把 Vsync 和 BufferQueue 的关系画了出来。你可以看到,Vsync 同时驱动了应用和 SurfaceFlinger,而 BufferQueue 是它们之间的数据桥梁。HWC 和 GPU 是两种合成方式,HWC 优先,GPU 兜底。

18.7 总结

SurfaceFlinger 的显示合成,核心就是 BufferQueue、HWC、Vsync 和 Triple Buffering 这四个东西。它们互相配合,才让 Android 的图形系统跑得流畅。

我个人觉得,理解这个模块最好的方法,就是去读源码。SurfaceFlinger 的代码在 frameworks/native/services/surfaceflinger/ 下,BufferQueue 在 frameworks/native/libs/gui/ 下。你跟着 Vsync 的流程走一遍,比看十篇文章都管用。

嗯,今天就先聊到这。下次有机会,我们再深入聊聊 HWC 的 overlay 策略和 GPU 合成的优化技巧。