ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Android工位机黑屏排查:从DMA-BUF泄漏到scrcpy编码器链路分析

2026/9/12 9:02:03 拓冰建站 浏览量
Android工位机黑屏排查:从DMA-BUF泄漏到scrcpy编码器链路分析 产线上一台 Android 工位机白天开足马力跑程序十几个小时以后突然黑屏按电源没反应远程 adb 还能连上但屏幕就是一动不动。我们一开始几乎都认定是 App 把系统资源吃死了毕竟现场反馈的时间点很巧合正好是在一次版本更新之后。结果把 App 翻了个底朝天最后才发现根子根本不在 App——罪魁祸首是 scrcpy 在连接 Rockchip 硬件编码器时产生的 DMA-BUF 泄漏。这个排查过程相当有代表性。它不是一个“改一行代码就能解决”的问题而是把应用层、系统层、硬件驱动层全部走了一遍才定位出来。今天这篇不打算讲“黑屏怎么重启”这种表面功夫而是把从头到尾的排查思路、证据链和修复手段写清楚给同样被这种“明明进程还活着但界面死了”的诡异问题折磨过的兄弟们一份可以直接照着操作的排查路线。1. 现场记录等等这App明明没崩1.1 第一波确认进程活着界面死了工位机出的问题和普通消费级 Android 设备不一样。消费级手机黑屏重启一下就行丢的是用户的时间工位机黑屏后面往往跟着一条产线的停顿PDA 扫描、工单流转全部停摆。所以当时我们接到现场反馈时第一反应不是“系统有问题”而是“哪个开发又把内存写爆了”。设备是 RK3568 平台Android 11 系统跑一个自研的 MES 工位应用同时通过 scrcpy 把屏幕实时投到办公室的大屏上做监控。黑屏发生之后我先做了一组最基础的验证物理按键无响应电源键长按关机也不行按了十几秒没反应通过局域网 adb 连接 shell 可以正常进adb shell top能看到应用进程还在尝试adb shell input keyevent 26模拟电源键屏依然不亮用adb shell screencap -p /sdcard/screen.png抓图抓出来的文件是全黑的。这一套下来基本能推翻“屏幕硬件坏了”的猜测。屏幕硬件如果挂了screencap 抓屏一般还有内容因为 SurfaceFlinger 合成出来的画面还在现在我抓出来是纯黑说明问题出在图形显示链路里系统还在跑但界面渲染已经断了。进程活着、系统响应 adb、画面输出却全黑这已经是一个“上层看似正常底层图形链路异常”的典型信号。不过当时我还不敢把 App 摘干净毕竟工位机上那个 App 是重度使用 SurfaceView、OpenGL 的出了问题确实会直接影响整个显示流程。1.2 排除法应用层嫌疑怎么一步步洗掉要确认应用是不是根因最直接的办法是在复现路径上做对照实验。我没急着去啃代码而是先做了一组“变量控制”第一轮把工位机上的自研 App 杀掉只保留 Launcher 桌面和 scrcpy 投屏进程让它空跑。结果很有意思大概 3 个小时以后屏幕照样黑。这个实验结果一出自研 App 的嫌疑直接降了一大半。第二轮为了排除 scrcpy 本身是“运气好撞上”的巧合我换成在完全没有 scrcpy 连接的设备上单独跑自研 App连跑 24 小时屏幕正常。两轮实验一对嫌疑目标就从“App”转移到了“scrcpy 相关链路”上。第三轮重新连上 scrcpy把 App 停掉只保留 scrcpy 的虚拟显示编码通道最后黑屏照样出现而且时间比之前更短。到这一步结论已经很清楚真正的问题不在自研 App 里而在于 scrcpy 这个工具把设备端的屏幕流通过 MediaCodec 送进 Rockchip 硬件编码器的这条链路。在这个阶段我得到过一个很有误导性的信息黑屏时logcat里能看到 SurfaceFlinger 报FATAL之类的异常字样乍一看像 SurfaceFlinger 崩了。但实际上那是 SurfaceFlinger 在分配 Buffer 失败时打的日志它本身并没有死。同理很多现场人员看到system_server有 ANR 日志就跑去查系统服务其实那都是“果”不是“因”。2. 从 meminfo 里挖出的关键嫌疑DMA-BUF 异常增长2.1 先弄明白 DMA-BUF 到底是什么DMA-BUF 这个名字很多做应用层开发的同事听着陌生但它在 Android 图形系统里无处不在。简单说DMA-BUF 是 Linux 内核提供的一种共享内存缓冲区机制允许不同的设备驱动、不同的进程之间共享同一块物理内存而不需要反复拷贝数据。拿显示一条屏幕流举例相机或者硬件编码器写了一帧画面到一块内存里这块内存不需要先拷给 CPU、再交给 GPU而是通过 DMA-BUF 直接把物理内存句柄传递过去GPU 用同一块地址去采样渲染。好处是效率极高极适合视频、显示、GPU 运算这类高频大数据流坏处是它的生命周期管理比普通内存严格得多——每个 DMA-BUF 对象有引用计数内核驱动里要负责“拿”还要负责“还”任何一个环节多拿了没还这块内存就成了收不回来的呆账。在 Rockchip 这样的 SoC 平台上DMA-BUF 的应用尤其广泛。RK 的 VPU 视频编解码单元、RGA 图形加速器、ISP 图像处理器以及 DRM/KMS 显示控制器基本全挂在 DMA-BUF/ION 体系上分配内存。Android 系统的 ION heap 在较新内核里虽然逐步被 dma-heap 取代但在 RK 平台上大量的驱动路径依然沿用着一套“dma_buf_fd 传递 引用计数管理”的模型。理解这个背景以后你再去看内存监控数据就会发现一个反常现象。2.2 meminfo 里那个持续爬升的数字黑屏复现之后我第一时间取了adb shell cat /proc/meminfo重点不是看 MemFree而是看 DMA-BUF 相关的统计字段。当时几次采样数据我记得非常清楚采样时间点系统运行时长scrcpy 连接/断开次数dumpsys meminfo 中 DMA-BUF 总量设备开机后约 15 分钟0 次未连接约 98 MB第一次黑屏前 2 小时约 6 小时4 次约 210 MB第一次黑屏时约 10 小时8 次约 330 MB重启后重新连接 scrcpy 3 小时约 3 小时2 次约 280 MB第二次黑屏时约 9 小时6 次约 510 MBDMA-BUF 总量在每次 scrcpy 连接/断开之后都会明显涨一截而且只涨不降。这基本就是教科书式的“泄漏”曲线正常情况下 DMA-BUF 应该随着连接断开被释放回系统总量保持在一个稳定区间一旦只涨不跌说明有谁在每次编解码会话结束后没有归还内存。我顺手又跑了adb shell dumpsys meminfo看按进程维度的 DMA-BUF 占用大多数进程都很正常唯独跟 mediaserver / MediaCodec 相连的那个进程DMA-BUF 占用一件比一件高。虽然 scrcpy 连接本身是靠 adb 服务进程承载的但真正跟编码器打交道的还是 Android 多媒体框架里的硬件编码器客户端。2.3 scrcpy 和 Rockchip 硬件编码器是怎么搭成“泄漏组合”的要理解这两者为什么会一起出事得说清楚 scrcpy 在 Android 端干了一件什么事。scrcpy 之所以在 Android 调试圈里这么流行是因为它不需要在手机上装 App而是通过 adb 把一个 server 端 jar 推到设备上在设备端开启一个虚拟显示VirtualDisplay然后用 MediaCodec 创建编码器把虚拟显示里的图像帧编码成 H.264/H.265再通过 adb 的 socket 回传到电脑端解码显示。这套流程里MediaCodec 在编码时会持续从 Surface 获取图形缓冲区。设备端收到一帧画面图形缓冲区先经 Surface 到达 MediaCodec 的输入端口再交给底层硬件编码器。在 Rockchip 平台上硬件编码器拿到的并不是普通内存而是带有 dma_buf 文件描述符的专用缓冲。解码和编码都会围绕这些 dma_buf fd 做 mapping、attach/detach、reference 计数加减。正常流程应该是编码器处理完一帧调用 release 接口归还缓冲区编码会话结束释放所有 attach 的 DMA-BUF 引用。但在 scrcpy 反复连接、断开、切换分辨率、切换编码参数的过程中MediaCodec 的 stop/release 路径和底层 VPU 驱动的释放路径经常出现竞争——上层认为已经释放了底层 VPU 还攥着 dma_buf 的引用计数不放手等上层进程退出fd 关闭底层那个引用却没减于是 DMA-BUF 对象永远“正在使用”物理内存永远无法回收。这个问题在 RK 平台上尤其明显是因为它的 VPU 编解码器驱动走了独立的 vcodec_service 服务缓冲区分配和释放都需要跨驱动节点做一次 ioctl。scrcpy 的特点是“高频启停”每次连接都是一次完整的编码器创建、使用、销毁流程不仅频率高而且异常退出时根本来不及等正常的释放回调直接把 fd 挂在驱动里。3. 证据链闭环定位逃逸的 FD 和内核节点3.1采集现场资料能取的证据先取下来排查这种疑难问题最大的忌讳是急着下结论和急着重启。一旦重启内存里的所有证据都没了。我当时的操作顺序是这样的先用 adb 保持设备在线千万别让现场同事按重启键。然后依次采集以下数据adb shell dumpsys meminfo保存完整的内存分布adb shell cat /proc/meminfo重点看 MemFree、Slab、DMA-BUF 相关字段adb shell dumpsys SurfaceFlinger看图层合成状态和 Buffer 分配情况adb shell cat /sys/kernel/debug/dma_buf/bufinfo这里是内核对所有 DMA-BUF 对象的台账能看到每个 buffer 来自哪个驱动、被谁引用adb logcat -b all保存完整系统日志如果能拿到 rootadb shell su -c cat /proc/zygote/fdinfo之类更底层的信息也一并保存。这套资料里最核心的是/sys/kernel/debug/dma_buf/bufinfo。它等于一个全局的 DMA-BUF 户口本每个缓冲区是谁申请的、现在被谁引用、引用计数是多少全都在里面。通过它可以直接看出泄漏的源头是哪一个驱动节点在捣鬼。3.2 从进程 FD 到内核引用挖出“赖着不走”的对象取回数据以后我的排查动作分三步第一步看进程 FD。找到跟 scrcpy 相关的 server 进程遍历它的/proc/pid/fd目录统计里面带 dma-buf 类型的 fd。正常状态下一个空闲的编码器会话不应该残留大量 dma_buf fd但我在黑屏前的采样里看到这个目录下面带anon_inode:dmabuf的 fd 数量一直在涨。更进一步我记录了两个时间点的 fd 数量变化采样时间点scrcpy server 进程内 dmabuf fd 数量其中指向编码器输出缓冲的 fd 数scrcpy 连接后 1 分钟125scrcpy 断开并重新连接 5 次后5729黑屏前最后一次采样11364进程持有的 dmabuf fd 数量一次比一次多而且没有任何一次回落这就是“泄漏发生在进程内”的铁证。正常情况终止一个编码会话fd 会整体关闭现在 fd 数量涨上去说明编码器底层的缓冲没有随会话结束而释放回系统。第二步查/sys/kernel/debug/dma_buf/bufinfo。我按照 dma_buf 的 exporter 名称排序发现一个特别刺眼的字段来自vcodec/rk_vpu_service的 buffer 对象数量占了全局 DMA-BUF 对象的大头而且每个对象的 task 名都指向 mediaserver 或 scrcpy server 的进程名。同时这些 buffer 的引用计数明显异常正常用完应该回落到 0但这些对象引用计数一直挂在 1 或者 2说明驱动里永远有一只手没有松开。第三步交叉验证时间线。我把 scrcpy 每次连接/断开的时间点和 dma_buf 对象数量增长的时间点对齐结果完全吻合。每次 scrcpy 断开都会在 bufinfo 里留下几个“永久占用”的 vcodec buffer。经过连续 6 次连接/断开循环编码器侧 DMA-BUF 总量从稳定值涨了近 4 倍。3.3 黑屏时刻的真正触发点DMA-BUF 泄漏本身不会直接导致黑屏真正压垮系统的是“分配失败”。当 DMA-BUF 被泄漏异常占用、可用内存越来越少时SurfaceFlinger 向图形缓冲区GraphicBuffer申请新内存就会出现失败或长时间等待。屏幕上的动画合成、帧渲染都需要新的 buffer一旦申请不到画面就卡在最后一帧随后整个界面进入不刷新状态表现就是黑屏。我在黑屏前一刻的 logcat 里看到了类似这样的关键日志SurfaceFlinger: Failed to allocate buffer (size: 1920 * 1080, format: ...) E/BufferQueueProducer: dequeueBuffer: BufferQueue has been abandoned or is invalid E/ANativeWindow: native_window_api_disconnect failed: ...这些日志说明分配已经失败而且 BufferQueue 也处于“被废弃/无效”的状态。最容易误解的地方就是这里——它看起来像 SurfaceFlinger 崩了其实只是底层“分不出内存”。如果你只盯着日志里的 FATAL 去重启不去统计 DMA-BUF 的增长那这个黑屏问题将会一直复现而且每次都会有人把锅扣在 App 头上。4. 修复思路与落地改造4.1 短期止血运维级别的快速处理在根因还没确认之前现场不能一直停摆。最直接的止血方案是把 scrcpy 的投屏链路降级或者关停。我当时给现场运维的临时方案是关闭 scrcpy 自动重连机制改为人工按需连接在设备端限制编码分辨率scrcpy --max-size 1280 --max-fps 15降低单次编码会话的内存峰值每 2 小时由 cron 任务主动断开一次 scrcpy给系统一个释放缓冲区的机会如果出现黑屏优先执行adb shell am restart或重启 mediaserver不要立刻重启整机可以快一点恢复。实际执行下来这套临时方案把故障间隔从“十几个小时必现”拉长到“三四天才一次”至少让产线可以继续跑给我们赢得了定位和修复的时间窗口。但它只能延后问题不能解决泄漏本身。4.2 中期规避从应用层和系统层减少触发路径接下来要解决的是“为什么 scrcpy 会经常打断编码器正常释放”这个问题。scrcpy 的编码器生命周期本身是符合 Android 规范的但它在异常断开时经常不走正常释放流程。比如 adb 连接突然断掉、电脑端 scrcpy 被直接 kill设备端的 server 进程不会立刻感知而是等到底层 socket 超时才发现连接没了。这个过程中 MediaCodec 可能正处于编码中间态VPU 驱动还留着未完成的 DMA-BUF 引用。等 server 强制退出上层下次再来申请编码器时底层会重复创建新 buffer而旧 buffer 始终没人回收。针对这个情况我们在系统服务层做了一层“看护”监听 scrcpy server 进程的退出事件一旦检测到异常退出主动调用一次/system/bin/stop media再启动用一次完整的编解码服务重启清掉 VPU 里滞留的 buffer对内核对 DMA-BUF 总量做阈值监控当设备可用内存低于某个水位且检测到大量滞留编码器 buffer 时自动重启 mediaserver在系统属性里关掉编码器不必要的“tunneling”模式让缓冲区分配路径变短减少驱动里引用加挂的点。这几条改完故障率明显下降但严格来说还只是“让泄漏没那么容易爆炸”并没有消除泄漏源。4.3 长期根治平台内核 patch 和“回归测试前置化”真正要根治需要 Rockchip 平台侧对 VPU 驱动做修复。这个级别的改动不是我们作为现场集成方能独立完成的但也并非没有任何办法。我这边做的事是把完整的证据链整理好发给 Rockchip 的 FAE包括DMA-BUF 泄漏曲线、bufinfo 里异常引用计数的 buffer 列表、复现步骤scrcpy 连续连接断开 10 次、内核日志。这类平台问题只要证据足够原厂通常能定位到 vcodec_service 驱动里的 dma_buf_put 调用时机问题。一般情况下会在驱动驱动路径上增加稳定引用计数检查或者在 MediaCodec stop 路径上强制释放未归还的 buffer。在等待原厂补丁的同时我们做了两件很有价值的事第一建立自动化回归脚本。脚本负责反复启动/停止 scrcpy 30 次每次间隔 10 秒全程记录 DMA-BUF 总量和 dmabuf fd 数量。如果增长超过设定阈值就判定失败。这个脚本现在已经成为我们 Android 工位机 ROM 发布前的例行测试项。第二把“黑屏现场如何取证”写成了操作手册。产线再碰到类似问题运维不会第一时间重启而是先执行一套固定的取证命令把 dumpsys、bufinfo、logcat 全部导出再重启设备。这套动作节省了后面无数扯皮的时间。最终原厂的 patch 在驱动层补上了异常路径的 dma_buf 引用释放我们又做了三轮压力测试scrcpy 反复连接断开 100 次DMA-BUF 总量稳定在 90-120 MB 区间不再持续增长。这个工位机的屏幕黑屏问题才算彻底闭环。5. 这次排查留给我的几个经验5.1 黑屏不等于是 UI 挂了先分清范畴再动手这次排查给我的第一教训是黑屏这种表现要拆成“系统层面还活着”和“显示链路已经断”两件事来看。用 adb 连一次设备就知道系统 CE 还活着用 screencap 抓一张图就知道显示链路是否正常再用 dumpsys SurfaceFlinger 看图层是否在合成。把这三步做下来问题范围立刻从整个 Android 系统收窄到图形内存这一条线。如果一开始就抱着“肯定是 App 写崩了”的刻板印象我可能会花很多时间在读自研 App 的代码上最后做一轮又一轮压力测试也复现不出来白白浪费一两天。5.2 内存泄漏不一定表现为“内存越来越少”用户场景里设备黑屏时的MemFree往往还有几百 MB这导致很多人看到内存还剩一大堆就排除了“内存泄漏”的可能性。但 DMA-BUF 这一类图形缓冲区泄漏有一个特性它在 meminfo 里算 Total 时占了内存却不一定以“普通进程 RSS”的形式体现而且它占用的可能是特定的图形内存池整个系统能用于 SurfaceFlinger 分配的富余能力早就被慢性抽干了。所以排查长期运行的 Android 设备问题我建议把dumpsys meminfo里按进程的 DMA-BUF 字段、以及内核 bufinfo 都纳入日常监控。输出量虽然大但每一条都是潜在的诊断线索。5.3 工具自身也可能是“故障源”scrcpy 是我日常都在用的调试利器但我从没想过它会在设备端留下这种隐藏的内存炸弹。这也提醒我在工位机这种 7x24 小时连续运行的场景里任何“辅助工具”“监控工具”都要按生产组件的标准来看待该限制就限制、该看护就看护不能用开发电脑上的随意心态去跑产线设备。现在再遇到类似场景我的排查顺序已经固化成一套肌肉记忆先看屏幕抓图区分硬黑还是软黑再查 dumpsys meminfo 和 /proc/meminfo看 DMA-BUF 是否有异常然后查/sys/kernel/debug/dma_buf/bufinfo锁定泄漏来自哪个驱动最后通过对照实验和服务重启确认泄漏能否被主动回收所有证据齐了再和平台厂商对线要补丁。这套链路我已经在另一个 RK 平台的扫码枪设备上复用过一次效果依旧明显。如果你们的 Android 设备也出现过“长期运行后黑屏、重启就好、过阵子又犯”的老毛病我真建议你们先查一下 DMA-BUF说不定就少走一大段弯路。