1. 一张图背后的显示链路全景
1.1 为什么“一张图”值得画
Android 显示链路这个话题,我在刚接触 Framework 那会儿就尝试过画图,结果画了三版都不满意。第一版太粗,只画了 App 到 SurfaceFlinger 的箭头;第二版太细,把每个 BufferQueue 的 slot 状态都标上,结果图比代码还难读;第三版才找到平衡点——按数据流向分层,每层只保留一个核心动作和一组关键角色。
之所以强调“一张图看懂”,是因为这条链路涉及的模块实在太多:应用进程里的 View 树、RenderThread、GPU 驱动、SurfaceFlinger、HWC、DRM/KMS,中间还夹着 BufferQueue、Gralloc、Fence 这些同步机制。如果不用一张分层图把它们的职责边界和数据交接点固定下来,排查问题时很容易在错误的层里打转。比如画面撕裂,有人去查 View 的 onDraw,有人去查 HWC 的 compose,其实真正的问题可能出在 Fence 的 signal 时机上。
这张图的价值不在于好看,而在于定位。当你拿到一个掉帧、花屏、黑屏或者延迟问题,第一件事应该是问:数据现在卡在哪一层?是 App 没产出帧,还是 SurfaceFlinger 没合成,还是 HWC 没送显?这张图就是回答这个问题的地图。
1.2 链路分层的核心逻辑
我把整条链路切成四层,从下往上分别是:应用绘制层、合成层、硬件合成层、显示驱动层。这个切法不是拍脑袋,而是对应了 Android 图形栈的实际进程和硬件边界。
应用绘制层跑在 App 进程里,核心是 UI 线程和 RenderThread 的配合。UI 线程负责构建 DisplayList,RenderThread 负责把 DisplayList 转成 GPU 命令并提交。这一层的产出物是一个填好内容的 GraphicBuffer,通过 BufferQueue 交给下一层。
合成层就是 SurfaceFlinger 的地盘。它从各个应用的 BufferQueue 里取 buffer,根据自己的合成策略决定哪些层用 GPU 合成、哪些层交给 HWC。这一层的关键是层叠管理和合成方式决策。
硬件合成层对应 HWC(Hardware Composer)。HWC 是显示硬件的抽象,它告诉 SurfaceFlinger 自己能处理几个层、每个层支持什么格式和变换。如果 HWC 说“这个层我搞不定”,SurfaceFlinger 就得回退到 GPU 合成。
显示驱动层就是 DRM/KMS 这套内核子系统。HWC 最终通过 DRM 接口把合成好的帧提交给显示控制器,显示控制器再按照时序把像素推给屏幕。这一层管的是刷新率、分辨率、色彩空间这些和物理面板强相关的参数。
注意:这四层不是严格串行的。比如 HWC 可以在 SurfaceFlinger 还没完成 GPU 合成时就预提交部分层,这种并行是 Android 显示管线低延迟的关键,但也是排查时序问题时最容易让人迷惑的地方。
1.3 关键角色速查表
在展开细节之前,先把这条链路上的核心角色列出来,后面每一层都会反复提到它们。
| 角色 | 所在层 | 核心职责 | 常见问题信号 |
|---|---|---|---|
| RenderThread | 应用绘制层 | 执行 GPU 绘制命令 | 掉帧时先看它的耗时 |
| BufferQueue | 应用/合成交界 | 生产者消费者缓冲管理 | dequeueBuffer 超时 |
| SurfaceFlinger | 合成层 | 层叠管理与合成决策 | 合成耗时突增 |
| HWC | 硬件合成层 | 硬件合成能力抽象 | validate 失败回退 GPU |
| DRM/KMS | 显示驱动层 | 显示控制器提交 | commit 失败或时序错乱 |
| Gralloc | 跨层 | 图形缓冲分配 | 分配失败或格式不支持 |
| Fence | 跨层 | GPU/显示同步 | 等待超时导致卡顿 |
这张表建议配合链路图一起看。实际排查时,我习惯先看问题现象落在哪个角色的职责范围内,再去查那个角色的状态。比如画面卡住不动,先看 Fence 是不是没 signal;画面颜色不对,先看 Gralloc 分配的格式和 HWC 声明的格式是否匹配。
2. 应用绘制层:从 View 到 GraphicBuffer
2.1 UI 线程与 RenderThread 的分工
很多刚入行的同学会以为 View 的 onDraw 执行完画面就出来了,其实 onDraw 只是把绘制指令记录到 DisplayList 里,真正的 GPU 渲染发生在 RenderThread 上。这个分工是 Android 从 5.0 引入 RenderThread 之后定下来的,目的是把耗时的 GPU 操作从 UI 线程剥离,避免阻塞输入响应。
UI 线程的流程大致是:ViewRootImpl.performTraversals触发 measure、layout、draw,draw 阶段构建 DisplayList。然后通过ThreadedRenderer把 DisplayList 同步给 RenderThread。RenderThread 收到后,执行CanvasContext的绘制,把 DisplayList 转成 GPU 命令,提交到 GPU。
这里有个关键点:UI 线程和 RenderThread 是并行的。UI 线程在准备下一帧的 DisplayList 时,RenderThread 可能还在渲染上一帧。这种流水线设计提升了吞吐,但也意味着如果某一帧的 GPU 渲染特别慢,UI 线程准备好的下一帧就得在队列里等着,表现为掉帧。
2.2 BufferQueue 的生产者消费者模型
RenderThread 渲染完成后,需要把结果 buffer 交给 SurfaceFlinger。这个交接通过 BufferQueue 完成。BufferQueue 是一个典型的生产者消费者队列,App 侧是生产者,SurfaceFlinger 侧是消费者。
BufferQueue 内部维护一组 slot,每个 slot 对应一个 GraphicBuffer。生产者通过dequeueBuffer拿到一个空闲 slot,渲染完成后通过queueBuffer把 slot 标记为已填充。消费者通过acquireBuffer拿到已填充的 slot,用完后通过releaseBuffer归还。
这个模型里最容易出问题的是slot 数量。默认情况下 BufferQueue 可能只有 2 到 3 个 slot。如果消费者消费太慢,生产者就会在dequeueBuffer上阻塞,UI 线程跟着卡住。反过来,如果生产者产出太快,消费者来不及消费,队列满了之后生产者也会阻塞。所以调优时经常需要根据场景调整 slot 数量,比如视频播放场景可能需要更多 buffer 来平滑抖动。
实操心得:用
dumpsys SurfaceFlinger --list可以列出当前所有 layer,再针对具体 layer 用dumpsys SurfaceFlinger查看它的 BufferQueue 状态。重点看queued和dequeued的计数,如果 queued 一直涨而 dequeued 不动,说明消费者侧有问题。
2.3 从 DisplayList 到 GPU 命令的转换
DisplayList 是一组绘制操作的记录,比如“画一个矩形”“画一段文字”“应用一个变换”。RenderThread 拿到 DisplayList 后,通过 Skia 或 Vulkan 后端把它转成 GPU 能执行的命令。
这个转换过程有几个优化点值得注意。第一是批量合并,把相同材质的绘制操作合并成一个 draw call,减少 GPU 状态切换。第二是裁剪优化,把不可见的绘制操作直接丢弃。第三是纹理上传,把 Bitmap 等资源上传到 GPU 显存,这一步如果频繁发生会很耗性能。
我遇到过一个问题:某个列表滑动时掉帧严重,查下来是每个 item 的图标都在重新上传纹理。原因是图标没有做缓存,每次绑定都触发一次纹理上传。后来改成用Bitmap的prepareToDraw预上传,掉帧就消失了。这个案例说明,应用绘制层的优化不能只看 onDraw 的耗时,还要看 RenderThread 的实际 GPU 操作。
2.4 应用层的常见性能陷阱
应用绘制层有几个高频陷阱,我按遇到频率排个序。
第一是过度绘制。多个层叠的 View 都在画背景,GPU 做了很多无用功。用开发者选项里的“调试 GPU 过度绘制”可以直观看到,红色区域就是过度绘制严重的地方。解决办法是去掉不必要的背景,或者用clipRect限制绘制区域。
第二是布局层级过深。每多一层 View,measure 和 layout 就多一轮递归。虽然现在有 ConstraintLayout 可以扁平化,但很多老项目还是嵌套很深。用 Layout Inspector 可以查看层级,超过 10 层的就要考虑优化。
第三是在 onDraw 里创建对象。onDraw 会被频繁调用,里面 new 一个 Paint 或者 Rect 都会加重 GC 负担。正确做法是在构造函数里创建好,onDraw 里只做赋值和绘制。
第四是同步等待。比如在 UI 线程里等一个网络请求或者文件读取,直接卡死整条链路。这个属于基础问题,但实际项目里还是经常见到。
3. 合成层:SurfaceFlinger 的决策逻辑
3.1 SurfaceFlinger 的启动与 layer 管理
SurfaceFlinger 是 Android 显示系统的核心服务,在系统启动时由 init 进程拉起。它维护着所有 layer 的信息,每个 layer 对应一个 Surface,也就是应用可以绘制的一块画布。
当应用创建一个 Surface 时,SurfaceFlinger 会为它创建一个 layer,并分配一个 BufferQueue。应用通过这个 BufferQueue 提交 buffer,SurfaceFlinger 通过它获取 buffer。layer 有很多属性,比如位置、大小、透明度、变换矩阵、裁剪区域、Z 序等。这些属性决定了合成时的最终效果。
layer 的 Z 序管理是合成的基础。SurfaceFlinger 按照 Z 序从低到高排列 layer,然后决定哪些层可以合并、哪些层需要单独处理。Z 序相同的 layer 会按照创建顺序排列,这个细节在排查层叠问题时很重要。
3.2 合成方式的选择:GPU 还是 HWC
SurfaceFlinger 拿到所有 layer 后,要决定怎么合成。有两种方式:GPU 合成和 HWC 合成。
GPU 合成是把所有 layer 的 buffer 作为纹理,通过 GPU 绘制到一个新的 buffer 上。这种方式灵活,支持任意变换和混合,但耗电、占带宽。HWC 合成是把 layer 直接交给显示硬件,由硬件完成叠加。这种方式省电、低延迟,但受硬件能力限制,比如支持的层数、格式、变换都有限。
SurfaceFlinger 的决策逻辑是:先让 HWC 的prepare接口评估每个 layer,HWC 返回每个 layer 的合成方式建议。如果 HWC 说某个 layer 它能处理,SurfaceFlinger 就把它标记为 HWC 合成;如果 HWC 说处理不了,SurfaceFlinger 就把它标记为 GPU 合成。最后 SurfaceFlinger 把 GPU 合成的结果作为一个新的 layer 交给 HWC,和那些 HWC 直接处理的 layer 一起送显。
这个决策过程每次刷新都会执行,因为 layer 的属性可能变化。比如一个视频层从全屏变成小窗,HWC 可能就从能处理变成不能处理,合成方式随之切换。切换本身有开销,所以频繁切换会导致性能抖动。
注意:HWC 的
prepare和set是两个阶段。prepare是评估,set是提交。如果prepare之后 layer 属性又变了,SurfaceFlinger 需要重新prepare。这个重试机制在排查合成问题时经常被忽略。
3.3 合成时机与 VSync 的关系
SurfaceFlinger 的合成不是随时进行的,而是跟着 VSync 走。VSync 是显示器的垂直同步信号,表示一帧显示完成、下一帧可以开始。SurfaceFlinger 收到 VSync 后,开始新一轮合成。
Android 的 VSync 有多个来源:HWC 产生的硬件 VSync、SurfaceFlinger 模拟的软件 VSync、以及应用侧的 VSync。这些 VSync 通过 Choreographer 和 DispSync 机制协调,确保应用绘制、SurfaceFlinger 合成、HWC 送显三个环节按节奏进行。
如果某个环节错过了 VSync,就会掉帧。比如应用在 VSync 到来时还没完成绘制,SurfaceFlinger 就只能用上一帧的 buffer,表现为画面卡顿。这种掉帧在dumpsys SurfaceFlinger的统计里能看到,重点看missed计数。
3.4 合成层的性能分析手段
分析合成层性能,我常用的工具有三个。
第一个是dumpsys SurfaceFlinger。它能输出当前所有 layer 的状态、合成方式、耗时统计。重点看Composition部分的耗时,如果 GPU 合成耗时超过 8ms,在 60Hz 下就很容易掉帧。
第二个是dumpsys SurfaceFlinger --latency。它能输出指定 layer 的帧延迟数据,包括预期显示时间、实际显示时间、帧间隔。这个数据可以导入表格做进一步分析,看延迟分布是否稳定。
第三个是 Perfetto。它能抓取整条链路的 trace,包括应用绘制、SurfaceFlinger 合成、HWC 提交。Perfetto 的好处是能看到跨进程的时序关系,比如应用 queueBuffer 的时间点和 SurfaceFlinger acquireBuffer 的时间点之间的间隔,这个间隔就是 buffer 在队列里的等待时间。
这三个工具配合使用,基本能定位合成层的绝大多数问题。我的习惯是先用 dumpsys 看整体状态,再用 Perfetto 抓具体场景的 trace,最后用 latency 数据验证优化效果。
4. 硬件合成层:HWC 的能力与限制
4.1 HWC 的抽象接口
HWC 是显示硬件的抽象层,它定义了一组接口供 SurfaceFlinger 调用。核心接口有三个:prepare、set、getDisplayConfigs。
prepare接收一组 layer,返回每个 layer 的合成方式建议。这个接口是 HWC 表达自己能力的主要途径。如果 HWC 支持某个 layer 的格式和变换,它就返回HWC2::Composition::Device;如果不支持,返回HWC2::Composition::Client,意思是让 SurfaceFlinger 用 GPU 合成。
set接收最终的 layer 列表和每个 layer 的合成方式,HWC 据此配置硬件并提交显示。这个接口是实际送显的步骤。
getDisplayConfigs返回显示器支持的配置,包括分辨率、刷新率、色彩空间等。SurfaceFlinger 根据这些配置决定使用哪个模式。
HWC 的版本从 HWC1 演进到 HWC2,接口设计变化很大。HWC2 引入了更细粒度的能力查询和更灵活的 layer 管理。现在主流设备都是 HWC2 或 HWC3。
4.2 硬件合成的能力边界
HWC 不是万能的,它的能力受显示硬件限制。常见的限制包括:
- 层数限制:很多 HWC 只支持 4 到 8 个 layer 同时硬件合成。超过这个数量,多出来的 layer 就得 GPU 合成。
- 格式限制:HWC 可能只支持特定的像素格式,比如 RGB565、RGBA8888。如果应用提交的 buffer 是其他格式,HWC 可能处理不了。
- 变换限制:旋转、缩放、裁剪这些变换,HWC 可能只支持部分组合。比如支持 90 度旋转但不支持任意角度旋转。
- 混合限制:透明度混合、颜色矩阵这些操作,HWC 可能只支持简单模式。
这些限制不是固定的,不同厂商的 HWC 实现差异很大。同一款芯片,不同厂商的调优策略不同,HWC 的能力声明也可能不同。所以排查问题时不能假设 HWC 一定能处理某个 layer,要以实际prepare的返回为准。
4.3 回退机制与性能影响
当 HWC 处理不了某个 layer 时,SurfaceFlinger 会回退到 GPU 合成。这个回退不是免费的,它有几个性能影响。
第一是额外的 GPU 负载。GPU 合成需要把 layer 的 buffer 作为纹理绘制到目标 buffer,这消耗 GPU 算力和带宽。如果回退的 layer 很多,GPU 负载会显著上升。
第二是额外的内存带宽。GPU 合成需要读写 buffer,这消耗内存带宽。在移动设备上,内存带宽是稀缺资源,过度消耗会导致整体性能下降。
第三是额外的同步开销。GPU 合成完成后,结果 buffer 要交给 HWC 送显,这中间有 Fence 同步。同步本身有延迟,如果频繁回退,延迟会累积。
所以优化显示性能的一个重要方向就是减少 GPU 合成回退。具体做法包括:控制 layer 数量、使用 HWC 支持的格式、避免 HWC 不支持的变换、合理设置 layer 属性等。
实操心得:用
dumpsys SurfaceFlinger查看每个 layer 的合成方式,如果发现某个 layer 一直是 GPU 合成,就要查原因。常见原因是格式不匹配或者变换太复杂。把格式改成 HWC 支持的,或者把变换拆解成 HWC 能处理的组合,往往能显著降低 GPU 负载。
4.4 HWC 与 DRM 的对接
HWC 最终要通过 DRM/KMS 把帧提交给显示控制器。DRM 是内核的显示子系统,KMS 是它的模式设置部分。HWC 通过 DRM 的 ioctl 接口配置显示控制器,包括设置 framebuffer、配置 plane、提交 commit。
DRM 的 plane 概念和 HWC 的 layer 概念对应。一个 plane 对应一个硬件叠加层,HWC 把 layer 映射到 plane 上。如果 plane 数量不够,多出来的 layer 就得 GPU 合成。所以 HWC 的层数限制本质上来自 DRM 的 plane 数量限制。
DRM 的 commit 是原子操作,要么全部生效,要么全部不生效。这个特性保证了显示配置的一致性,但也意味着如果某个 plane 的配置有问题,整个 commit 会失败。排查 DRM 问题时,dmesg里会有 commit 失败的日志,重点看是哪个 plane 的哪个属性导致的。
5. 显示驱动层:DRM/KMS 的提交与显示
5.1 DRM 的基本概念
DRM 是 Direct Rendering Manager 的缩写,是内核管理显示和渲染设备的子系统。KMS 是 Kernel Mode Setting 的缩写,是 DRM 里负责显示模式设置的部分。
DRM 的核心对象有四个:CRTC、Encoder、Connector、Plane。CRTC 代表显示控制器,负责从 framebuffer 读取像素并按照时序输出。Encoder 负责把 CRTC 的输出转换成 Connector 能接受的信号。Connector 代表物理接口,比如 HDMI、DP、MIPI DSI。Plane 代表硬件叠加层,负责把 framebuffer 合成到 CRTC 的输出上。
这四个对象的关系是:Plane 把多个 framebuffer 合成到一个 CRTC 上,CRTC 按照时序输出,Encoder 转换信号,Connector 送到物理接口。HWC 的工作就是把 Android 的 layer 映射到 DRM 的 plane 上,然后配置 CRTC 的时序和 Connector 的输出。
5.2 原子提交与显示时序
DRM 的原子提交是显示配置的核心机制。一次原子提交包含一组属性变更,这些变更要么全部生效,要么全部不生效。这个机制保证了显示配置的一致性,避免了中间状态导致的画面异常。
原子提交的流程是:先构建一个 atomic state,设置各个对象的属性,然后调用drm_atomic_commit。内核会检查这个 state 是否合法,如果合法就应用到硬件,如果不合法就返回错误。
显示时序是 CRTC 的核心配置,包括像素时钟、水平同步、垂直同步、前后沿等参数。这些参数决定了显示器的刷新率和分辨率。如果时序配置错误,显示器可能不亮或者画面错乱。HWC 从 Connector 的 EDID 里读取显示器支持的时序,选择合适的模式配置 CRTC。
5.3 刷新率切换与自适应
现代显示设备支持多种刷新率,比如 60Hz、90Hz、120Hz。Android 支持刷新率切换,根据内容需求动态调整。比如静态画面用 60Hz 省电,游戏用 120Hz 流畅。
刷新率切换的流程是:SurfaceFlinger 根据当前场景决定目标刷新率,通过 HWC 通知 DRM,DRM 配置 CRTC 的时序切换到目标刷新率。切换本身有开销,所以不能太频繁。Android 的刷新率切换策略考虑了内容帧率、触摸事件、系统负载等因素。
自适应刷新率是更高级的特性,比如 LTPO 屏幕支持 1Hz 到 120Hz 的无级调节。这种屏幕可以根据内容精确匹配刷新率,进一步省电。但自适应刷新率的控制更复杂,需要 HWC 和 DRM 的紧密配合。
5.4 显示驱动层的调试手段
调试显示驱动层,我常用的手段有三个。
第一个是dmesg。内核日志里会有 DRM 的提交日志、时序配置日志、错误日志。如果显示异常,先看 dmesg 里有没有 commit 失败或者时序错误的记录。
第二个是modetest。这是 DRM 的测试工具,可以列出所有 DRM 对象、查看当前配置、手动设置模式。用它可以验证硬件是否支持某个时序,或者排查配置问题。
第三个是drm_info。这个工具能输出 DRM 设备的详细信息,包括支持的格式、plane 数量、CRTC 能力等。在排查 HWC 能力问题时,drm_info 的输出是重要参考。
这三个工具需要 root 权限,在用户设备上可能用不了。但在开发板或者工程机上,它们是排查显示问题的利器。
6. 常见问题与排查技巧实录
6.1 掉帧问题的分层排查法
掉帧是最常见的显示问题,排查时我习惯按层往下查。
先看应用层。用dumpsys gfxinfo查看应用的绘制耗时,重点看Draw、Prepare、Process三个阶段的耗时。如果某个阶段超过 16ms,就是应用层的问题。常见原因是布局太复杂、onDraw 里有耗时操作、频繁创建对象等。
再看合成层。用dumpsys SurfaceFlinger查看合成耗时,如果 GPU 合成耗时超过 8ms,就是合成层的问题。常见原因是 GPU 合成回退太多、layer 数量太多、buffer 格式不匹配等。
最后看驱动层。用dmesg查看 DRM 提交日志,如果有 commit 失败或者时序错误,就是驱动层的问题。常见原因是 plane 配置错误、时序不匹配、带宽不足等。
这个分层排查法的好处是每一步都有明确的判断依据,不会在错误的层里浪费时间。
6.2 画面撕裂与同步问题
画面撕裂是显示同步问题的典型表现。原因是显示控制器在读取 framebuffer 时,GPU 还在写入,导致上半部分是旧帧、下半部分是新帧。
解决撕裂的核心是同步。Android 用 Fence 机制同步 GPU 和显示控制器。GPU 渲染完成后,通过 Fence 通知显示控制器可以读取。显示控制器读取完成后,通过 Fence 通知 GPU 可以复用 buffer。
如果 Fence 机制出问题,就会撕裂。常见原因是 Fence 没有正确 signal,或者 signal 时机不对。排查时用dumpsys SurfaceFlinger查看 Fence 状态,重点看acquireFence和releaseFence的时间戳。
注意:有些撕裂是应用自己造成的,比如在 onDraw 里直接操作 Surface,绕过了 BufferQueue 的同步机制。这种撕裂在 SurfaceFlinger 层面看不出来,需要查应用的绘制代码。
6.3 黑屏与花屏的定位思路
黑屏和花屏是更严重的显示问题,定位思路和掉帧不同。
黑屏先查链路是否通。用dumpsys SurfaceFlinger --list看 layer 是否存在,用dumpsys SurfaceFlinger看 layer 是否有 buffer,用dmesg看 DRM 是否 commit 成功。如果 layer 存在但没 buffer,是应用没产出;如果有 buffer 但没显示,是合成或驱动的问题。
花屏先查格式和时序。用drm_info看 plane 支持的格式,用modetest看当前时序配置。格式不匹配会导致颜色错乱,时序不匹配会导致画面错位。如果格式和时序都正常,再查 buffer 的内容是否正确,可能是应用绘制的问题。
这两种问题的排查都需要对整条链路有清晰的认识,这也是为什么我强调要画一张链路图。
6.4 常见问题速查表
| 问题现象 | 可能层级 | 排查工具 | 常见原因 |
|---|---|---|---|
| 掉帧 | 应用层 | dumpsys gfxinfo | 布局复杂、onDraw 耗时 |
| 掉帧 | 合成层 | dumpsys SurfaceFlinger | GPU 合成回退多 |
| 掉帧 | 驱动层 | dmesg | commit 失败、带宽不足 |
| 撕裂 | 同步 | dumpsys SurfaceFlinger | Fence 未 signal |
| 黑屏 | 全链路 | dumpsys + dmesg | buffer 缺失、commit 失败 |
| 花屏 | 驱动层 | drm_info + modetest | 格式不匹配、时序错误 |
| 延迟高 | 合成层 | Perfetto | buffer 等待时间长 |
| 刷新率异常 | 驱动层 | dmesg | 时序切换失败 |
这张表建议打印出来贴在工位上,排查问题时先对照现象找到可能层级,再用对应工具深入。
6.5 几个容易踩的坑
第一个坑是只看应用层。很多性能问题表面上是应用掉帧,实际根因在合成层或驱动层。比如 HWC 回退导致 GPU 负载高,应用绘制本身没问题,但整体帧率上不去。所以排查时要养成从下往上看的习惯,先确认底层没问题,再查上层。
第二个坑是忽略 Fence。Fence 是跨层同步的关键,但它的状态在常规 dumpsys 里不够直观。我建议在排查同步问题时,专门抓 Perfetto trace 看 Fence 的 signal 时间点,这比看 dumpsys 的文本输出有效得多。
第三个坑是假设 HWC 能力。不同设备的 HWC 能力差异很大,不能假设某个 layer 一定能硬件合成。排查时要实际看prepare的返回,而不是凭经验判断。
第四个坑是忽略刷新率。刷新率切换会影响帧率表现,如果排查时没注意当前刷新率,可能会误判。比如 120Hz 下 8ms 的合成耗时是正常的,但 60Hz 下 8ms 就接近掉帧边缘。所以排查前先确认当前刷新率。
7. 从链路图到实际调试的映射
7.1 用链路图指导工具选择
链路图不只是理解用的,它还能指导工具选择。每一层都有对应的调试工具,知道问题在哪一层,就知道该用哪个工具。
应用层用dumpsys gfxinfo、Layout Inspector、Perfetto 的应用 trace。合成层用dumpsys SurfaceFlinger、Perfetto 的 SurfaceFlinger trace。硬件合成层用 HWC 的日志和drm_info。驱动层用dmesg、modetest、drm_info。
这个映射关系能大幅提升排查效率。以前我遇到问题会挨个工具试一遍,现在先定位层级,直接用对应工具,省了很多时间。
7.2 跨层问题的联合分析
有些问题不是单一层的,而是跨层的。比如延迟高,可能是应用产出慢、buffer 等待久、合成耗时、送显延迟叠加的结果。这种问题需要联合分析。
联合分析的工具首选 Perfetto。它能同时抓取应用、SurfaceFlinger、HWC、DRM 的 trace,在一个时间轴上展示。通过看各层的耗时占比,能定位主要瓶颈在哪一层。
我处理过一个案例:视频播放延迟高,Perfetto 显示应用产出正常、合成耗时正常,但 buffer 在队列里等待时间很长。进一步查发现是 BufferQueue 的 slot 数量太少,生产者产出后没有空闲 slot,只能等消费者释放。把 slot 数量从 2 调到 4 后,延迟明显下降。这个案例说明,跨层问题需要跨层工具,单看某一层的数据是不够的。
7.3 建立自己的调试 checklist
最后分享一个我自己的习惯:把常见问题的排查步骤整理成 checklist,每次排查时按 checklist 走,避免遗漏。
checklist 的内容包括:确认当前刷新率、确认 layer 是否存在、确认 buffer 是否产出、确认合成方式、确认 Fence 状态、确认 DRM commit 状态。这六步覆盖了整条链路的关键节点,走完基本能定位问题所在层。
这个 checklist 不是固定的,随着遇到新问题会不断补充。比如后来加了“确认色彩空间”这一项,因为遇到过 HDR 内容在 SDR 屏幕上显示异常的问题。建立自己的 checklist 是提升排查效率的有效方法,建议每个从业者都做一份。
实操心得:checklist 最好做成可执行的脚本,把常用的 dumpsys 和 dmesg 命令封装进去,一键输出关键信息。这样排查时不用记命令,直接跑脚本看结果,效率更高。