ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android显示链路全解析:从应用到屏幕的合成与调试实战

Android显示链路全解析:从应用到屏幕的合成与调试实战 1. 从一次花屏说起为什么需要理解显示链路几年前我在调试一台车载设备时遇到一个很典型的问题应用层明明已经把画面绘制完成日志里也能看到onDraw正常回调但屏幕上就是隔三差五闪一下黑偶尔还伴随半屏撕裂。当时我第一反应是应用代码有问题查了两天绘制逻辑一无所获。后来把注意力转到系统层用dumpsys SurfaceFlinger抓了一轮合成状态才发现问题出在图层合成的时序上——某个 overlay 图层的 buffer 提交节奏和主图层对不上导致合成器拿到了一帧不完整的画面。那次经历让我意识到一件事只懂应用层绘制不懂系统显示链路遇到显示类问题基本就是盲人摸象。你能画出来不代表你能显示出来你能显示出来不代表你能稳定、流畅、低延迟地显示出来。这中间隔着一条相当长的链路从应用进程的 Canvas 绘制到 Surface 的 buffer 队列到 SurfaceFlinger 的合成决策再到 HWC 硬件叠加最后经 DRM/KMS 送到物理屏幕的时序控制器。任何一环出问题用户看到的就是花屏、卡顿、撕裂、延迟。这篇内容就是想把这条链路完整地串一遍。它不是那种只讲概念的科普而是我这些年做 Android 显示相关调试、性能优化、问题定位时积累下来的实战理解。适合谁看如果你在做 Android 应用开发但总被为什么我的动画掉帧困扰如果你在做 Framework 或系统定制需要改显示相关逻辑如果你在做车载、TV、平板这类对显示时序敏感的产品那这条链路你绕不开。我会尽量用生活化的类比把每个环节讲清楚同时把关键的调试命令、参数含义、常见坑点都带上让你看完能直接上手排查问题。先给一个整体印象Android 显示链路可以粗略理解成一条流水线应用是生产车间Surface 是传送带SurfaceFlinger 是总装调度中心HWC 是自动化装配臂DRM 是出厂运输通道屏幕是最终交付的展厅。每个环节都有自己的节奏和缓冲节奏对不上就会出问题。下面我们逐段拆。2. 应用侧到 Surface画面是怎么交出去的2.1 绘制不是直接画在屏幕上很多人初学 Android 绘图时会有个误解以为onDraw里画的东西直接就上屏了。实际上应用进程根本没有直接操作屏幕的权限它画的每一笔都落在一块离屏缓冲区上这块缓冲区就是 Surface 背后管理的 GraphicBuffer。你可以把 Surface 想象成一块画布应用在这块画布上作画画完之后把画布递给系统系统再决定怎么把这块画布贴到屏幕上。这个递的动作在 Android 里通过 BufferQueue 完成。BufferQueue 是一个生产者-消费者模型应用进程是生产者负责 dequeue 一块空闲 buffer、绘制、queue 回队列SurfaceFlinger 是消费者负责 acquire 一块已绘制好的 buffer、合成、release 回去。这个模型的好处是生产和消费解耦应用不需要等系统合成完才能画下一帧两边可以并行跑这是流畅显示的基础。提示BufferQueue 的默认深度通常是 3triple buffering意味着最多可以有 3 块 buffer 在流转。这个数字不是随便定的它直接决定了应用能提前画多少帧也影响延迟和内存占用。2.2 三种绘制路径Canvas、OpenGL ES、Vulkan应用往 Surface 上画有三条主流路径。最上层是Canvas也就是我们熟悉的View.onDraw(Canvas)底层通过 Skia 图形库渲染适合常规 UI。往下一层是OpenGL ES游戏、地图、相机预览这类需要高性能渲染的场景基本都走这条路通过 EGL 把 GL 上下文和 Surface 绑定。再往下是Vulkan更接近硬件、开销更低但开发复杂度也更高目前主要在高端游戏和部分系统渲染场景使用。这三条路径最终都会把像素写进 GraphicBuffer区别在于谁来写、怎么写。Canvas 走的是 CPU 光栅化部分场景有 GPU 加速GL/Vulkan 走的是 GPU 渲染。理解这一点很重要因为当你排查为什么这帧没出来时需要先确认是哪条路径出的问题——是 Skia 没画完还是 GL 命令没提交还是 buffer 没 queue 上去。2.3 Choreographer 与 VSYNC节奏的指挥棒画面不是你想画就能随时画的得跟着屏幕的刷新节奏走。这个节奏由VSYNC垂直同步信号驱动而应用侧接收 VSYNC 的入口就是Choreographer。每次 VSYNC 到来Choreographer 会依次回调输入、动画、遍历measure/layout/draw这几个阶段应用就在这个窗口期内完成一帧的绘制。为什么要跟着 VSYNC 走因为如果应用画得比屏幕刷新快多出来的帧会被丢弃白白耗电如果画得比屏幕慢就会出现掉帧。跟着 VSYNC 走能让绘制节奏和显示节奏对齐这是流畅体验的前提。Choreographer 本质上是一个节拍器它把 VSYNC 信号分发给需要感知帧节奏的模块。这里有个常见的坑如果某一帧的绘制耗时超过了 VSYNC 周期比如 60Hz 下是 16.67ms这一帧就赶不上当前 VSYNC只能等下一个用户就会感知到一次卡顿。所以做性能优化时我们经常用Choreographer.FrameCallback或者 systrace/Perfetto 去测量每一帧的耗时找出超过 16.67ms 的坏帧。2.4 一个容易被忽略的细节Surface 的生命周期Surface 不是永远存在的它跟着 Window 走。Activity 创建时 Window 被添加Surface 被创建Activity 销毁时 Surface 被释放。但有些场景下 Surface 会被销毁重建比如旋转屏幕、进入画中画、切换分辨率。如果应用在 Surface 已经销毁后还往上面画就会报Surface has been released之类的错误。我在做视频播放器时踩过这个坑播放过程中旋转屏幕Surface 被重建但解码器还持有旧的 Surface 引用结果画面直接黑掉。解决办法是监听SurfaceHolder.Callback的surfaceDestroyed和surfaceCreated在销毁时暂停渲染、重建时重新绑定。这个细节在文档里往往一笔带过但实际项目中非常容易出问题。3. SurfaceFlinger合成调度中心到底在做什么3.1 它不只是合成更是资源调度者SurfaceFlinger 这个名字容易让人以为它只负责把多个图层合成一个。实际上它的职责远不止于此它管理所有图层Layer的生命周期决定每个图层用哪种合成方式协调 VSYNC 节奏处理图层可见性、裁剪、变换、透明度还要和 HWC 协商谁来干实际的合成活。你可以把它理解成一个总装调度中心既要管物料buffer又要管工序合成方式还要管节拍VSYNC。每个 Window 在 SurfaceFlinger 里对应一个 Layer。Layer 记录了这块 Surface 的位置、大小、Z 序、透明度、变换矩阵、裁剪区域等信息。SurfaceFlinger 每收到一个 VSYNC就会遍历所有可见 Layer根据它们的属性决定合成策略。3.2 合成方式Client 合成 vs Device 合成合成有两种方式。Client 合成也叫 GPU 合成是 SurfaceFlinger 自己用 GPU 把所有图层画到一块 buffer 上再交给显示设备。Device 合成也叫 HWC 合成、Overlay 合成是把图层直接交给 HWC由显示硬件的叠加器Overlay在扫描输出时实时叠加不需要 SurfaceFlinger 先合成一块完整的 buffer。哪种更好Device 合成通常更省电、延迟更低因为省掉了 GPU 合成这一步也不需要额外的 buffer 读写。但 Device 合成有硬件限制叠加器能处理的图层数量有限常见是 4 到 8 个对图层的格式、缩放、旋转、混合模式也有约束。一旦超出限制SurfaceFlinger 就得回退到 Client 合成。注意很多显示性能问题根源就是本该走 Device 合成的图层被迫走了 Client 合成。比如图层数量超了、格式不支持、做了非整数缩放都会触发回退导致功耗上升、延迟增加。3.3 用 dumpsys 看清合成现场排查显示问题dumpsys SurfaceFlinger是最常用的工具。它能打印出当前所有 Layer 的状态、合成方式、buffer 情况。几个我常用的子命令# 列出所有图层及其基本信息 adb shell dumpsys SurfaceFlinger --list # 查看合成相关的详细统计包括每个图层的合成类型 adb shell dumpsys SurfaceFlinger # 查看指定图层的延迟信息 adb shell dumpsys SurfaceFlinger --latency layer-name--latency这个特别有用它会输出该图层最近 128 帧的时间戳包括应用绘制完成时间、VSYNC 时间、合成呈现时间。通过这三个时间戳你能算出应用的绘制延迟和整体呈现延迟。我之前定位一个操作后画面慢半拍的问题就是靠这个命令发现应用绘制完成到最终呈现之间隔了两帧说明中间有 buffer 积压。3.4 图层可见性与 Z 序的坑SurfaceFlinger 只合成可见的图层不可见的图层会被跳过。但可见的判断不只看setVisibility还要看图层是否被完全遮挡、是否在屏幕外、alpha 是否为 0。有时候一个图层明明设置了可见但因为被上层不透明图层完全盖住SurfaceFlinger 会把它标记为不可见跳过合成。这个机制本身是优化但会带来一个调试陷阱你以为某个图层在合成实际上它被优化掉了。排查时如果发现某个图层没显示先确认它是不是被上层盖住了或者 Z 序排错了。Z 序由 Window 的层级和setZOrderMediaOverlay之类的 API 决定搞错了就会出现画面被盖住的诡异现象。4. HWC 与 DRM从合成到点亮屏幕的最后一段4.1 HWC 的角色硬件叠加的翻译官HWCHardware Composer是 Android 显示架构里的硬件抽象层它向上对接 SurfaceFlinger向下对接显示驱动。SurfaceFlinger 把图层列表交给 HWCHWC 根据硬件能力决定哪些图层能走 Overlay、哪些需要 GPU 预合成然后把最终方案告诉驱动。HWC 的版本迭代挺快从 HWC1 到 HWC2接口变化不小。HWC2 引入了更细粒度的能力查询和更灵活的合成决策让 SurfaceFlinger 能更精确地知道硬件到底支持什么。做系统定制时如果 HWC 实现有问题最典型的表现就是该走 Overlay 的走了 GPU 合成或者图层叠加顺序错乱。4.2 DRM/KMS内核里的显示管家再往下就是内核层的DRMDirect Rendering Manager和KMSKernel Mode Setting。DRM 负责管理显示设备、内存缓冲、命令提交KMS 负责配置显示模式分辨率、刷新率、时序参数。Android 通过 HWC 把合成结果提交给 DRMDRM 再驱动显示控制器把像素按时序扫描输出到屏幕。这一层离应用开发者比较远但做底层调试时绕不开。比如调整屏幕刷新率、配置多屏显示、处理 HDR都要和 DRM/KMS 打交道。/sys/class/drm/下面能看到各个显示连接器的状态modetest这类工具能直接操作 KMS 做测试。4.3 刷新率切换不只是改个数字现在高刷屏很普遍60Hz、90Hz、120Hz 甚至更高。刷新率切换看着简单实际上涉及整条链路的协同。应用要能感知刷新率变化并调整绘制节奏SurfaceFlinger 要重新配置 VSYNC 周期HWC 和 DRM 要切换显示时序。如果某一环没跟上就会出现切换瞬间的卡顿或闪烁。我在做高刷适配时遇到过一个典型问题应用在 120Hz 下画得飞快但切换到 60Hz 后Choreographer 的节拍没及时调整导致应用还在按 120Hz 的节奏提交 buffer结果 buffer 积压延迟反而变大。解决办法是监听刷新率变化动态调整绘制策略。这个细节在官方文档里讲得不多但实际项目中很关键。4.4 一个完整的时序视角把整条链路串起来看一帧的生命周期大致是这样的VSYNC 到来Choreographer 回调应用绘制应用把 buffer queue 到 BufferQueueSurfaceFlinger 在下一个 VSYNC 收到 buffer决定合成方式交给 HWCHWC 决定 Overlay 方案提交给 DRMDRM 在下一个 VSYNC 把画面扫描输出到屏幕。所以从应用绘制到用户看到中间至少隔了一到两个 VSYNC 周期这就是为什么操作后画面慢半拍往往不是应用的问题而是链路本身的延迟。理解这个时序对做延迟敏感的应用比如云游戏、AR、手写笔特别重要。你要做的不是让应用画得更快而是想办法压缩链路中的缓冲级数比如减少 BufferQueue 深度、强制走 Device 合成、优化 VSYNC 对齐。5. 实战排查一条命令定位显示问题5.1 先建立分层排查的思路显示问题千奇百怪但排查思路可以统一从应用层往硬件层逐层排除。先确认应用有没有正常绘制看onDraw回调、看 GPU 渲染耗时再确认 buffer 有没有正常提交看 BufferQueue 状态再确认 SurfaceFlinger 有没有正常合成看 dumpsys再确认 HWC 走了哪种合成方式最后看 DRM 有没有正常输出。这个顺序的好处是每一层都有对应的工具和日志能快速缩小范围。最怕的是一上来就怀疑硬件结果查了半天发现是应用少调了一次invalidate。5.2 常用命令速查我把这些年常用的显示调试命令整理成一张表方便你按需取用命令作用适用场景dumpsys SurfaceFlinger --list列出所有图层确认目标图层是否存在dumpsys SurfaceFlinger查看合成详情确认合成方式、buffer 状态dumpsys SurfaceFlinger --latency layer查看图层延迟定位呈现延迟、buffer 积压dumpsys gfxinfo package查看应用渲染耗时定位应用侧掉帧dumpsys window查看窗口状态确认窗口层级、可见性systrace/Perfetto全链路时序追踪定位跨层性能问题提示--latency输出的时间戳单位是纳秒解读时注意换算。三列分别对应应用绘制完成VSYNC呈现正常情况下三者间隔应该稳定如果某一段突然变大说明那一环有瓶颈。5.3 一个真实案例的排查链路回到开头那个花屏问题我当时的排查过程是这样的先用dumpsys gfxinfo确认应用绘制正常没有坏帧再用dumpsys SurfaceFlinger看合成状态发现主图层走的是 Device 合成但那个 overlay 图层偶尔会从 Device 合成回退到 Client 合成进一步查 HWC 日志发现回退的原因是 overlay 图层的 buffer 格式在某些帧上不一致触发了硬件不支持。最后定位到是 overlay 数据源的格式转换逻辑有 bug偶尔会产出非预期格式的 buffer。这个案例说明显示问题的根因往往不在你第一眼怀疑的地方。分层排查、逐层验证比凭直觉猜要靠谱得多。5.4 性能优化中的几个经验值做显示性能优化有几个经验值可以参考。60Hz 下每帧预算 16.67ms120Hz 下是 8.33ms应用绘制加合成必须在这个窗口内完成。BufferQueue 深度建议保持默认的 3太浅容易 underrun太深增加延迟。Device 合成的图层数量上限因硬件而异常见是 4 到 8超过就要考虑合并图层。呈现延迟方面从触摸到画面更新做得好的设备能控制在 50ms 以内超过 100ms 用户就会有明显感知。这些数字不是绝对的但能帮你快速判断当前状态是否正常。比如你测出来呈现延迟 200ms那肯定有问题得往链路里找瓶颈。6. 那些文档不会告诉你的坑6.1 图层数量不是越多越好有些开发者习惯给每个 UI 元素都开一个独立 Surface觉得这样灵活。实际上图层数量直接影响合成开销和功耗。图层越多SurfaceFlinger 遍历和决策的成本越高HWC 能走 Overlay 的概率越低越容易回退到 GPU 合成。我的建议是能合并的图层尽量合并尤其是静态内容没必要单独占一个图层。6.2 透明度和混合模式是性能杀手带 alpha 的图层、用了特殊混合模式的图层往往无法走 Device 合成因为硬件叠加器对混合的支持有限。如果你的界面大量使用半透明效果很可能整条链路都在走 GPU 合成功耗和延迟都会上去。做 UI 设计时能不用半透明就不用非用不可时也要控制范围。6.3 分辨率适配的隐藏成本非整数缩放、非原生分辨率渲染都会增加合成开销。比如在 1080p 屏幕上渲染 720p 内容再放大HWC 可能不支持这种缩放只能走 GPU。做多分辨率适配时尽量让渲染分辨率贴近屏幕原生分辨率减少缩放带来的额外开销。6.4 调试时别忘了看功耗显示链路的很多问题最终会体现在功耗上。同样的界面走 Device 合成和走 Client 合成功耗可能差 20% 以上。做优化时除了看帧率和延迟也要用功耗工具测一下确认优化真的有效。我见过不少优化只是把延迟从一处挪到了另一处整体功耗反而上升。6.5 版本差异要心里有数Android 每个大版本对显示链路都有调整。比如某个版本改了 BufferQueue 的默认深度某个版本调整了 HWC 的接口某个版本引入了新的刷新率切换机制。做跨版本适配时不能想当然地套用旧经验得针对目标版本实测。我一般会在项目初期就搭好不同版本的测试环境避免后期返工。7. 把这条链路装进脑子里写到这里这条从应用到屏幕的完整链路基本串完了。我自己的体会是理解显示链路最大的价值不是让你记住每个模块的名字而是让你在遇到问题时知道该往哪个方向看。画面没出来是应用没画、buffer 没提交、还是合成被跳过画面卡顿是应用绘制慢、合成回退、还是 VSYNC 没对齐有了这条链路的框架排查就有了章法不会像无头苍蝇一样乱撞。如果你刚开始接触这块建议从dumpsys SurfaceFlinger和dumpsys gfxinfo这两个命令入手先学会看现状再逐步深入到 HWC 和 DRM。如果你已经在做系统定制那 HWC 的合成决策逻辑和 DRM 的时序配置是必须啃下来的硬骨头。这条链路涉及的模块多、层次深一次看不透很正常多结合实际项目反复对照慢慢就形成直觉了。最后分享一个小习惯我每次遇到显示相关的 bug都会先把dumpsys SurfaceFlinger的完整输出存一份问题复现时再存一份两份对比着看往往能发现平时忽略的差异。这个笨办法帮我定位过不少疑难问题比单纯盯着代码看有效得多。
返回列表