1. 一张图看懂Android显示完整链路:从App绘制到屏幕像素的奇幻漂流
如果你正在学习Android Framework、做性能优化,或者单纯好奇“我手指在屏幕上点了一下,图像到底是怎么画出来的”,那么这篇文章就是为你准备的。我做了多年Android系统层开发,带过不少新人,发现大家最容易卡住的地方,就是“只知道SurfaceView能显示画面,但不清楚从View的draw()方法调用,到屏幕真正亮起来,中间隔着多少个系统组件”。
这第1篇,我们不抠代码细节,先把Android显示系统的全局地图铺开。我会用最直白的方式,把这条链路上的每一个关键节点讲清楚:App进程怎么把UI画进内存,SurfaceFlinger怎么把这些内存合成到一起,HWC怎么跟显示硬件打交道,最后像素怎么变成你看到的画面。一句话概括:Android的显示链路,就是一条从CPU/GPU绘制,到进程间传递,再到硬件扫描输出的流水线。
这个内容适合谁?想做应用层性能优化的开发者,能从中找到卡顿的根源方向;刚入门的Framework学习者,能建立完整的知识坐标系;甚至做系统移植、驱动开发的兄弟,也能理解上层行为与底层硬件之间的咬合关系。我保证,看完你会对Android系统多一层敬畏之心——原来一块屏幕亮起来,背后是这么精密的一场协作。
2. 显示链路全景:核心成员与职责划分
在展开细节之前,得先把这些核心成员的名字和分工明确下来。因为后面所有讨论,都绕不开它们。
2.1 应用进程内的绘制接力:从View到Skia/OpenGL
你写的每个Activity,内部都有一棵View树。当某个View需要重绘时,系统会触发一次完整的绘制遍历:测量尺寸、确定位置、逐个执行onDraw()。但请注意,onDraw()里你拿到的Canvas,背后坐着的可不是普通的内存画板,而是经过了重重包装的“画布代理”。
Canvas调用的绘图指令,会被翻译成底层绘图引擎的命令。Android主流使用的绘图引擎是Skia,它是一个软件渲染为主的2D图形库;而如果你的应用开启了硬件加速(默认开启),绘制指令则会被转换成OpenGL ES命令,交给GPU去执行。这里有个关键概念:硬件加速下,View的draw()方法产生的不是像素,而是绘制指令。这些指令会被记录在DisplayList里,GPU再根据DisplayList执行真正的像素渲染。
绘制完成后,像素被渲染到一个叫Surface的缓冲区内。每个Window(窗口)都对应一个Surface,Surface本质上是BufferQueue的生产者端。App是生产者,它往BufferQueue里填充图像数据;消费者则是后面的SurfaceFlinger。
2.2 Framework层的核心枢纽:SurfaceFlinger与WindowManager
SurfaceFlinger是Android显示系统的“中央厨房”,负责把所有App提交来的画面合成为一帧最终图像。它运行在一个独立的系统进程里,拥有极高的优先级。所有App的Surface,最终都要通过Binder IPC把Buffer交给SurfaceFlinger来合成。
WindowManagerService(WMS)则是窗口秩序的管理者,它决定每个窗口的大小、位置、层级(Z序)、透明度、动画状态。SurfaceFlinger在合成每一帧时,要参考WMS维护的窗口层级关系,决定哪个窗口在上、哪个在下、哪些区域被遮挡可以不画。两者配合的经典场景就是:你在玩游戏时收到了微信消息弹窗,弹窗的Z序高于游戏,SurfaceFlinger会把两个Buffer叠到一起——游戏画面在下,弹窗在上,最终合成一帧完整画面。
2.3 硬件抽象与内核驱动:HWC与Display Driver
SurfaceFlinger虽然负责合成,但它并不直接操控屏幕硬件。它把合成结果分成若干个Display Layer集合,交给HWC(Hardware Composer)去处理。HWC是硬件抽象层的一个组件,由显示芯片厂商实现,它知道如何最高效地把图层混合到一块上,并最终通过内核的Display Driver驱动,把像素数据送给屏幕面板。
这里要区分两种合成方式:GPU合成和硬件合成。GPU合成就是SurfaceFlinger用OpenGL ES把多个图层画到一块Buffer上,再把这块Buffer送到显示器;硬件合成则是HWC通过显示控制器的Overlay通道,把多个图层直接混叠成最终画面,基本不消耗GPU。理解这两者的差异,是后续做性能优化的基石。
2.4 数据结构大管家:BufferQueue与GraphicBuffer
所有画面数据的流转,都基于BufferQueue机制。BufferQueue是一个典型的生产者-消费者模型,App(生产者)从队列里“借”一个Buffer,填充像素,再还给队列;SurfaceFlinger(消费者)从队列里取出Buffer,合成完之后归还。这套机制保证了内存区域不会发生读写冲突,同时通过Buffer复用避免反复分配内存。
Buffer本身对应的是GraphicBuffer对象,它封装了匿名共享内存或硬件显存中的一块区域,携带分辨率、像素格式、颜色空间等元信息。在Android 8.0以后,GraphicBuffer还通过BufferHub机制支持了跨进程文件描述符传递,效率大幅提升。
3. 一张图拆解显示链路各阶段核心细节
现在我们把整条链路切成四个阶段,逐一拆解。下面这张图是我根据多年源码阅读经验整理的流程骨架,后面所有文字都是围绕这张图展开的。
图示示意(非代码):App进程onDraw → DisplayList → GPU渲染 → BufferQueue入队 → SurfaceFlinger合成 → HWC硬件合成 → Display驱动 → 物理屏幕。
3.1 阶段一:App进程内从View绘制到Buffer生产
这一阶段从ViewRootImpl.performTraversals()开始,它会依次执行measure、layout、draw三大流程。性能优化时,我们常说要减少measure/layout次数,因为这两步是CPU密集型的计算;draw本身如果不太复杂,耗时会低一些,但要留意DisplayList的录制开销。
硬件加速开启时,View.draw()方法里创建的Canvas,实际上是RenderNode的封装。每个View对应一个RenderNode,它保存了该View的绘制属性(位移、缩放、透明度、阴影等)和DisplayList。子View的RenderNode组成一颗渲染树,整个过程不直接操作像素,只记录“怎么画”。真正执行像素绘制时,是由RenderThread(渲染线程)来做的,这个线程独立于主线程,避免UI线程被GPU操作阻塞。
这里有个容易被忽略的点:不是所有绘制都在RenderThread完成。如果View调用了setLayerType(View.LAYER_TYPE_SOFTWARE)强制软件渲染,那么onDraw()里的指令会直接通过Skia的CPU路径执行,像素被画到Bitmap上,再上传到Surface。这种情况常见于需要大量复杂绘制操作且GPU收益不明显的场景,比如某些PDF渲染或极度简单的UI。软件渲染是阻塞式执行,绘制量大时卡顿会非常明显。
绘制完成后,App会通过Surface的queueBuffer()把Buffer提交到BufferQueue。正常情况下App并不等渲染结束再干别的,而是“渲染完一帧就提交一帧,继续准备下一帧”。如果渲染速度跟不上屏幕刷新率(比如60Hz下16.6ms/帧),BufferQueue里的Buffer就会耗尽,App端会被阻塞等待空闲Buffer,这就是“掉帧”的一种根源。
3.2 阶段二:Binder IPC传递与BufferQueue的流转
Buffer从App到SurfaceFlinger,中间的关键是BufferQueue的跨进程传递机制。在Android 8.0之前,Buffer的传递严重依赖Binder传输GraphicBuffer的引用;8.0之后引入了AIDL化的BufferQueue接口,通过Transaction打包向SurfaceFlinger发送。
这里要理解Burstable传递的细节:App调用queueBuffer()时,Buffer本身物理内存并不跟着Binder数据包走。Binder传的是GraphicBuffer的句柄(Binder节点引用),SurfaceFlinger拿到句柄后通过GraphicBuffer的Binder接口按需映射内存。如果App与SurfaceFlinger在同一进程中(比如系统UI的某些部分),甚至可以直接共享内存映射,零拷贝。
BufferQueue的容量通常设置为3个Buffer(也就是三重缓冲)。三重缓冲的意义在于:显示屏幕正在扫描的是一块Buffer(前缓冲),GPU正在渲染的是另一块(后缓冲),第三块则是为了应对生产者和消费者节奏不一致的波动。多一块缓冲能略微吸收卡顿尖峰,但也会增加触控到显示的延迟。所以Android会动态调整Buffer数量,有时甚至降到2个来兼顾跟手性。
3.3 阶段三:SurfaceFlinger合成策略与VSync节奏
SurfaceFlinger的合成节奏由VSync信号驱动。VSync由硬件(或软件模拟)产生,频率与屏幕刷新率一致。当VSync到来时,SurfaceFlinger会执行一次合成:遍历所有可见Surface的BufferQueue,取出新到的Buffer,根据窗口层级关系和透明区域,生成合成计划。
合成计划的来源有两个:
- 如果图层数少(比如只有状态栏+应用+导航栏三个图层),且每个图层都不透明、不旋转、不做复杂变换,HWC可以直接用Overlay通道硬件合成,SurfaceFlinger几乎不用干活。
- 如果某个图层有圆角、缩放、模糊等特效,或者图层数超过了硬件的Overlay通道数,SurfaceFlinger就得用GPU把这些图层画到一块临时Buffer里,做成一个大的Layer,再交给HWC输出。
GPU合成比硬件合成多了一次额外的绘制开销,但灵活性高,支持任意变换。HWC硬件合成效率高、省功耗,但是功能受限。性能优化的核心方向之一,就是尽量减少让SurfaceFlinger做GPU合成的图层数——比如避免在多个悬浮窗上加模糊效果,或者减少全屏Surface的层数。
合成完成后,SurfaceFlinger会把最终Buffer交给HWC,HWC内部会通过setLayerBuffer()等接口配置显示控制器,把Buffer地址、裁剪区域、缩放比例告诉驱动,最终由Display Controller按像素格式逐行扫描输出到屏幕。
3.4 阶段四:显示驱动与物理像素输出
这个阶段离上层最远,但也最不可控。Display Driver通常位于内核空间,它的职责是:接收HWC(通过framebuffer或DRM/KMS接口)设置的图层参数,配置显示控制器的DMA通道,把内存中的像素数据按照MIPI DSI、eDP或HDMI协议时序逐像素发送给显示面板。
显示面板本身也有刷新行为:上面的每个像素都靠晶体管存储电压,面板控制器按行列扫描刷新这些像素。LCD面板需要背光,OLED自发光。扫描方式按“渐进式”(逐行一次刷完)或“隔行式”(分奇偶场)区分,现代手机普遍是渐进式。刷新率和分辨率共同决定像素时钟频率,分辨率越高、刷新率越高,DMA带宽占用越大,这也是为什么高分辨率高刷新率手机更费电。
有一个容易混淆的概念:刷新率不等于流畅度。刷新率是硬件每秒扫多少次屏幕,流畅度则取决于每帧是否能赶上VSync节奏。帧率低于刷新率会掉帧,帧率高于刷新率则会出现撕裂(tearing)。Android通过VSync与三重缓冲的配合,试图保持“一帧一帧”按节奏输出,撕裂通常被天然规避,但掉帧卡顿仍然是我们最常遇到的问题。
4. 链路中的经典问题与排查方向参考
做显示优化,最烦的就是“我明明没改代码,怎么突然卡了”。这类问题往往不在应用层,而在链路更深处。我整理了几类最常见的故障,以及对应的排查路径,希望对你有参考价值。
4.1 卡顿掉帧:别急着优化业务,先看链路哪段在堵
如果你发现特定场景下帧率骤降,先用systrace抓trace,关注三个阶段:主线程的performTraversals耗时是否超5ms、RenderThread的draw耗时是否异常、SurfaceFlinger合成是否超时。如果卡在三段之外的Buffering,说明BufferQueue没Buffer可消费了,App生产速度跟不上,而不是系统不给力。
另一种情况是SurfaceFlinger自身过载:比如同时运行的悬浮窗太多,且每个都开了模糊。你在dumpsys SurfaceFlinger里能看到Skipped frames计数飙高,此时通常是HWC通道不够、GPU合成压力大。解决思路是减少悬浮窗层,或关闭低价值图层的阴影模糊效果。
4.2 Surface Buffer报错:常见于视频流的异常释放
视频类App经常遇到BufferQueue has been abandoned或failed to allocate buffer。前者通常是对面消费者(SurfaceFlinger或MediaCodec)已经释放了Surface,App还在往里面递交Buffer;后者一般是内存不足,或GraphicBuffer分配器连接Gralloc分配失败,常见于低内存设备。
排查时去/sys/kernel/debug/binder看Binder节点状态,或者cat /proc/meminfo确认显存可用量。多数情况是代码里没有正确处理SurfaceDestroyed回调,在Surface已销毁后还继续queueBuffer。解决方式是注册SurfaceHolder.Callback,在surfaceDestroyed时停止渲染线程,并做好重新创建Surface的重试机制。
4.3 掉帧率追踪与帧率统计工具推荐
推荐几组挺有效的命令,我平时排查基本就靠它们:
adb shell dumpsys SurfaceFlinger --latency adb shell dumpsys gfxinfo <package_name> framestats adb shell screenrecord --bugreport /data/local/tmp/trace.mp4dumpsys SurfaceFlinger --latency会输出每一帧的时间戳,你能看到是否连续掉帧;framestats能精确到VSync、App绘制、合成等各段耗时,是哪一层拖了后腿一目了然。如果你在研发手机ROM,还可以用Perfetto的UI模式直接看SurfaceFlinger线程调度,信息量更全。
4.4 多屏与异形屏适配的显示链路差异
折叠屏、刘海屏、瀑布屏这类设备,显示链路多了“显示区域裁剪”的环节。刘海区域不会被应用绘制覆盖,那么SurfaceFlinger在合成时就需要处理DisplayCutout的区域裁剪逻辑。多屏联动时,每个物理屏会对应一个独立的DisplayDevice,但SurfaceFlinger仍然是全局唯一的合成者,它要根据每个DisplayDevice的VSync独立调度合成。因此,当你做多屏投屏时,要留意两个屏的刷新率可能不同,Buffer生产策略需要同时满足两个消费者的节奏——这也是很多投屏方案掉帧的隐藏原因之一。
5. 深入理解显示链路后的三个实用感悟
文章到这里,主体链路和各阶段细节都讲完了。最后分享几个我这些年在系统层开发中沉淀下来的体会,也许对你的开发思路会有些启发。
第一,时刻记得显示链路是一个流水线,而不是一次调用。当你想优化性能时,不要只在应用层找问题。我见过一个播放器项目,视频解码一切都正常,就是画面顿挫,后来追踪发现是SurfaceFlinger接到了过多的悬浮窗Layer,硬件Overlay通道不够用,每次都走GPU合成,功耗和延迟双双升高。好多事情,换个观察视角就能找到真相。
第二,VSync是整个系统的“心跳”,理解它才能理解现代Android UI。现在很多新手机都是120Hz刷新率,你App的Buffer生产如果仍然是按60Hz的老策略来,即使系统帧率再高,你体验到的依然是每两帧才更新一次。Choreographer的存在就是为了让App与硬件VSync对齐,做动画时利用它来驱动,而不是自己开Handler死循环,是更聪明的做法。
第三,工具链是自己最好的老师。我强烈建议花两周时间把Perfetto和systrace跑熟,把“看trace”当成日常习惯,而不是出问题了才抓。当你对正常链路下的trace形态烂熟于心,异常出现时,你一眼就能看出哪一段时间线是歪的。
这一篇我们先完成全局认知:App侧绘制、BufferQueue数据流、SurfaceFlinger合成、HWC硬件输出,再到物理屏幕。下一篇文章,我会在全局认知的基础上,带你认真看一遍SurfaceFlinger的完整合成时序——尤其是它的VSync机制、Transaction处理流程,以及和WMS的联动关系,把这条链路上的每一个关键逻辑都抠透。这个系列的目标,就是帮你彻底用掉“显示链路”这块硬骨头上的所有迷茫。