
开始做Android图形性能优化之前我有个建议先别急着抓trace别急着读Systrace的彩色条带先花半天时间把Android图形系统架构的整体骨架刻在脑子里。原因很简单——你遇到的几乎所有卡顿、掉帧、花屏、功耗问题归根结底都在讲同一件事一条图像数据在几个参与者之间怎么产生、怎么排队、怎么被消费。如果手里只有几个零散知识点定位问题基本靠猜优化结果往往是按下葫芦浮起瓢。这个图形专题系列第一篇我就想把Android图形系统架构掰开揉碎讲清楚从应用层到HAL层每一层负责什么、边界在哪里、数据以什么形式流转顺便把后面几篇要展开讲的内容做个地图。这个内容适合三类人看一是做Android系统开发、Framework定制的人二是做应用层性能优化、想搞清楚卡顿根因的开发者三是正准备啃Android源码但不知道该从哪下手的学习者。看完之后你至少能回答三个问题一帧画面从App产生到屏幕显示中间经过哪几个环节每个环节的耗时上限在哪里出了问题该去哪一层找证据。1. 先建立全局坐标系Android图形栈的分层模型1.1 从画到显的四个关键角色很多人提起Android图形系统第一反应是SurfaceFlinger但SurfaceFlinger只是其中一个环节。一条完整的图像流水线里有四个角色缺一不可应用进程里的渲染线程负责把View、Compose、SurfaceView的内容绘制成一张张位图也就是“帧”。BufferQueue帧数据的搬运通道应用把画好的帧放进去消费方从里面取走。SurfaceFlinger系统核心合成器把多个应用、多个窗口的图层按Z轴顺序合成为一帧最终画面。HWCHardware Composer硬件合成器抽象层把合成结果交给显示控制器Display Controller最终输出到屏幕。这个链路看起来简单但每个角色都不是单线程干活而且它们之间的连接方式决定了整个系统的性能边界。App侧的渲染线程通过CPUSkia/Canvas或GPUOpenGL ES/Vulkan生成内容生成的帧不是直接进屏幕而是先放进一块共享内存区域等SurfaceFlinger来取。SurfaceFlinger拿到所有窗口的帧之后经过合成Composition再交给HWCHWC负责把像素真正推送到物理显示接口上。理解这个模型最关键的一点是App负责“逐层绘制”SurfaceFlinger负责“多层叠加”HWC负责“最终输出”。三者各管一段谁也替代不了谁。很多性能问题就出在把这三者的职责搞混了——比如有人把界面卡顿归咎于SurfaceFlinger但实际上App侧根本没有按期把帧交上来有人以为加大HWC能力就能让所有页面变流畅但其实大量小图层在App侧就已经拖慢了渲染线程。1.2 源码里对应的关键目录架构不是空中楼阁它最终落在代码上。我建议读者把下面几个目录在本地AOSP里标上书签后续篇幅会频繁提到目录对应模块作用frameworks/native/libs/guiBufferQueue、Surface、SurfaceControl生产-消费模型的核心实现frameworks/native/services/surfaceflingerSurfaceFlinger服务图层管理、合成调度、VSYNCframeworks/native/services/surfaceflinger/RenderEngineGPU合成引擎用GPU完成客户端合成hardware/interfaces/graphics/composerHWC接口硬件合成抽象层frameworks/base/core/java/android/viewViewRootImpl、Surface应用侧Java层入口frameworks/native/libs/renderengineRenderEngine抽象渲染引擎封装这几个目录你顺着读一遍基本就能在代码层面把所有角色对号入座。实际写代码的时候你会发现很多关键类名在Java层和Native层各有一份比如Surface在Java层是android.view.SurfaceNative层是android::Surface它们通过JNI联系但职责不同。Java层Surface主要给App提供绘制接口Native层的Surface才是BufferQueue的生产端门面。2. BufferQueue贯穿图形系统的那条“传送带”2.1 生产者与消费者图像数据的交接仪式BufferQueue是整个Android图形系统里最容易被忽视、但最值得细看的部分。我习惯把它比作一条传送带App把画好的帧放到一个槽位上SurfaceFlinger从槽位上取走取完之后槽位又空出来给App继续画。这个“放-取-还”的循环几乎构成了Android图形系统的全部节奏。从API层面看生产者走的是dequeueBuffer申请空槽→ 写入像素 → queueBuffer上架这条路消费者走的是acquireBuffer取走→ 处理/合成 → releaseBuffer归还这条路。缓冲区在任一时刻处于四种状态之一状态含义谁拥有FREE空闲可以被生产者申请队列DEQUEUED生产者已申请正在画生产者QUEUED已画完等待消费者队列/消费者ACQUIRED消费者已取走消费者这四种状态的流转设计得很巧妙它保证了同一个缓冲区不会同时被生产者和消费者读写。你实际调优时遇到的“掉帧”“纹理等待”本质上就是某个状态卡住的时间超出了预算。值得注意的是现代AndroidAndroid 10以后的BLAST架构里BufferQueue的回调机制已经发生了改变不再通过传统的listener通知SurfaceFlinger“有新帧来了”而是App直接通过Transaction机制提交Buffer再由SurfaceFlinger按VSYNC节奏统一处理。这个细节在后面讲调度时还会展开但核心的生产-消费模型没有变。2.2 为什么缓冲区数量是2到3个而不是更多很多初学者会问既然排队会等那把缓冲区做多一点不就不等了这就是典型的“只看到延迟没看到功耗”。缓冲区数量直接决定了“App最多能提前画多少帧”缓冲区越多画面延迟越高系统为了维持流畅需要付出的调度成本也越高。Android默认在绝大多数场景使用双缓冲或三缓冲这是一个经过权衡的折中。用一组数字来说明假设屏幕是60Hz每帧预算16.6ms。双缓冲情况下App绘制完一帧后可能没有空槽必须等到SurfaceFlinger把上一帧合成完并归还缓冲区才能继续画如果绘制耗时超过16.6ms就会立刻丢掉一个VSYNC周期。三缓冲给App多了一个“提前画”的槽位允许某一帧短时间超出预算让系统靠队列消化波动。这里需要澄清一个常见误区三缓冲不是提升帧率的开关而是“用多一个缓冲区的内存换取绘制耗时抖动时的容错能力”。你开启三缓冲之后从dumpsys SurfaceFlinger里看到的总Buffer数可能是3个甚至更多但实际每一帧的延迟反而可能增加——因为App提前画出来的帧是排着队等待合成的。我见过不少团队一遇到卡顿就调大BufferQueue的maxBufferCount结果掉帧率没降触摸延迟却肉眼可见地变高了。在Android 16上这套BufferQueue机制依然是图形栈的地基只是上层做了更多异步化处理。比如BLASTBufferQueue把生产者和SurfaceFlinger的交互进一步解耦让App侧提交帧的路径更短。这些优化都是在不动地基的前提下把砖块砌得更薄。3. SurfaceFlinger的合成策略它究竟在忙什么3.1 合成Composition不是渲染Rendering我经常被问到同一个问题“SurfaceFlinger是不是负责把所有View画出来”答案是不。App侧的View绘制、Canvas绘制、OpenGL/Vulkan绘制才是真正的渲染SurfaceFlinger做的事情叫合成——它把多个已经画好的图层按照Z轴顺序、透明度、裁剪区域叠成一张最终画面。打个比方App负责把食材做成几盘菜SurfaceFlinger负责把它们摆到一张桌子上。SurfaceFlinger的合成路径有两种一种是客户端合成Client Composition也就是它自己调用GPURenderEngine把多个图层画到一个目标缓冲区里另一种是设备合成Device Composition也就是把图层列表直接交给HWC让显示硬件自己完成叠加。实际处理时往往是混合的HWC能处理的图层走硬件不能处理的图层先由GPU合成到一个图层上再把结果交给HWC。合成的开销和图层数强相关。半透明图层、带圆角裁剪的图层、带阴影的图层都会让GPU合成时的片元计算量成倍上升。你在日常开发中经常会遇到一个现象某个页面上弹出了一个带大范围阴影的悬浮窗整机帧率立刻下降原因就是SurfaceFlinger本来可以走设备合成的图层组合被打乱了被迫退回GPU合成。3.2 VSYNC调度App与SurfaceFlinger的握手协议合成不能想什么时候做就什么时候做否则会出现画面撕裂——屏幕上半部分显示新帧、下半部分还在显示旧帧。Android通过VSYNC信号来同步整条流水线每个硬件VSYNC周期有两个派生信号VSYNC-App和VSYNC-SF。前者驱动App的Choreographer回调告诉应用“你可以开始画下一帧了”后者驱动SurfaceFlinger告诉合成器“现在可以把已提交的帧合成了”。Android 16时期这套双VSYNC模型仍然存在但细节上做了不少演进比如支持可变刷新率VRR面板时系统会动态调整VSYNC频率而不是死板地锁在60Hz或120Hz。对开发者而言理解VSYNC的关键不在于记住信号怎么生成而在于明白“掉帧的真实定义”一帧画面没有在预定的VSYNC周期内被App绘制完成并提交给SF或者SF没能在下一个周期前完成合成都会导致该帧被跳过或重复显示。很多人看Systrace时有一个误区看到SF的合成时间只有2ms就认为SF很快然后去质疑App侧。实际上SF只能消费App提交的帧如果App在VSYNC到来时根本没有新帧可合成SF就会用旧帧再撑一个周期在trace上表现为一个很短的合成段一个很宽的空白真正的瓶颈在App侧。反过来如果App每帧都准点提交但SF合成时间飙到10ms以上这时候才应该考虑是图层数太多还是HWC没有接管。3.3 SurfaceFlinger的Transaction机制与帧IDAndroid 10的BLAST改造之后SurfaceFlinger接收的不再是简单的“新缓冲区通知”而是一整套Transaction状态。App端通过SurfaceControl.Transaction来设置图层的位置、大小、透明度、裁剪区域、Buffer等属性SF按帧序列号FrameNumber统一应用这些变更。这个机制让合成变得更加原子化减少中间状态的暴露。正是这个变化让现代Android能做到“等待下一个VSYNC统一提交并应用所有窗口的变更”避免了多个App在不同时间点提交图层属性变化导致的闪烁和错乱。你在dumpsys SurfaceFlinger里看到的大量Transaction信息就是这个机制的状态快照。做系统级调试时学会读这些信息比猜代码路径有用得多。4. HWC硬件合成省电与性能的真正胜负手4.1 HWC不是什么“万能显卡”HWC是hardware/interfaces/graphics/composer里定义的一套HAL接口它向上对SurfaceFlinger暴露能力向下调用显示控制器的Overlay引擎。这里必须强调HWC不是GPU不负责把3D场景画出来它的核心能力是“把多个图层在显示硬件级别直接叠加”。显示控制器通常有几个Overlay Plane每个Plane可以接收一个独立图层硬件可以把这些图层按Z序叠加后直接输出到屏幕整个过程不需要GPU参与也不需要把多个图层先画到一张中间纹理上。这种“硬件直接叠”的方式优势很明显省电、低延迟、CPU/GPU占用几乎为零。视频播放、游戏画面、相机预览这些场景之所以流畅且耗电低就是因为走的是HWC设备合成。你可以把HWC想象成会议室里的矩阵切换器几路信号直接在输出端叠加中间不经过任何转码。4.2 合成决策哪些层走硬件哪些层走GPUSurfaceFlinger每帧都要向HWC提交一个候选图层列表HWC反馈“这些层我能直接叠那些层我搞不定”。搞不定的层会被标为Client Layer由SF用RenderEngineGPU先合成到一个专门的client target缓冲里再将这个缓冲作为一层交给HWC。这个决策过程每一帧都在进行受很多因素影响图层数量HWC的Overlay Plane数量是固定的常见是2到4个超过上限的图层只能合并成client层。图层格式某些硬件不支持任意格式的图层做Overlay比如带硬件旋转、带特殊压缩格式的层。显示区域重叠/半透明HWC处理不了复杂的混合blend需求Alpha混合容易整层退回GPU。帧缓冲约束比如HDR、宽色域、局部刷新DRM等能力不支持时也会回退。列一个实际场景你就能直观感受到这个决策的破坏力一个全屏VideoView播放视频1个视频层加上状态栏、导航栏2个系统层如果HWC有3个Plane正好全部设备合成。这时候你突然在屏幕上弹了个半透明悬浮窗图层变成4个Plane不够用了。SurfaceFlinger可能被迫把“状态栏悬浮窗导航栏”三个层合成成一层GPU合成耗时从0.5ms变成2ms整机流畅度立刻受牵连。所以系统优化时“图层管理”非常重要一些桌面团队专门做“图层合并优化”把多个小组件合并到一个独立的Surface上就是为了降低SF的合成压力。HWC接口发展到Composer 3.0以后还加入了更多能力协商和性能提示机制比如DisplayCapabilities、PerFrameMetadata等。Android 16的图形适配中厂商需要重点关注的还是自家的Plane数量和混用规则这直接影响整机的场景化功耗表现。5. Android 16时代这套架构的演进方向5.1 从OpenGL ES到Vulkan/ANGLE的结构性迁移讨论Android 16的图形架构绕不开一个问题OpenGL ES正在被“移出”主舞台。Android已经将ANGLEAlmost Native Graphics Layer Engine作为OpenGL ES在部分设备上的默认实现ANGLE会把OpenGL ES的API调用翻译成Vulkan来执行。这意味着即使应用还在用OpenGL ES写渲染代码底层驱动实际执行的很可能是Vulkan。这个变化对架构的影响很深远。Vulkan是一个显式控制GPU的API驱动承担的逻辑更少、应用承担的逻辑更多所以在老旧的OpenGL ES驱动上表现不稳定的场景换到ANGLEVulkan之后往往更一致。但这也带来新的兼容问题Shader的精度差异、同步行为不同、资源绑定方式不同都可能让原本“能用”的GL代码出现视觉差异。我在实际项目中就遇到过某个App开启ANGLE后文字描边发虚的情况最终定位到是特定GPU驱动对shader中mediump精度的处理方式与GLES原生效能不同。Android图形栈之所以坚定走向Vulkan根本原因是为了减少驱动负担、提高跨设备一致性。这个迁移对系统开发者来说意味着排查渲染问题时的工具链要跟着变Adreno工具、Mali Offline Compiler这些针对厂商驱动的调试方式要逐步让位于更通用的Vulkan层检查。5.2 刷新率、帧预测与HDR合成的新变量早在Android 11就开始支持可变刷新率VRRAndroid 16上显示系统对这个能力的抽象更成熟了。当屏幕支持48Hz到120Hz的动态切换时VSYNC不再是固定频率SF需要依据当前场景的内容移动速度来决定“下一帧用多高的刷新率输出”。这个决策涉及功耗和流畅度的权衡算法通常放在显示系统或厂商的Display HAL里。另一个值得关注的演进是帧预测Frame Prediction。在120Hz屏幕上如果App只按60FPS生成内容显示系统可以通过“智能预测”在两帧之间插入一帧预测画面从而把视觉流畅度提升到接近120FPS的观感。这个技术的难点在于预测的准确性——插错了就会产生鬼影。目前这项能力更偏向系统级优化但未来开放给应用接口后游戏和视频类应用可以直接受益。HDR合成方面现代Android要求合成器能够处理SDR和HDR图层混叠的场景同一个屏幕上普通界面是SDR亮度视频窗口是HDR亮度合成器必须对不同亮度域的图层做正确处理通常涉及色调映射Tone Mapping和亮度调节。Android 16在这一块的架构没有推翻重来但HWC对HDR元数据的透传能力、SF对混合亮度域的处理逻辑都在持续细化。这类问题普通应用开发者感知不强却是厂商适配HDR显示时必须过的关卡。6. 拿着架构图去定位问题的实战思路6.1 我排查图形问题的固定顺序架构是理论落到实际还得靠工具。这些年我处理图形性能问题时基本遵循一个从“产出端”到“消费端”的顺序先确认App侧是否按VSYNC周期产出帧执行adb shell dumpsys gfxinfo package看Total frames rendered、Janky frames、50th percentile这些指标。如果App卡顿明显去systrace里看Choreographer回调DoFrame有没有超时、draw/measure有没有卡在特定方法上。再确认SF侧是否按时完成合成adb shell dumpsys SurfaceFlinger --latency可以看到每个Surface的帧时间戳如果App侧每帧都准点但SF侧出现FrameSpacing波动说明问题在合成侧。最后看HWC的参与度adb shell dumpsys SurfaceFlinger --list和--layer可以看到每个图层的合成状态重点看是Device还是Client Composition。如果预期走设备合成的场景大面积退回客户端合成说明图层结构或者HWC兼容性出了问题。这套顺序有一个核心逻辑“谁该为这一帧的延迟负责从数据流的上游开始找”。很多新手习惯直接打开systrace全局搜索看到什么红就点哪里这样容易把App侧的慢和SF侧的慢混在一起。下面这个表是我自己整理的高频命令和用途读者可以直接存下来命令关键信息适合场景dumpsys gfxinfo帧耗时、掉帧统计、jank数据应用渲染性能dumpsys SurfaceFlinger --latencySurface帧时间戳、间隔SF合成节奏dumpsys SurfaceFlinger --layer图层属性、合成方式图层结构分析dumpsys SurfaceFlinger --list当前所有Surface查看可见窗口/图层dumpsys displayDisplay状态、刷新率模式刷新率/VRR问题adb shell screenrecord --bugreport录屏元数据复现花屏/闪烁问题6.2 架构图之外的三个实操忠告第一个忠告不要只盯着SurfaceFlinger找卡顿。我接手过一个项目现象是桌面滑动卡顿初步抓systrace发现SF合成时间偏高于是一群人在SF的合成算法里找了一周。后来我改了排查思路先看App侧提交帧的时间戳发现桌面的Launcher进程在特定场景下会突然出现一个90ms的绘制长帧而SF只是被迫把一个晚到的帧合成了。问题根源在Launcher的一个布局计算上跟SF一点关系都没有。第二个忠告图层数量是你最值得优化的性能指标。不管HWC有多强每多一个图层就多一分合成压力尤其是在有半透明效果、圆角裁剪、阴影的场景。应用开发时能合并的Surface尽量合并能用ViewStub延后创建的Surface不要提前创建系统开发时悬浮窗、Toast、权限弹窗这类临时图层用完之后必须及时释放。我见过某个ROM的录屏功能因为持有一个全屏高分辨率Surface不被释放导致后续所有App的合成都多了一个高开销图层整机功耗直接上去。第三个忠告读dumpsys报告时先关心“变化量”不要只看绝对值。SurfaceFlinger的dumpsys输出非常庞大动辄几千行。有经验的人不会逐行读而是先在交互正常和异常时各抓一份用diff找出变化的图层、变化的合成方式、变化的帧间隔。比如某次升级后出现花屏对比两份dumpsys很可能会发现某个图层的transform从0变成了某个旋转值这通常意味着App侧SurfaceControl的几何变换设置出了问题。这种“差分定位法”比在源码里靠猜高效得多。说实话Android图形系统这套架构从BufferQueue到SurfaceFlinger再到HWC核心骨架已经有很多年没有根本性推翻了。它就像一栋老房子每一代Android都在里面改水电、换门窗但承重墙一直没动过。正因为如此搞懂这套架构模型的投入产出比非常高——你在这里花的时间在未来很长一段时间里都能用上。这篇文章给的是一张全景地图后面几篇我打算沿着这条链路往深处走逐个环节展开讲BufferQueue的深度调优、SF的事务处理模型、HWC的合成决策算法这些都是实操中真正会踩坑的地方。先把地图记牢我们一步一步来。