1. 从一次滑动说起:一帧画面是怎么到屏幕上的
做Android开发的人,多少都听过SurfaceFlinger、HWUI、BufferQueue这些词,可真要问你“手指划过屏幕,那一帧画面到底经历了什么”,能把整条链路讲清楚的人其实不多。我这个系列就是想把这个坑填上,第一篇先搭整体框架,把各个模块的分工和关系建立起来。
先看一个最简单的场景:你用手指在屏幕上滑动一个列表,屏幕上不断出现新内容。这个过程拆开来看,大致是这么一条流水线:
- 应用进程里的UI线程处理触摸事件,更新View树,把要画的东西记录成DisplayList
- RenderThread(渲染线程)拿到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线程上,用户一滑动,画面稍微复杂一点,主线程就被拖死,掉帧就成了必然。把绘制放到RenderThread,UI线程就能腾出手来处理用户交互。这也是Android这些年一直在加强的方向,到了RenderThread之后,很多绘制工作甚至可以在UI线程空闲时提前预生成(比如RenderNode缓存)。
2.2 绘制落地:Skia、EGL与GPU
DisplayList被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理解为“应用窗口对应的画面缓冲区”的句柄。每个Window(Activity对应的PhoneWindow)都对应一个Surface,放在WindowManagerService(WMS)那边统一管理。
应用侧拿到Surface之后,可以拿到它的Canvas(通过Surface.lockCanvas),或者传给EGL做硬件绘制。本质上,Surface背后是一个BufferQueue的生产者(Producer)角色。应用往Surface上画画,就是往BufferQueue里生产缓冲区。
这里有一个我早期踩过的坑:很多人分不清SurfaceView、TextureView和普通View的区别。普通View是画在窗口的Surface上的,SurfaceView则是应用主动创建一个独立的Surface(图层),它和窗口的Surface是分开的,纹理数据直接给SurfaceFlinger,不走应用进程的合成。TextureView则是在普通View体系里“伪装”成一个控件,但内部有一个独立的Surface作为纹理来源。三者在性能和层级关系上有明显差异,后续系列里我会单独展开讲一讲。
3. 系统侧的心脏:SurfaceFlinger 与 BufferQueue
3.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一起推给SurfaceFlinger,SurfaceFlinger只做合成调度。
这个改动的意义在于:解耦。生产者不再需要等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图形栈是那种“初看很散,看透之后非常自洽”的体系。它的每一层设计,都围绕着“效率、延迟、功耗”这三个指标做权衡。只要你抓住这条主线,后面读任何一块源码,都不会迷失方向。