24、View的绘制与SurfaceFlinger:Surface与SurfaceFlinger的关系、BufferQueue与双缓冲/三缓冲、合成与显示

讲到这里,我们终于要触及Android渲染体系中最核心的环节了——SurfaceFlinger

前面我们聊了View的measure、layout、draw,也聊了CPU怎么把数据丢给GPU。但有个关键问题我一直没展开:这些绘制好的数据,到底是怎么跑到屏幕上的?

嗯,答案就在SurfaceFlinger里。

Surface与SurfaceFlinger:一对“生产者-消费者”

先说说Surface。我个人习惯把Surface理解成一块“画布”。每个Window(比如Activity、Dialog、Toast)都对应一个Surface。你在draw()里画的所有东西,最终都会落到这块画布上。

但Surface本身不负责显示。它只负责把数据准备好,然后交给SurfaceFlinger去合成和显示。

SurfaceFlinger是什么?它是Android系统的一个系统服务,运行在system_server进程里。它的工作说白了就两件事:

  • 接收各个App提交的Surface数据
  • 合成这些数据,然后输出到屏幕

我遇到过不少开发者,以为View绘制完就直接显示到屏幕了。其实不是。View绘制完只是把数据写到了Surface对应的Buffer里,真正“上屏”的操作,是SurfaceFlinger来完成的。

核心关系:App是生产者,SurfaceFlinger是消费者。Surface是它们之间的“数据通道”。

BufferQueue:双缓冲与三缓冲

那Surface和SurfaceFlinger之间怎么传递数据?靠的是BufferQueue

BufferQueue是一个生产者-消费者模型。App端往队列里放Buffer,SurfaceFlinger从队列里取Buffer。听起来简单,但这里有个坑——如果App生产得太快,SurfaceFlinger消费得太慢,怎么办?

你想想看,如果只有一个Buffer,App画完一帧,SurfaceFlinger还没显示完,App又得画下一帧。那画面就会撕裂。所以我们需要双缓冲

双缓冲

双缓冲就是两个Buffer:一个叫Front Buffer(前台缓冲),一个叫Back Buffer(后台缓冲)。

  • SurfaceFlinger显示Front Buffer里的内容
  • App往Back Buffer里绘制
  • 绘制完成后,交换两个Buffer的角色

这样就不会出现撕裂了。但双缓冲有个问题:如果App绘制一帧的时间超过了16.6ms(60fps的刷新间隔),那Front Buffer就一直被占用,App只能等着。这就产生了掉帧

三缓冲

三缓冲就是在双缓冲的基础上再加一个Buffer。当两个Buffer都被占用时,App可以往第三个Buffer里画。这样能缓解掉帧问题。

我记得在Android 4.1之前,系统默认是双缓冲。后来Google引入了Project Butter,才把三缓冲变成默认配置。我在项目中遇到过一些低端机,三缓冲反而导致内存压力大,后来我们改成了动态调整——根据帧率自动切换双缓冲和三缓冲。

避坑指南:我曾经在某个项目中,发现游戏界面偶尔卡顿。排查了半天,发现是BufferQueue满了,App在等Buffer释放。后来我们优化了绘制逻辑,把每帧的绘制时间压到了12ms以内,问题就解决了。所以,三缓冲不是万能的,关键还是得优化绘制性能

合成与显示:SurfaceFlinger的“最后一公里”

好了,现在App已经把数据写到了Buffer里,BufferQueue里排着队。SurfaceFlinger该干活了。

SurfaceFlinger的合成方式主要有两种:

  • GPU合成:用OpenGL ES把多个Surface合成到一个Buffer里,然后输出到屏幕
  • HWC合成:硬件合成器,由显示硬件直接合成多个图层

HWC合成效率更高,但需要硬件支持。GPU合成更灵活,但耗电。SurfaceFlinger会优先尝试HWC,如果不行就回退到GPU。

我画了一张图,帮你理清整个流程:

App 绘制 (CPU + GPU) Surface (画布) BufferQueue [Buffer1] [Buffer2] [Buffer3] SF 合成方式 GPU合成 / HWC合成 (优先HWC) 显示输出 (屏幕刷新) 帧率控制 VSync同步 16.6ms/帧 View绘制 → Surface → BufferQueue → SurfaceFlinger → 屏幕

VSync:让一切有序进行

你可能注意到了,上面提到了VSync。这是个很重要的概念。

如果没有VSync,App和SurfaceFlinger各干各的,那画面就会乱套。App可能刚画了一半,SurfaceFlinger就把数据取走了,导致画面撕裂。

VSync就是一个同步信号。它每隔16.6ms(60Hz)发一次,告诉App和SurfaceFlinger:该干活了

  • App收到VSync信号,开始绘制下一帧
  • SurfaceFlinger收到VSync信号,开始合成当前帧

这样就能保证App和SurfaceFlinger步调一致。

注意:如果App绘制一帧的时间超过了16.6ms,那它就会错过下一个VSync信号。这时候SurfaceFlinger只能重复显示上一帧,就产生了掉帧。所以,优化每帧的绘制时间,是避免掉帧的根本

总结一下

好了,我们来捋一捋整个流程:

  1. App通过View绘制,把数据写到Surface对应的Buffer里
  2. Buffer通过BufferQueue传递给SurfaceFlinger
  3. SurfaceFlinger用GPU或HWC合成多个Surface
  4. 合成后的数据输出到屏幕
  5. 整个过程由VSync同步,保证画面流畅

说白了,View绘制只是“画”的过程,SurfaceFlinger才是“显示”的关键。理解了这一点,你就能明白为什么有时候App绘制很快,但界面还是卡——可能是SurfaceFlinger那边堵住了。

我在项目中遇到过类似的问题。有一次,我们App的帧率稳定在55fps以上,但用户反馈说滑动不跟手。后来发现是SurfaceFlinger的合成耗时太长,导致BufferQueue堆积。我们调整了图层数量,把不必要的透明图层合并,问题就解决了。

嗯,这就是SurfaceFlinger的威力。它虽然不直接参与View的绘制,但却是整个渲染管线的“最后一公里”。

个人建议:如果你遇到界面卡顿问题,别只盯着App的绘制代码。用adb shell dumpsys SurfaceFlinger看看BufferQueue的状态,说不定能发现新大陆。


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