
1. 从一次花屏说起为什么需要理解显示链路几年前我在调试一台车机项目时遇到过一个非常典型的问题应用层播放视频一切正常但倒车影像切进来之后画面会出现大约半秒的撕裂偶尔还伴随一次黑闪。当时团队里有人怀疑是解码器的问题有人怀疑是摄像头驱动折腾了两天没结果。后来我把dumpsys SurfaceFlinger的输出抓下来逐帧比对才发现问题出在图层合成的合成策略切换上——倒车影像作为一个新的图层加入时合成方式从 GPU 合成临时切到了硬件合成器而这次切换没有做好缓冲同步。这件事让我彻底意识到Android 显示链路不是一个应用画完就完事的黑盒而是一条从应用到屏幕、跨越四五个进程和多个硬件模块的完整流水线。任何一个环节的时序、格式、同步机制出问题最终都会以花屏、撕裂、卡顿、黑闪的形式呈现在用户眼前。这篇内容就是想把这条链路完整地拆开讲清楚。它适合三类人一是做应用开发但总被为什么我的动画掉帧困扰的工程师二是刚接触 Framework、想搞明白SurfaceFlinger到底在干什么的初学者三是做车机、TV、平板等定制系统的同行需要定位显示相关的疑难杂症。我不会只给你一张干巴巴的框图而是把每个环节为什么存在数据怎么流动出问题怎么查都讲透。读完你至少能做到看到花屏能判断大概在哪一层抓到dumpsys输出能看懂关键字段知道SurfaceFlinger、HWC、DRM各自负责什么。2. 显示链路的整体分层从应用到像素的六层结构2.1 一张图背后的分层逻辑如果真要用一张图概括 Android 显示链路它应该是纵向分层的从上到下大致是应用层 → 系统服务层WindowManager/SurfaceFlinger→ 合成层HWC/GPU→ 内核显示驱动层DRM/KMS→ 硬件接口层MIPI/DP/HDMI→ 物理屏幕。很多人第一次看这个分层会觉得是不是太复杂了但每一层的存在都有它的必然性。应用层负责画什么它不关心屏幕有几个、刷新率多少系统服务层负责谁来画、画到哪块缓冲区它要协调多个应用争抢屏幕合成层负责怎么把多个图层拼成一张图因为现代界面几乎不可能是单个应用独占全屏内核驱动层负责把拼好的图送到正确的硬件通道因为一块板子上可能挂着 LCD、HDMI、DP 多个显示设备。理解这个分层的关键在于每一层只解决自己那一层的问题层与层之间通过明确的契约缓冲区、格式、时序交互。当你遇到显示问题时第一步永远是判断问题出在哪一层而不是盲目改代码。2.2 各层的核心职责与关键对象我把每一层的核心职责和对应的关键对象整理成一张表方便你建立整体印象层级核心职责关键对象/模块典型问题应用层绘制内容到 SurfaceCanvas、OpenGL ES、Vulkan、Surface掉帧、绘制错误窗口管理管理窗口层级与位置WindowManager、SurfaceControl层级错乱、位置偏移合成服务决定图层如何合成SurfaceFlinger、Layer合成策略异常硬件合成硬件完成图层叠加HWC、Overlay合成能力不足内核显示管理显示控制器与通道DRM/KMS、CRTC、Plane时序错误、黑屏硬件接口物理信号传输MIPI DSI、DP、HDMI信号不稳、闪屏这张表建议你记在心里。后面每一节我都会围绕其中某一层展开讲清楚它的内部机制和排查手段。2.3 为什么完整链路比单点知识更重要我见过太多工程师只懂自己那一亩三分地应用开发只关心Choreographer回调Framework 开发只关心SurfaceFlinger的合成逻辑驱动开发只关心DRM的atomic commit。结果就是一旦问题跨层就互相甩锅。举个真实例子应用反馈滑动卡顿Framework 说合成没问题驱动说时序正常。最后发现是应用在一个Surface上频繁resize导致SurfaceFlinger反复重建缓冲区而每次重建都触发一次HWC的 validate 失败最终走了 GPU 合成路径多出来的拷贝造成了卡顿。这个链条上任何一环单独看都正常但连起来就是问题。所以这篇内容的核心价值不是教你某个 API 怎么用而是帮你建立跨层的因果推理能力知道一个现象可能由哪些层的哪些机制引起知道怎么用工具逐层排除。3. 应用侧Surface 是怎么被画出来的3.1 从 View 到 Surface 的绘制路径应用层绘制的起点通常是View树的draw调用但真正和显示链路对接的是Surface。一个Activity对应一个Window一个Window在WindowManagerService里注册后会创建一个SurfaceControl进而关联到一个Surface。应用通过Canvas软件绘制或OpenGL ES/Vulkan硬件加速绘制往这个Surface上写内容。这里有个容易被忽略的点应用并不是直接往屏幕的帧缓冲上画而是往一块由BufferQueue管理的图形缓冲区上画。BufferQueue是 Android 图形架构里最核心的生产者-消费者模型应用是生产者SurfaceFlinger是消费者。应用dequeue一块空闲缓冲区画完queue回去SurfaceFlinger再acquire出来合成。这个模型解释了为什么应用掉帧不一定会导致屏幕卡顿——只要SurfaceFlinger手里还有上一帧的缓冲区它就能继续合成。但反过来如果应用queue太快缓冲区耗尽应用就会被阻塞表现为卡住。3.2 三重缓冲与帧同步的实际影响BufferQueue的缓冲区数量是可以配置的常见的是双缓冲和三缓冲。三缓冲的意义在于当某一帧合成耗时较长时应用还能继续往第三块缓冲区上画不至于立刻被阻塞。这在滑动、动画场景下能明显减少掉帧。但三缓冲不是银弹。它增加了内存占用也增加了一帧的延迟。在车机倒车影像这种对延迟极度敏感的场景我通常会建议把相关Surface调成双缓冲牺牲一点流畅度换更低的延迟。这个取舍没有标准答案取决于你的场景。实测中还有一个坑很多应用在Surface尺寸变化时没有正确处理缓冲区。比如分屏切换、旋转屏幕时如果应用没有及时响应surfaceChanged回调继续往旧尺寸的缓冲区上画就会出现拉伸或黑边。这类问题在dumpsys SurfaceFlinger里能看到图层的buffer size和frame size不一致。3.3 应用侧排查的常用手段应用侧最实用的工具是Choreographer的帧回调日志和gfxinfo。adb shell dumpsys gfxinfo package能给出每一帧的绘制耗时分布包括Draw、Prepare、Process、Execute四个阶段。如果Draw阶段耗时高说明是应用自己的绘制逻辑重如果Execute高可能是 GPU 负载问题。提示gfxinfo的帧统计默认只保留最近 120 帧左右排查偶发卡顿时要先reset再复现否则数据会被历史帧污染。另外adb shell dumpsys SurfaceFlinger --latency layer name能拿到某个图层的帧提交时间戳用来判断应用是否真的在稳定出帧。这个命令在排查应用说画了但屏幕没更新这类问题时特别有用。4. SurfaceFlinger合成服务的中枢逻辑4.1 SurfaceFlinger 到底在做什么SurfaceFlinger是 Android 显示链路的中枢。它的核心工作可以概括为三件事收集图层、决定合成方式、提交合成结果。每一帧SurfaceFlinger会遍历当前所有可见的Layer检查哪些图层有新的缓冲区、哪些图层的属性位置、透明度、裁剪发生了变化。然后它调用HWC的prepare接口让硬件合成器判断哪些图层它能直接合成、哪些需要 GPU 先处理。根据HWC的返回结果SurfaceFlinger决定这一帧走硬件合成还是 GPU 合成最后通过HWC的set接口提交。这个过程每秒要重复 60 次甚至 120 次所以效率极其关键。SurfaceFlinger内部有大量的缓存和优化比如只在图层变化时才重新计算合成策略比如用fence来做跨进程的缓冲区同步。4.2 Layer 的生命周期与合成策略一个Layer从创建到销毁会经历几个关键状态创建时注册到SurfaceFlinger获得一个Layer句柄有缓冲区时进入可见候选被HWC接受时进入硬件合成被拒绝时回退到 GPU 合成销毁时从图层列表移除。合成策略的切换是很多显示问题的根源。HWC对能硬件合成的图层有严格限制比如图层数量上限、格式要求、缩放比例限制。一旦某个图层不满足条件整个合成可能被迫走 GPU 路径带来额外的拷贝和功耗。我遇到过最隐蔽的一个案例某个定制 ROM 里状态栏图层的格式被改成了HWC不支持的格式导致每一帧状态栏都要 GPU 合成。表面上看不出问题但功耗比正常情况高了 15%。后来通过dumpsys SurfaceFlinger里的Composition Type字段才发现状态栏一直是Client合成。4.3 用 dumpsys 读懂合成现场adb shell dumpsys SurfaceFlinger是排查显示问题的第一把钥匙。它的输出很长但关键信息集中在几个部分Display 信息分辨率、刷新率、当前合成方式。Layer 列表每个图层的名称、Z 序、可见性、缓冲区格式、合成类型。HWC 信息硬件合成器的能力、当前接受的图层。重点看Composition Type字段Device表示硬件合成Client表示 GPU 合成。如果本该硬件合成的图层显示为Client就要查为什么HWC拒绝了它。常见原因包括图层格式不支持、缩放比例超出范围、图层数量超过上限、有透明区域需要混合。注意不同 Android 版本dumpsys SurfaceFlinger的输出格式差异较大Android 10 之后引入了--list参数可以更清晰地列出图层。排查前先确认你的系统版本别拿旧版本的字段去套新版本。5. HWC 与 GPU 合成两条路径的取舍5.1 HWC 的职责边界HWCHardware Composer是显示硬件的抽象层它把图层叠加这件事交给显示控制器去做。显示控制器通常有多个Plane图层通道每个Plane可以独立读取一块缓冲区然后在扫描输出时实时叠加。这个过程不占用 GPU功耗低、延迟低。但HWC的能力是有限的。一块典型的显示控制器可能只有 4 到 8 个Plane且对格式、缩放、旋转有各种限制。当图层数量或属性超出能力时HWC会告诉SurfaceFlinger这些图层我处理不了你先用 GPU 合成成一张图再给我。这就是 GPU 合成路径。5.2 GPU 合成的代价与触发条件GPU 合成意味着SurfaceFlinger要用 OpenGL ES 把所有需要 GPU 处理的图层渲染到一张离屏纹理上再把这张纹理交给HWC作为一个图层显示。多出来的这一步会带来GPU 负载增加、内存带宽占用增加、一帧的额外延迟。触发 GPU 合成的常见条件我整理如下触发条件说明规避思路图层数量超限超过 HWC Plane 数量合并图层、减少悬浮窗格式不支持如某些 YUV 格式统一用支持的格式缩放比例超限超出硬件缩放范围应用侧预缩放旋转角度特殊非 0/90/180/270避免任意角度旋转透明混合复杂需要复杂 blend简化透明度层级理解这张表的意义在于很多性能问题其实是合成策略问题。你在应用层优化半天绘制结果发现瓶颈在合成路径上那就白费功夫了。5.3 合成路径的实测对比我在一台中端车机上做过对比测试同一个导航界面正常情况走硬件合成整机功耗约 3.2W人为增加两个悬浮图层让合成回退到 GPU 路径后功耗升到 4.1W帧率还从 60 掉到 52。这个差距在车载这种功耗敏感场景里是必须重视的。所以我的经验是做定制系统时一定要把关键界面的合成路径确认一遍。用dumpsys SurfaceFlinger看Composition Type确保核心界面走的是Device合成。如果发现回退优先从图层数量和格式入手优化而不是去调 GPU 频率。6. DRM/KMS内核里的显示控制器管理6.1 DRM 框架的核心概念到了内核层显示相关的核心框架是DRMDirect Rendering Manager其中的KMSKernel Mode Setting负责显示模式设置。KMS里有几个关键对象CRTC显示控制器负责扫描输出、Encoder编码器把像素转成信号、Connector连接器对应物理接口如 HDMI、Plane图层通道对应 HWC 的 Plane。SurfaceFlinger通过HWC的 HAL 接口最终会调用到DRM的atomic commit把一组图层配置原子性地提交给显示控制器。所谓原子性是指这一组配置要么全部生效要么全部不生效避免出现中间状态导致的闪烁。6.2 显示时序与刷新率的底层逻辑CRTC的核心工作是按照一定的时序扫描输出像素。这个时序包括行同步、场同步、前后沿等参数。这些参数由显示设备的 EDID 或设备树决定。如果时序配错轻则画面偏移重则黑屏。刷新率也是在这一层设置的。高刷新率屏幕90Hz、120Hz需要CRTC以更高的频率扫描。Android 从 11 开始引入了可变刷新率VRR支持允许根据内容动态调整刷新率。但 VRR 的实现依赖DRM和HWC的配合如果 HAL 没实现好就会出现刷新率切换时的闪烁。6.3 内核层排查的入口内核层排查主要靠dmesg和debugfs。dmesg里能看到DRM的初始化日志、模式设置日志、错误信息。/sys/kernel/debug/dri/下能看到每个CRTC和Plane的当前状态。如果遇到黑屏第一步是确认CRTC是否在正常工作。可以读/sys/kernel/debug/dri/0/state看CRTC的active状态和当前绑定的Plane。如果CRTC不 active说明模式设置没成功要查Encoder和Connector的链路。提示调试DRM问题时drm.debug内核参数非常有用。加上drm.debug0x1e可以打开详细的调试日志能看到每一次atomic commit的完整参数。7. 跨层排查实战一次撕裂问题的完整定位过程7.1 问题现象与初步判断回到开头提到的倒车影像撕裂问题。现象是正常界面切换倒车影像时画面出现约半秒的横向撕裂偶尔黑闪一次。初步判断撕裂通常和垂直同步VSync有关黑闪则可能是缓冲区没准备好。第一步我先抓了dumpsys SurfaceFlinger对比切换前后的图层状态。发现倒车影像图层加入时Composition Type从Device变成了Client也就是合成路径发生了切换。这解释了为什么会有额外延迟但还不能完全解释撕裂。7.2 逐层排除的过程接着我查了HWC的日志发现倒车影像图层的格式是YUV420而当时HWC对这个格式的支持有 bug导致它拒绝了硬件合成。这是合成路径切换的直接原因。然后我查了DRM的atomic commit日志发现切换瞬间有一次commit失败重试。这次重试导致了时序上的一个空档正好对应黑闪。最后确认应用侧在切换时没有等待Surface的frameAvailable回调就提交了缓冲区导致第一帧数据不完整这是撕裂的直接来源。7.3 修复方案与验证修复分三步一是让HWC正确支持YUV420格式这是驱动侧的修复二是应用侧在切换时等待缓冲区就绪再提交三是在SurfaceFlinger侧增加合成路径切换时的缓冲同步。修复后我用dumpsys SurfaceFlinger --latency连续抓了 500 帧的时间戳确认切换期间没有丢帧撕裂和黑闪都消失了。这个案例的价值在于一个表面现象背后往往是多层问题的叠加只改一层解决不了。8. 几个容易踩的坑与个人经验8.1 别把 dumpsys 输出当成万能钥匙dumpsys SurfaceFlinger很好用但它只反映抓取那一刻的状态。很多问题是时序相关的单次抓取看不到。我的做法是写个脚本每秒抓一次连续抓几十次然后对比差异。这样才能捕捉到瞬态问题。8.2 合成路径切换要提前验证任何会引入新图层的功能悬浮窗、画中画、多屏上线前都要验证合成路径。我见过太多项目在功能开发完后才发现功耗超标回头改合成策略成本很高。建议在功能设计阶段就把图层数量和格式确认清楚。8.3 刷新率适配不是改个数字那么简单高刷适配涉及应用、SurfaceFlinger、HWC、DRM四层。应用要能跟上高帧率出帧SurfaceFlinger要正确调度HWC要支持对应模式DRM要能切换时序。任何一层没跟上高刷就是摆设。实测中我建议先用dumpsys SurfaceFlinger确认当前实际刷新率再逐层排查。8.4 保留一份自己的排查清单最后分享一个我自己的习惯维护一份显示问题排查清单按现象 → 可能层级 → 排查命令 → 常见原因组织。遇到新问题先对照清单快速定位解决后把新案例补进去。几年下来这份清单成了团队里最值钱的东西。显示链路的问题看似复杂但归类之后其实就那几类关键是建立系统化的排查思路而不是每次都从头猜。