很多人第一次接触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层有一个容易被忽略但极其重要的角色——HWC(Hardware 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内部都绑定一个BufferQueue,App通过dequeueBuffer拿到一块可写的GraphicBuffer,绘制完成后queueBuffer把它交回队列。SurfaceFlinger那头则通过acquireBuffer取走这张Buffer合成。
这里有个非常重要但经常被误解的参数——Buffer的数量。很多人以为双层buffer就够了,其实Android通常使用三缓冲来平衡延迟和吞吐量。多一块buffer意味着生产者不用每次都等消费者释放buffer,可以提前准备下一帧,这样才能在保持流畅的同时不增加太多内存占用。
关于buffer状态的流转,用一张表看更清楚:
| Buffer状态 | 持有者 | 说明 |
|---|---|---|
| Free | BufferQueue | 未使用,可被dequeue |
| Dequeued | App | App正在填充像素数据 |
| Queued | BufferQueue | App已完成绘制,等待消费 |
| Acquired | SurfaceFlinger或消费者 | 正在被合成或处理 |
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、120Hz),VSYNC就按这个节拍发出脉冲。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把“我希望合成这些图层”的请求发给HWC,HWC根据硬件能力决定哪些图层能吃进硬件Overlay,哪些必须交给GPU。
这本质是一场“讨价还价”。SurfaceFlinger先默认让HWC处理尽可能多的图层,HWC一旦发现吃不消,就返回“我只能处理其中几个,剩下的你自己用GPU画”。于是SurfaceFlinger完成GPU合成后,把结果再交给HWC,HWC再做一次最终上屏组合。
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的人都在脑子里存一份。