ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android图形系统全解析:从SurfaceFlinger到BufferQueue的渲染管线

Android图形系统全解析:从SurfaceFlinger到BufferQueue的渲染管线 1. 从一次滑动说起一帧画面是怎么到屏幕上的做Android开发的人多少都听过SurfaceFlinger、HWUI、BufferQueue这些词可真要问你“手指划过屏幕那一帧画面到底经历了什么”能把整条链路讲清楚的人其实不多。我这个系列就是想把这个坑填上第一篇先搭整体框架把各个模块的分工和关系建立起来。先看一个最简单的场景你用手指在屏幕上滑动一个列表屏幕上不断出现新内容。这个过程拆开来看大致是这么一条流水线应用进程里的UI线程处理触摸事件更新View树把要画的东西记录成DisplayListRenderThread渲染线程拿到DisplayList用Skia把内容画到一块图形缓冲区上这块缓冲区通过BufferQueue交到系统进程的SurfaceFlinger手里SurfaceFlinger把所有应用的图层Layer做一次合成把最终结果交给硬件合成器HWC或GPU显示控制器把合成好的画面扫描到屏幕上这条链路里每一环都有大量细节但作为第一篇我建议你先把这个“流水线”的直觉建立起来应用负责画SurfaceFlinger负责合HWC/显示系统负责出。后面所有复杂概念本质上都是在回答这三件事里的某个子问题。这个系列适合谁看我认为三类人值得读做应用开发遇到卡顿、掉帧、启动白屏想深入定位问题的做系统定制或ROM开发需要理解SurfaceFlinger、WindowManager联动机制的纯粹想搞懂Android图形体系准备面试或转图形方向的第一篇不会堆代码重心放在框架、模块边界、数据流和关键角色上让你能拿着一张大图去理解后续的每一篇。2. 应用侧看不见的绘制管线2.1 View、DisplayList 与 RenderThread 的分工很多开发者以为“自定义View的onDraw写了代码画面就出来了”实际上onDraw只是开始。从Android 4.0之后View的绘制就走上了硬件加速路线onDraw里写的drawXxx调用会被记录成一份DisplayList而不是立刻执行真正的绘制命令。什么意思你可以把DisplayList理解成一张“绘制清单”上面记着“这里画一个圆那里画一段文字颜色是什么”。UI线程只负责“记账”不负责“干活”。真正干活的是RenderThread——一个专门干渲染的线程。它拿到这份清单后才会调用Skia去执行真正的绘制。为什么要分成两个线程因为UI线程要保证对触摸、输入事件的及时响应如果绘制都压在UI线程上用户一滑动画面稍微复杂一点主线程就被拖死掉帧就成了必然。把绘制放到RenderThreadUI线程就能腾出手来处理用户交互。这也是Android这些年一直在加强的方向到了RenderThread之后很多绘制工作甚至可以在UI线程空闲时提前预生成比如RenderNode缓存。2.2 绘制落地Skia、EGL与GPUDisplayList被RenderThread执行时真正的“画笔”是Skia也就是从Android 8.0开始全面启用的绘图引擎。Skia是一套纯软件实现也支持硬件加速的2D图形库它负责把你的圆、文字、位图、特效转换成GPU能理解的东西。这里有一个细节值得注意RenderThread不是直接把Skia命令交给GPU。中间还隔着一层EGL。EGL是OpenGL ES和窗口系统之间的桥梁它的作用是创建渲染上下文EGLContext创建与Surface绑定的绘图表面EGLSurface管理绘制过程中与屏幕的同步eglSwapBuffers一个常见的理解误区是“Skia就是软件绘制GPU绘制是OpenGL的东西”。实际上Android 8.0之后Skia既可以跑在CPU上软件渲染也可以跑在GPU上Skia GL backend默认情况下硬件加速开启时Skia会通过OpenGL ES把绘制指令发给GPU执行。到了Android 10之后还加入了Vulkan backend部分设备默认走Vulkan渲染。所以应用侧的真实流程是View树 → DisplayList → RenderThread → Skia → OpenGL ES/Vulkan → 绘制到Surface对应的缓冲区注意最后一步Skia是“画到缓冲区里”不是“画到屏幕上”。这一步非常关键它决定了应用进程和系统进程之间的边界——应用只负责往缓冲区填像素这个缓冲区最终怎么上屏不归应用管。2.3 Surface应用与系统之间的交接契约说到缓冲区就必须提Surface。你可以把Surface理解为“应用窗口对应的画面缓冲区”的句柄。每个WindowActivity对应的PhoneWindow都对应一个Surface放在WindowManagerServiceWMS那边统一管理。应用侧拿到Surface之后可以拿到它的Canvas通过Surface.lockCanvas或者传给EGL做硬件绘制。本质上Surface背后是一个BufferQueue的生产者Producer角色。应用往Surface上画画就是往BufferQueue里生产缓冲区。这里有一个我早期踩过的坑很多人分不清SurfaceView、TextureView和普通View的区别。普通View是画在窗口的Surface上的SurfaceView则是应用主动创建一个独立的Surface图层它和窗口的Surface是分开的纹理数据直接给SurfaceFlinger不走应用进程的合成。TextureView则是在普通View体系里“伪装”成一个控件但内部有一个独立的Surface作为纹理来源。三者在性能和层级关系上有明显差异后续系列里我会单独展开讲一讲。3. 系统侧的心脏SurfaceFlinger 与 BufferQueue3.1 BufferQueue生产者和消费者的数据搬运通道缓冲区从应用进程搬到系统进程中间靠的是BufferQueue。这是一个非常经典的生产者-消费者模型在Android图形栈里贯穿始终。简单画一下它的结构BufferQueue核心里维护着一组图形缓冲区GraphicBuffer默认可以分配多个常见配置是3个左右生产者应用侧的Surface通过dequeueBuffer拿一块空闲缓冲区画完后通过queueBuffer交给BufferQueue消费者绝大多数情况是SurfaceFlinger通过acquireBuffer拿这块缓冲区去使用用完通过releaseBuffer归还为什么要搞多个缓冲区如果只有一块那生产者用的时候消费者没法用必然阻塞用两块双缓冲勉强能交替但一旦某一帧耗时抖动生产者可能等不到空闲缓冲区发生掉帧。三块缓冲区也就是“三缓冲”给流水线留出了弹性空间允许短暂的生产消费节奏波动。这块BufferQueue的位置很有意思它名义上属于应用进程的Surface但真正的Core实体在SurfaceFlinger进程里应用侧持有的其实是Binder代理。也就是说BufferQueue里的缓冲区是通过Binder跨进程传递fd文件描述符来共享内存的。这也是GraphicBuffer使用共享内存映射实现的原因。3.2 BLAST现代Android上的BufferQueue进化如果你看过Android 11以上的源码会发现一个高频词叫BLAST——Buffer Lifecycle And Streaming Technology。这是Google对BufferQueue和SurfaceFlinger协作方式的一次重构。在BLAST之前SurfaceFlinger是“拉”模式它收到vsync后主动去检查每个Layer有没有新缓冲区。在BLAST之后变成了“推”模式生产者应用queueBuffer时直接把缓冲区和相关Transaction一起推给SurfaceFlingerSurfaceFlinger只做合成调度。这个改动的意义在于解耦。生产者不再需要等SurfaceFlinger逐帧询问缓冲区生命周期、帧时间戳、透明度变化等元数据都可以跟着Transaction一起提交这让帧调度、延迟控制变得精确可控。也给后续的帧率自适应如根据场景动态切换刷新率打好了基础。所以读现代Android源码API 29以上你会发现BLASTSurfaceControl、Transaction这些类满天飞。理解BLAST是理解Android图形栈新架构的钥匙强烈建议多花点时间。3.3 SurfaceFlinger的合成工作几乎所有应用的Surface最终都要交给SurfaceFlinger统一处理。为什么不能每个应用直接往屏幕上画因为屏幕只有一个多个应用窗口叠加在一起需要知道谁在上谁在下、谁遮挡谁、透明度怎么算、要不要裁剪。这些全局性的安排必须由一个集中管理者来做——这就是SurfaceFlinger存在的意义。SurfaceFlinger的核心工作可以概括为接收每个Layer的缓冲区更新通过BufferQueue或BLAST Transaction根据Layer的Z序、透明度、变换矩阵等属性计算合成区域调用HAL层的HWC接口尝试把合成工作交给硬件overlay完成如果硬件处理不了overlay数量不够、图层带有圆角/阴影等特效回退到GPU用OpenGL/Vulkan合成将最终合成结果提交给显示系统等待下一帧vsync上屏注意第四点。SurfaceFlinger并不是“一定用GPU合成”。市面上很多手机硬件平台提供多个硬件overlay plane显示层SurfaceFlinger会优先把各个Layer直接铺到不同的硬件层上由显示控制器直接混合输出不经过GPU。这条路省电、低延迟。只有当Layer数目超过硬件极限或者某些特殊效果如模糊硬件不支持时才走GPU合成。3.4 Transaction图层属性的提交机制在BLAST架构下SurfaceFlinger所有对图层属性的变更都通过Transaction事务来提交。一个Transaction可以包含图层的位置、大小、裁剪区域图层的透明度、阴影、圆角半径缓冲区的上台时间戳、是否参与缓存输入事件的分发区域这里有个经验之谈Transaction的提交频率可以很高因为它走的是内存通道而不是直接跨进程锁同步。但如果你频繁地在每个帧回调里创建大量Transaction也会让SurfaceFlinger进程压力变大。日常开发里除非做窗口动画否则不要手动去碰Transaction交给ViewRootImpl和WMS默认处理就好。4. 硬件与显示Composer、Vsync、刷新率4.1 HWC HAL硬件合成器的抽象层从Android 8.0之后显示合成相关的HAL统一为HWC 2.x。它的核心接口可以这么理解getCapabilities查询硬件支持哪些能力比如最多几个overlay层、是否支持色彩增强createLayer / destroyLayer创建合成层validate让HAL验证当前合成方案是否可行返回哪些Layer需要GPU合成CLIENT层、哪些可以直接交给硬件DEVICE层present把合成结果提交到显示器我当年做系统开发时最喜欢看的log就是SurfaceFlinger合成的记录它会明确告诉你每一帧哪些Layer是走device合成、哪些走了client合成。走device越多说明硬件利用越充分电量消耗越少如果大量layer都走了client合成就值得反思为什么没利用硬件层了最常见的元凶是图层带了半透明效果、旋转角度或者复杂的背景模糊。这里也顺便回应热搜词里那些Intel UHD Graphics驱动相关的信息Android图形栈在PC模拟器上运行时会使用宿主机的GPU作为Vulkan/OpenGL后端。如果宿主机显卡驱动是老的Intel UHD 620/630且驱动不更新宿主机的Vulkan支持不完整Android模拟器里的图形加速就可能退化为软件渲染表现为界面极卡、启动极慢。很多人在Android Studio里遇到模拟器黑屏或卡顿第一反应是分配的内存不够其实更常见的是宿主机GPU驱动太旧导致的图形后端不可用。建议优先更新主板厂商提供的显卡驱动再在AVD里调整Graphics选项如swiftshader_indirect和host模式之间的切换。4.2 Vsync一切的节拍器Android图形栈是一个按帧驱动的系统所有生产、消费、合成动作都跟着**垂直同步信号Vsync**走。Vsync由硬件通常来自Display设备或专门的sync模块产生经过系统分发后有两个目的地应用侧通过Choreographer回调通知应用开始准备新一帧系统侧SurfaceFlinger根据需要触发合成为什么必须跟随vsync因为屏幕是逐行扫描刷新的如果在扫描中途改变缓冲区内容画面会出现撕裂tearing。跟随vsync可以在两次扫描之间安全地切换缓冲区保证画面完整。这个机制和所有现代游戏引擎的帧同步是同一套逻辑。Android还引入了Vsync偏移offset机制让应用vsync和系统vsync错开一点时间避免应用和SurfaceFlinger在同一时刻争抢CPU同时也给“应用画完 → 交给SF合成”留出时间窗口。这个偏移量可以配置在深度调优触控跟手性或延迟问题时偏移是一个很值得调整的参数。4.3 刷新率与帧率自适应刷新率的意义早期Android设备刷新率固定60Hz也就是每16.6ms刷新一屏内容。近年来高刷屏普及90Hz、120Hz成为标配帧率调度也随之复杂化。Android 12之后引入了更完善的帧率管理系统会根据应用设置、画面内容动态切换刷新率播放视频时降到60Hz或更低游戏或滑动场景升到120Hz以平衡流畅度和功耗。理解这个背景对排查“为什么我的界面在120Hz屏幕上还是卡”这类问题很有帮助。有些卡顿并不是渲染慢了而是帧率调度策略没跟上应用没有声明支持高刷、内容静态时降了频、vsync分配不均匀等。排查这类问题要会看Perfetto里的FrameTimeline和SurfaceFlinger的调度记录只看systrace的CPU耗时是不够的。5. 关键调试命令我日常排查图形问题的工具箱框架讲完直接给一套我自己实际在用的排查手段都是落地可执行的。5.1 看SurfaceFlinger状态adb shell dumpsys SurfaceFlinger这条命令信息量极大重点看各Layer的name、z序、尺寸、是否可见BufferQueue状态比如哪些Layer有缓冲区块数异常、有没有卡buffer合成策略device/client混合情况HWC状态和错误计数如果你发现某个Layer的buffer长时间不更新同时界面又卡住大概率是生产端出了问题要么生产者App卡死了要么BufferQueue堵塞了。对应地看App帧率adb shell dumpsys gfxinfo package_name这个命令会输出最近帧的绘制耗时统计包括Draw、Prepare、Process、Execute几个阶段的具体耗时右上角还有丢帧Janky frames统计。性能优化时我会先跑一次这个看整体情况再决定要不要上Perfetto。看窗口层级状态adb shell dumpsys window adb shell dumpsys activity top这两个命令能确认Window到Surface的映射关系是否正常。有些“黑屏/白屏”问题往往是Window存在但Surface创建失败或者Surface被非法销毁了这种问题从SurfaceFlinger和Window的双重视角对照着看定位会快很多。5.2 上Perfetto做帧级分析比systrace更现代的是Perfetto。抓取方式adb shell perfetto --time 5s -o /data/misc/perfetto-traces/trace.perfetto-trace后续拉到本地adb pull /data/misc/perfetto-traces/trace.perfetto-trace在Perfetto UI里打开后重点看这些轨道SurfaceFlinger的合成活动确认vsync周期内SF是否及时响应目标App的RenderThread和Choreographer活动看渲染任务是否超时BufferQueue的acquire/release时间线看缓冲区是否堵塞CPU调度信息确认渲染线程是否有足够的运行时间片我自己最常用的排查思路是先看是App画慢了RenderThread耗时长还是SF合慢了SF处理Transaction/合成耗时长还是系统调度问题线程没被调度到。Perfetto能把这三类问题明显区分开省掉大量猜的环节。6. 新手入门最容易犯的三个误区最后聊几个我见过很多次的理解误区算是把框架知识落到实践上的提醒。第一个误区是以为“画到屏幕”和“画到Buffer”是一回事。做应用优化时经常有人盯着onDraw耗时看画一帧挺快的但界面还是卡。原因很简单onDraw只是记录DisplayList真正耗时可能在RenderThread的Skia执行阶段或者EGL的缓冲交换阶段又或者SurfaceFlinger的合成阶段。只看自定义View或者只看onDraw是整个链路里的一个截断视角想定位卡顿必须拉通全链路看。第二个误区是遇到掉帧就怀疑内存不够或CPU性能差。实际上图形栈里掉帧的原因五花八门BufferQueue没有可用的缓冲区生产消费节奏失衡、SurfaceFlinger合成超时过度复杂的Layer效果、vsync分发延迟调度器配置不合理、HWC回退到GPU合成导致功耗和耗时飙升。这些都不是加内存能解决的得靠框架知识去逐层排查。第三个误区是完全依赖标准答案式的结论比如“SurfaceView一定比TextureView好”。这是简化太过的结论。SurfaceView确实省一次纹理拷贝但它在窗口层、动画处理上有限制TextureView虽然多一次合成开销但在支持View变换、动画方面更灵活。真实项目里的取舍要结合场景视频播放选SurfaceView更合理需要和其他View做复杂联动时TextureView省心。这两个选择在下文深入讲Surface子系统时我会给出更细致的对比和实验数据。7. 学习这套框架的路径建议第一篇内容到这里框架已经立起来了。我建议你按下面这条路径继续深入而不是直接去死磕某一份源码先把“应用画 → BufferQueue传 → SurfaceFlinger合 → HWC出”这条主链路的图自己画一遍每个环节能写下对应的类名Surface、RenderThread、BufferQueue、Layer、HWC用dumpsys SurfaceFlinger命令对照真实设备的输出把抽象名词和实际Layer对应起来跑一次Perfetto找一帧从App到SF的时间关系建立起“16ms一帧”的直觉再回头读源码从BufferQueue的dequeue/acquire流程入手逐个模块深入这个系列接下来会分别挑细节去拆BufferQueue的完整生命周期、SurfaceView/TextureView的对比、SurfaceFlinger合成算法的演进、Vsync调度与帧率控制、HWC硬件合成原理还有常见卡顿问题的实战定位案例。我个人的体会是Android图形栈是那种“初看很散看透之后非常自洽”的体系。它的每一层设计都围绕着“效率、延迟、功耗”这三个指标做权衡。只要你抓住这条主线后面读任何一块源码都不会迷失方向。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进