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。
我画了一张图,帮你理清整个流程:
VSync:让一切有序进行
你可能注意到了,上面提到了VSync。这是个很重要的概念。
如果没有VSync,App和SurfaceFlinger各干各的,那画面就会乱套。App可能刚画了一半,SurfaceFlinger就把数据取走了,导致画面撕裂。
VSync就是一个同步信号。它每隔16.6ms(60Hz)发一次,告诉App和SurfaceFlinger:该干活了。
- App收到VSync信号,开始绘制下一帧
- SurfaceFlinger收到VSync信号,开始合成当前帧
这样就能保证App和SurfaceFlinger步调一致。
注意:如果App绘制一帧的时间超过了16.6ms,那它就会错过下一个VSync信号。这时候SurfaceFlinger只能重复显示上一帧,就产生了掉帧。所以,优化每帧的绘制时间,是避免掉帧的根本。
总结一下
好了,我们来捋一捋整个流程:
- App通过View绘制,把数据写到Surface对应的Buffer里
- Buffer通过BufferQueue传递给SurfaceFlinger
- SurfaceFlinger用GPU或HWC合成多个Surface
- 合成后的数据输出到屏幕
- 整个过程由VSync同步,保证画面流畅
说白了,View绘制只是“画”的过程,SurfaceFlinger才是“显示”的关键。理解了这一点,你就能明白为什么有时候App绘制很快,但界面还是卡——可能是SurfaceFlinger那边堵住了。
我在项目中遇到过类似的问题。有一次,我们App的帧率稳定在55fps以上,但用户反馈说滑动不跟手。后来发现是SurfaceFlinger的合成耗时太长,导致BufferQueue堆积。我们调整了图层数量,把不必要的透明图层合并,问题就解决了。
嗯,这就是SurfaceFlinger的威力。它虽然不直接参与View的绘制,但却是整个渲染管线的“最后一公里”。
个人建议:如果你遇到界面卡顿问题,别只盯着App的绘制代码。用adb shell dumpsys SurfaceFlinger看看BufferQueue的状态,说不定能发现新大陆。
公众号:蓝海资料掘金营,微信deep3321