
很多人第一次接触Android显示链路都是因为线上遇到一个诡异问题画面偶尔闪一下、滑动卡顿、GPU占用莫名偏高。定位了一圈代码没问题、布局没问题最后才发现是某个图层一直在触发GPU合成。要理解这类问题只有把“Android显示完整链路”整个打通——从App里的View到系统合成再到屏幕像素中间每一站都可能成为瓶颈。这篇文章我就按一条完整的链路来拆App侧怎么把UI变成数据系统侧怎么把多块图层合在一起硬件侧怎么把合成结果送到屏幕以及每一站的排查入口在哪。适合刚入门的Android开发也适合干了几年但一直没把Framework和硬件层串起来的同学。1. 先建立一张全局图显示链路到底跨了几层在动手看代码之前先搞清楚整体架构。Android显示链路可以粗暴地拆成四层应用层、系统服务层、HAL硬件抽象层、内核与硬件层。每一层负责的事情截然不同但它们之间环环相扣。应用层主要干的是“把UI画成可传输的数据”。你在Activity里写的XML布局、自定义View的onDraw最终都会被变成GPU能理解的绘制指令和纹理数据。Android 4.0之后默认开启硬件加速这个“变成”的过程大量依赖GPU完成。系统服务层是SurfaceFlinger的主场。它做的事情很像一个“桌面合成器”手机屏幕上同时有状态栏、桌面、App窗口、输入法窗口这些内容分别属于不同的Surface,SurfaceFlinger按照Z轴顺序把所有Surface合成成一整帧画面再交给显示设备。HAL层有一个容易被忽略但极其重要的角色——HWCHardware Composer。它的价值在于用硬件模块分担SurfaceFlinger的合成工作。有些场景适合GPU合成有些场景直接让Display Controller硬件拼接更省电HWC就是在这个决策过程中充当“翻译官”和“分诊台”。最底层就是内核的DRM/KMS显示驱动以及物理屏幕面板。这一层关系到帧缓冲、时序、刷新率等硬核参数。到了这一层所有处理过的图像数据最终变成像素点阵按固定的刷新节奏点亮屏幕。下面这张表可以帮你快速定位“你当前遇到的问题通常属于哪一层”层次关键模块主要输出/职责常见问题类型应用层ViewRootImpl、HWUI、RenderThread生成DisplayList与纹理数据布局过深、绘制过重、掉帧系统服务层WindowManagerService、SurfaceFlinger管理窗口层级、合成所有Surface图层错乱、合成性能差、闪屏HAL层HWC编排合成策略、协商显示参数合成策略异常、硬件不支持内核与硬件DRM/KMS、Display Controller、屏幕面板输出帧数据到物理屏幕刷新率异常、撕裂、烧屏心里有了这张图后面的每一层我们都能聊出点实操细节。2. App进程内从View到GPU纹理的接力赛2.1 传统绘制流程的“三座大山”几乎所有讲绘制的文章都会提到measure、layout、draw三步但很多人没意识到这三步和“显示链路”之间隔着一层重要的东西——Recording。Android硬件加速开启后View的onDraw里执行的Canvas操作并不会直接往屏幕上画而是被记录到一个叫DisplayList的结构里。这个DisplayList像一份“绘制命令清单”GPU后续照着这份清单干活。拿一个最简单的Button来说measure决定它多大layout决定它在哪draw往DisplayList里塞进“画一个圆角矩形”“写一段文字”“贴一张图标”这些命令。真正把它们变成像素是RenderThread的事。RenderThread是Android 5.0之后引入的独立线程专门负责执行DisplayList渲染。它的存在让UI线程和渲染工作并行UI线程继续处理输入和布局RenderThread专心排队提交渲染任务。很多人只盯主线程的卡顿却忽略了RenderThread里也可能有瓶颈比如某个DrawOp太复杂导致GPU单帧耗时过长。2.2 BufferQueue渲染产物的“传送带”App这边渲染出来的画面不是直接送进屏幕的而是先放进一个叫BufferQueue的队列。这是经典的生产者-消费者模型。App侧作为生产者不断生产填满图像数据的Buffer系统侧消费这些Buffer去做合成。每个Surface内部都绑定一个BufferQueueApp通过dequeueBuffer拿到一块可写的GraphicBuffer绘制完成后queueBuffer把它交回队列。SurfaceFlinger那头则通过acquireBuffer取走这张Buffer合成。这里有个非常重要但经常被误解的参数——Buffer的数量。很多人以为双层buffer就够了其实Android通常使用三缓冲来平衡延迟和吞吐量。多一块buffer意味着生产者不用每次都等消费者释放buffer可以提前准备下一帧这样才能在保持流畅的同时不增加太多内存占用。关于buffer状态的流转用一张表看更清楚Buffer状态持有者说明FreeBufferQueue未使用可被dequeueDequeuedAppApp正在填充像素数据QueuedBufferQueueApp已完成绘制等待消费AcquiredSurfaceFlinger或消费者正在被合成或处理2.3 一个典型的应用层掉帧是怎么发生的假设页面滚动手势触发后主线程执行onDraw耗时过长DisplayList迟迟闭不了盘RenderThread拿不到新的渲染任务BufferQueue里的buffer空转SurfaceFlinger因为缺少新帧只能在旧帧上重复合成。屏幕上表现出来就是掉帧、卡顿。排查这类问题我建议第一时间用dumpsys gfxinfo 包名看帧耗时。如果draw阶段耗时高问题在应用自身代码如果sync/upload阶段高问题可能在纹理上传和GPU交互。这类现场数据比空谈理论有用得多。3. SurfaceFlinger与VSYNC系统侧如何把多块图层合成一帧3.1 窗口层级与可见区域计算SurfaceFlinger手上管着几十个Surface。每个Surface除了图像内容还携带一系列几何属性位置、尺寸、透明度、旋转角度、裁剪区域等。这些属性加在一起才构成“这一个图层在最终画面上长什么样”。每次合成时SurfaceFlinger要做一次“全局排序”。比如状态栏Surface的Z值最高App Surface排在中间壁纸Surface垫底。还有一个关键步骤叫“可见区域计算”——如果上面的图层盖住了下面的区域SurfaceFlinger就没必要对遮挡区域做合成。Android的SurfaceFlinger在这块做过大量优化无效区域越早剔除GPU和HWC的负载越低。3.2 VSYNC整个显示系统的“心跳节拍”VSYNC垂直同步是Android显示链路里最重要的时钟信号。屏幕以固定频率刷新比如60Hz、90Hz、120HzVSYNC就按这个节拍发出脉冲。Android把VSYNC分成两个关键时间点App VSYNC用来触发应用开始绘制新帧SF VSYNC用来触发SurfaceFlinger开始合成。Choreographer在应用层扮演了“领跑员”的角色。它注册VSYNC回调安排measure/layout/draw在下一帧开始前完成。这就是为什么你在主线程里post回调时它不一定是立即执行而是等下一个VSYNC信号。理解了这个“节拍”你就理解了很多卡顿优化方案背后的逻辑——比如把耗时操作移出主线程不是为了“让代码跑得快”而是为了“让那一帧的VSYNC回调不超时”。3.3 合成决策GPU合成还是硬件合成SurfaceFlinger拿到所有可见的Surface Buffer后需要决定用哪种方式合成。第一种是GPU合成Client合成。SurfaceFlinger通过OpenGL ES将所有图层渲染到一块目标Buffer里。这种方式灵活但耗电因为GPU全程参与。第二种是硬件合成Device合成。SurfaceFlinger把图层信息直接发给HWC由显示控制器的硬件Overlay Plane直接完成图层叠加GPU几乎不干活功耗最低。什么时候走GPU合成、什么时候走硬件合成由HWC帮SurfaceFlinger做决策但在某些情况下HWC也会“失败退回”。比如图层带有复杂圆角或遮罩效果硬件Overlay不支持只能回到GPU合成。实际项目里经常碰到的性能优化很多都围绕“尽量让更多Surface走Device合成”。如果你发现某个动画让GPU负载直线上升先检查是不是动画图层的合成策略从Device退化成了Client。4. HWC与Display像素上屏前最后的关键一跳4.1 HWC如何“讨价还价”协商合成方案HWC的文章在普通技术博客里相对少但它在链路里的地位不亚于SurfaceFlinger。HWC通过HAL接口向上层暴露能力支持多少个硬件图层、是否支持旋转、是否支持缩放。SurfaceFlinger把“我希望合成这些图层”的请求发给HWCHWC根据硬件能力决定哪些图层能吃进硬件Overlay哪些必须交给GPU。这本质是一场“讨价还价”。SurfaceFlinger先默认让HWC处理尽可能多的图层HWC一旦发现吃不消就返回“我只能处理其中几个剩下的你自己用GPU画”。于是SurfaceFlinger完成GPU合成后把结果再交给HWCHWC再做一次最终上屏组合。4.2 Display管线的现代面貌与帧提交到了Android 8.0之后默认使用的是DrmDisplayComposer它直接面向内核DRM模块。Display控制器负责把帧内容按照时序信号输出到屏幕面板。现代智能手机多用MIPI DSI接口连接SoC和屏幕面板刷新过程本质上就是一个持续不断的Buffer切换。这里得提一下“前后台buffer”的概念屏幕当前显示的画面来自“前台buffer”正在准备显示下一帧的渲染结果写在“后台buffer”。当VSYNC到来驱动把后台buffer和前台buffer做一次交换flip屏幕上立刻出现新画面。这个交换动作和SurfaceFlinger的合成节奏必须严格对齐否则会出现画面撕裂tearing。4.3 从帧数据到光信号像素点亮的过程很多人把“显示”等同于“绘制”其实从framebuffer到屏幕点亮中间还经过图像处理单元DPU/ISP的加工。这个模块负责色调映射、色彩管理、背光调节等。像素数据从RGB数值变成屏幕上的光就是靠这一层的输出。你在开发时设置的windowBrightness、系统夜间模式、HDR色调映射最终都在这一层生效。到了这一步完整链路才算真的闭合App画图——BufferQueue传帧——SurfaceFlinger合成——HWC编排——Display Controller输出到面板。5. 链路视角下的排查方法从现象快速定位瓶颈5.1 从dumpsys输出看链路每一站的实时状态排查显示问题时我习惯按这条链路逐层取证。最常用的三把工具是dumpsys SurfaceFlinger、dumpsys gfxinfo和systrace。先看dumpsys SurfaceFlinger里每层Surface的Buffer状态。如果大量Buffer长期处于Queued状态没人消费说明SurfaceFlinger合成速度跟不上生产如果有些Surface长期没有更新但还在参与合成那它可能正在浪费GPU资源。再看dumpsys gfxinfo 包名的帧数据。重点看“Draw”“Prepare”“Process”“Execute”四项耗时分布。其中Execute阶段和GPU执行有关如果耗时偏高往往是Shader太复杂或者纹理过大。最后用systrace或Perfetto抓一帧完整时间线把App线程、RenderThread、SurfaceFlinger、HWC串在同一时间坐标上。你能精确看到是App的VSYNC回调晚了还是SurfaceFlinger合成慢了还是HWC提交返回晚了。这些信息互相印证才能准确锁定罪魁祸首。5.2 实战中三个常见的“链路背锅侠”无意义的全屏模糊背景某个页面用了很大的Bitmap做高斯模糊背景mono的纹理上传和处理在RenderThread上占掉小十毫秒。明明没有动画需求却把整条链路的合成压力拉高了。解决办法是压缩模糊图尺寸或者改用自定义Shader实时模糊。隐藏Surface未释放弹窗关闭后Surface没有及时销毁SurfaceFlinger还在维护一堆不可见图层导致GPU合成面积不降反升。查一下WindowManager是否及时做了移除清理。非整帧失效动画某个View动画只改动了一小块区域但因为它的Surface设置了全透明或旋转属性导致整块图层都无法走硬件Overlay合成。这个在HWC的layer类型里能看到。5.3 我个人的排查习惯我个人的经验是不要在没取证前就动手改代码。先用systrace抓一次复现现场把整条链路的时间轴拉出来确认问题到底发生在哪一段再做针对性修改。改完再用同一套指标测对比帧耗时、Jank数量、GPU占用三项数据。链路问题很多时候不是单点原因改完App侧可能还要调整SurfaceFlinger侧的窗口参数甚至要重新评估图层的合成策略。6. 一张图背后的读图方法文章标题叫“一张图看懂Android显示完整链路”最后说说这张“图”应该怎么读。如果你自己画这张图我建议从左往右画一条主流水线最左边是App进程节点是ViewRootImpl、Choreographer、RenderThread箭头指向BufferQueue。中间是系统服务区SurfaceFlinger接收多个BufferQueue的输入下方标注VSYNC信号右侧输出合成帧。再往右是HWC节点它和SurfaceFlinger之间有一条双向协商箭头。最右边是DRM/KMS、Display Controller和屏幕面板箭头终点是“像素点亮”。读图时记住三个核心关系VSYNC是全局时钟BufferQueue是跨进程搬运工HWC是省电开关。任何时候遇到显示性能问题先在这张图上定位“数据停在哪个节点”再对症下药。读这张图还有一个诀窍每一层关注的核心指标不同。App层看帧耗时和VSYNC回调延迟系统层看Surface数量与合成策略硬件层看HWC layer类型和显示时序。把指标对到图上问题往往一眼就能看出来。我自己当初也是从“只知道setContentView”的阶段走过来的直到第一次用systrace把App和SurfaceFlinger的时间轴对在一起才真正懂“你看到的屏幕上那一帧是整个系统协同工作的结果”。这张图值得每个做Android的人都在脑子里存一份。