18、图形系统:SurfaceFlinger、HWUI、GPU 渲染、Vsync 与 Triple Buffering

图形系统,说白了就是Android的“脸面”。你每次滑动屏幕、打开应用、播放动画,背后都有一整套流水线在疯狂运转。我早年刚接触这块时,觉得不就是画个图嘛,能有多复杂?直到被SurfaceFlinger的同步问题折磨了几个通宵……嗯,从那以后,我再也不敢小看它了。

18.1 整体架构:谁在画?谁在拼?谁在送显?

整个图形流水线,可以拆成三个角色:

  • 应用层(HWUI):负责把Button、TextView这些控件,变成GPU能懂的绘制指令。
  • 合成层(SurfaceFlinger):把多个应用的“画布”合成一张完整的屏幕图像。
  • 硬件层(GPU + Display):真正执行渲染,并把最终画面推送到显示器。

我个人习惯把这三层比作“画家、拼图师、快递员”。画家画好每一块拼图,拼图师把它们拼成完整画面,快递员按时送到屏幕上。任何一个环节掉链子,你就会看到卡顿或撕裂。

核心要点:SurfaceFlinger是系统服务,运行在独立的进程中。它不直接参与绘制,只负责合成。HWUI是应用进程里的渲染引擎,它调用OpenGL ES或Vulkan来绘制。

Android 图形系统三层架构 应用进程 HWUI / Canvas GPU 绘制指令 SurfaceFlinger 图层合成 HWC / GL合成 显示硬件 GPU / Display Vsync 刷新 Buffer 传递 送显 Vsync 信号

18.2 HWUI:从XML到GPU指令

你写的布局文件,最终怎么变成屏幕上的像素?HWUI就是干这个的。它把View树遍历一遍,生成一系列绘制操作(DisplayList),然后提交给GPU。

我记得有一次优化启动速度,发现HWUI在首次绘制时耗时严重。原因很简单——它需要为每个View创建DisplayList。后来我们用了异步预加载,把常用页面的DisplayList提前构建好,启动速度直接快了30%。

我的建议:如果你在做自定义View,尽量用canvas.clipRect()限制绘制区域。这能减少GPU的无效渲染,效果立竿见影。

18.3 SurfaceFlinger:图层合成大师

SurfaceFlinger接收来自多个应用的Buffer(通常是GraphicBuffer),然后决定怎么合成。这里有两条路:

  • GPU合成:把所有图层用OpenGL ES合成到一块Buffer里。灵活,但费电。
  • HWC合成:交给显示硬件(Display Controller)直接合成。省电,但有限制。

实际项目中,SurfaceFlinger会动态选择。比如你只叠了两个全屏图层,HWC就能搞定。但如果你搞了个悬浮窗、又开了画中画,图层一多,HWC可能就撂挑子了,退回GPU合成。

避坑指南:我曾经遇到过一个bug——悬浮窗导致整个系统掉帧。查了两天才发现,是悬浮窗的Buffer格式不匹配,导致SurfaceFlinger无法走HWC合成,被迫用GPU合成,帧率直接腰斩。解决方案:确保所有Surface的像素格式统一,尽量用RGBA_8888。

18.4 GPU渲染:OpenGL ES 与 Vulkan

Android的GPU渲染,早期全靠OpenGL ES 2.0。现在主流是OpenGL ES 3.x,而Vulkan正在慢慢普及。Vulkan的优势在于更低的CPU开销更好的多线程支持

我个人习惯在性能敏感的场景下用Vulkan。比如相机预览、AR应用。但日常App开发,OpenGL ES完全够用。别为了炫技强行上Vulkan,驱动兼容性问题会让你头疼。

// 一个典型的OpenGL ES绘制三角形代码(简化)
glUseProgram(program);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glDrawArrays(GL_TRIANGLES, 0, 3);

你想想看,这段代码背后,CPU要准备顶点数据、编译着色器、管理状态机。如果每帧都做这些,性能肯定崩。所以HWUI会把绘制指令缓存起来,避免重复劳动。

18.5 Vsync:心脏起搏器

Vsync(垂直同步)是显示器的刷新节奏。通常60Hz的屏幕,每16.6ms发出一个Vsync信号。Android的图形系统完全围绕Vsync运转。

  • 应用端:收到Vsync后,开始处理输入、动画、绘制。
  • SurfaceFlinger端:收到Vsync后,开始合成并送显。

如果应用绘制耗时超过16.6ms,就会错过当前Vsync,导致掉帧。这就是你感觉“卡”的根本原因。

关键机制:Android 4.1引入了Project Butter,核心就是“一切跟着Vsync走”。应用和SurfaceFlinger都通过Choreographer来注册Vsync回调,步调一致。

18.6 Triple Buffering:解决卡顿的杀手锏

为什么需要Triple Buffering?我们先看Double Buffering的问题:

  1. 应用在Buffer A里绘制。
  2. 绘制完成后,交换到Buffer B,SurfaceFlinger开始合成B。
  3. 如果应用在下一帧Vsync到来时还没画完,它只能等——这就产生了空闲时间。

Triple Buffering增加了一个Buffer C。当应用在画A、SurfaceFlinger在合成B时,C处于空闲。如果应用没赶上Vsync,它可以立刻用C继续画,不用干等。说白了,就是用内存换时间。

方案 Buffer数量 延迟 CPU/GPU空闲率 适用场景
Double Buffering 2 高(容易空闲等待) 低负载、稳定帧率
Triple Buffering 3 略高(多1帧延迟) 低(减少空闲) 高负载、帧率波动大

我在做游戏引擎移植时,遇到过Triple Buffering的“副作用”——触摸延迟变大了。因为多了一帧的缓冲,从触摸到显示,多了16.6ms。后来我们做了触摸预测,才把这个问题压下去。

小技巧:dumpsys SurfaceFlinger的输出里,你可以看到当前使用的Buffer数量。如果发现经常掉到Double Buffering,说明系统内存紧张,或者SurfaceFlinger在降级运行。

18.7 实战:如何定位图形性能问题?

最后分享几个我常用的调试手段:

  • 抓取Systrace:看SurfaceFlinger和应用的Vsync对齐情况。如果应用绘制时间超过16.6ms,一目了然。
  • 使用GPU Profiling:在开发者选项里打开“GPU渲染模式分析”,看柱状图是否经常超过绿线。
  • 检查Overdraw:用开发者选项的“显示过度绘制区域”,红色区域越多,性能越差。

嗯,图形系统这块内容确实多。但只要你理解了SurfaceFlinger的合成逻辑、Vsync的节拍、以及Triple Buffering的缓冲思想,大部分性能问题都能迎刃而解。记住:一切卡顿,都是因为错过了Vsync

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