Android工位机黑屏卡死:scrcpy与Rockchip编码器DMA-BUF泄漏真相
2026/9/12 5:16:56 网站建设 项目流程

工位机黑屏这种问题,大多数时候大家都默认先去翻 App 的崩溃日志、看业务代码,结果往往绕一大圈都是白费。这次我遇到的 Android 工位机周期黑屏卡死,表面看像极了业务 App 内存溢出引发的系统崩溃,实际排查到最后,根因却藏在 scrcpy 投屏工具和 Rockchip 硬件编码器的 DMA-BUF 泄漏上。整个过程走了不少弯路,花了两天才把证据链补齐。这文章我就按当时排查的真实顺序来写,把每个关键节点、判断依据、验证方法都摊开,希望对做 Android 工控、自助终端维护的朋友有帮助。

这批工位机是 Rockchip 平台的 Android 系统,设备本身不带触摸显示屏,业务上全靠 scrcpy 投屏到中控台做远程操作,现场大概有三四十台,分布在厂区不同工位。故障复现有很强的规律:开机头半小时一切正常,大约两小时后画面开始掉帧,再过十几分钟直接黑屏,主机上的指示灯还亮着,但整个系统失去响应,只能断电重启。

1. 故障现场还原:规律性黑屏是现代工控系统最值得留意的信号

1.1 黑屏卡死的具体表现和影响范围

我先说清楚“黑屏”到底是个什么状态,这直接影响后续排查方向。工位机当时的黑屏不是单纯显示无信号,而是整个 Android 系统进入了半死状态。正常情况下 scrcpy 窗口哪怕网络波动,最多也就是画面卡住,鼠标操作无响应,但窗口还是活的。这次不同,scrcpy 窗口直接黑掉,中控台这边用 adb shell 敲命令,返回非常慢,有些命令十几秒才出结果,甚至干脆超时。设备端的串口日志如果有接,能看到内核还在跑,但关键服务已经在反复重启。

另外一个很关键的细节:重启之后能恢复正常,然后大概两小时左右又进入同样的状态,非常有规律。这种“重启即好、运行数小时必挂”的模式,基本排除硬件偶发故障和单纯的 App 死循环,更多指向资源泄漏类的软件问题。常见的是内存泄漏、文件描述符泄漏、Binder 事务堆积、内核 slab 异常增长等。

1.2 为什么大家第一反应都会怀疑 App

我接手时,前一个同事已经把锅扣在了工位机主业务 App 上,理由是黑屏之前 App 界面特别卡,ANR 弹窗不断,Android 系统弹了“系统界面无响应”,随后就黑了。再加上工位机安装的 App 是自己团队写的,项目进度紧、代码质量一般,谁都愿意相信是 App 内存吃满导致系统把它杀了,甚至把 SurfaceFlinger 拖崩了。

但这里有一个很明显的矛盾:如果是 App 内存泄漏,杀掉 App 进程之后系统应该能恢复,不至于整机卡死到 adb 都难敲命令。而且黑屏之后 App 进程实际上已经不在了,系统仍然不响应,说明低内存崩溃只是结果,不是根因。真正的问题还藏在系统底层,只是 App 成了最先被牺牲的对象。这个直觉上的反差不提前建立起来,后面很容易一直在业务层打转。

2. 三层排查法:从最上层业务 App 一路挖到内核日志

2.1 第一层:业务 App 的崩溃、ANR 与内存占用

既然要排除 App 因素,那就按标准套路先把业务 App 体检一遍。我抓了黑屏前后的 logcat 缓冲、Anr traces、崩溃堆栈,也调了 amstat 和 dumpsys meminfo 看进程级内存,还专门盯着 App 的 Java heap、Native heap、GraphicBuffer 大小变化。

排查结果是:App 的 Java heap 一直稳定在合理区间,没有大对象堆积,GC 频率正常;Native heap 稍微高一些,但没有持续爬升;GraphicBuffer 占用有波动,但没到吓人的程度。再看 ANR 触发点,全部发生在主线程执行文件读写的时候,而且时间点都集中在系统整体性能已经严重劣化之后,属于被“拖死”,不是自己“作死”。这个结论基本把业务 App 从根因嫌疑里摘出来了。

不过这里也留了一个值得注意的观察:App 的GraphicBuffer 在运行一小时之后出现过一次突然升高、随后又恢复的情况,当时的异常时间点正好和 scrcpy 窗口重连时间吻合,但我那会儿还没意识到这会是后面 DMA-BUF 泄漏案的关键线索。

2.2 第二层:CPU、内存、IO 的整体资源画像

App 排除后,我把视野拉到了系统整体资源上。用 top 和 dumpsys cpuinfo 观察各进程 CPU 占用,发现一个扎眼的情况:mediacodec、media.hwcodec 这两个进程的 CPU 占用随着运行时间在缓慢增加,从最初的 3% 左右涨到黑屏前的 15% 上下。工位机业务本身对编解码需求很低,这个异常增长强烈暗示有视频编码任务在持续吃掉 CPU。

内存方面,用 free 命令看 MemAvailable 曲线,整个过程是单调下降的,越到后面掉得越快,有一种“内存水龙头越开越大”的既视感。因为工位机内存配置不大,4GB 的机器,两小时后 Available 能掉到三四百 MB,触发 lowmemorykiller 杀进程,系统面板先崩,接着是 SurfaceFlinger 也被杀,整个显示链路就断了。

IO 方面没有明显异常,闪存读写速率正常,也没有 io wait 飙升。这说明问题大概率不是磁盘卡 IO 引起的,而更偏向 CPU + 内存的组合异常。既然 CPU 的异常集中在 mediaserver / media.hwcodec,下一步必须去解这几个进程到底在干什么。

2.3 第三层:内核日志和内存统计里的“奇怪”告警

到了这一层,终于开始出有效信息。抓 /proc/kmsg 和 dmesg,发现黑屏前大量刷以下日志:

  • page allocation failure: order:4, mode:0xcc0(GFP_KERNEL)
  • DMA-BUF: 长时间未回收……
  • ion_cma_heap: 无法分配连续物理内存
  • lowmemorykiller: 大量杀进程,包括 system_server 关联的 persistent 进程

page allocation failure 说明内核已经连续多次无法分配高阶内存页,而且是在 CMA 堆上失败的,这基本可以断定问题不是普通的内存碎片,而是有模块占用了大量 DMA-BUF 资源和物理内存没归还。dmesg 里还反复出现 scrcpy 相关设备节点的 open/close 行为,配合 meminfo 里的 ION heap 占用曲线看,一个恐怖的画面逐渐清晰:投屏连接一开,内存就源源不断被吃掉,投屏一停,内存不再减少。到这一步,我基本把根因方向锁定在了 scrcpy 到 Rockchip 编码器这一条链路上的缓冲区泄漏。

3. 证据链锁定:DMA-BUF 数量单调递增与 ION 堆耗尽

3.1 用内核 debugfs 统计 DMA-BUF 数量和归属进程

要想实锤 DMA-BUF 泄漏,不能只靠猜,得有量化证据。Android 内核如果开启了 CONFIG_DMA_BUF_DEBUG 或 debugfs 节点,可以直接通过 bufinfo 看当前所有 DMA-BUF 的归属、大小和引用状态。Rockchip 平台一般路径是 /sys/kernel/debug/dma_buf/bufinfo,或者也有 /proc/rk_dma_buf_info 这类 BSP 私有的节点。我当时的做法是定时采样,一分钟抓一次,连续抓一个小时:

while true; do echo "== $(date +%T) ==" >> /data/dma_buf_trace.txt cat /sys/kernel/debug/dma_buf/bufinfo >> /data/dma_buf_trace.txt cat /sys/kernel/debug/ion/heaps/system >> /data/dma_buf_trace.txt sleep 60 done

抓出来的数据非常有说服力。系统里 DMA-BUF 的总数量从开机后的 500 多个,到运行一小时后涨到 1200 多个,卡死前接近 2000 个;其中由 media.hwcodec 进程持有的 bufinfo 数量增长最快,而且单个 buf 的 size 大多在 1MB 到 4MB 之间,一看就是视频编码帧缓冲。对比正常设备,同样运行两小时,DMA-BUF 总量应该稳定在 600 以内。

另一个更直接的指标是 ION heap 的使用量。Rockchip 的硬件编码器主要是走 ion_cma_heap 和 ion_system_heap 分配内存,通过 cat /sys/kernel/debug/ion/heaps/ 下面对应 heap 的统计,能看到 cma heap 的总 usage 从几百 MB 一路爬到接近 1GB,直到 heap 耗尽触发分配失败。这些分配失败的调用栈里,清一色指向 rk_vcodec_enc_buf_alloc 相关的符号。到这里,整个逻辑链已经闭环了:scrcpy 触发设备端硬件编码器不断申请 DMA-BUF → Rockchip 编码器驱动部分缓冲区没有按预期释放 → cma heap 耗尽 → 内核无法分配内存 → 系统全面卡死。

3.2 做一个干净的对照实验确认元凶

证据链虽然已经很有指向性,但我还是补了一个对照实验,避免被噪声误导。实验条件很简单:设备重启后保持业务 App 正常运行,但不启动 scrcpy,让系统自行跑两个小时。结果是 DMA-BUF 总数始终稳定在 500 左右,MemAvailable 曲线基本水平,CPU 占用也没有出现逐渐爬升的迹象,一切正常。

然后把业务 App 拉起来的同时也开 scrcpy,但限制在 15fps 低码率,刚跑 20 分钟就能看到 media.hwcodec 的 DMA-BUF 数量开始稳步上升,free 内存开始往下走。连续做三组实验,三组结果高度一致。实验到这里,如果还有人怀疑是业务 App 的问题,那只能说证据没看全。

3.3 排除版本干扰:scrcpy 本身有没有漏洞

查到这里,还需要回答一个问题:到底是 scrcpy 不按 Android MediaCodec 规范释放缓冲区,还是 Rockchip 编码器驱动在底层没正确回收 DMA-BUF?要区分这两者,可以用系统自带的 MediaCodec 接口写一个小工具,脱离 scrcpy 直接推屏幕内容给编码器。

我在测试机上用 MediaCodec 以 surface 输入模式跑 H.264 硬编,速率恒定在 30fps,跑了一个小时,DMA-BUF 数量增长非常有限,说明 Rockchip 编码器在正常调用路径下基本是可靠的。那问题就更大概率出在 scrcpy 与编码器交互的特殊路径上,尤其是 scrcpy 在低延迟模式下频繁请求编码器重置、动态切换参数、或者异常断开重连时,驱动里有些 buffer 引用没有被妥善归还。市面上 scrcpy 版本五花八门,工位机上跑的是一个比较老的 2.0 分支定制版,官方后续若干版本里对 MediaCodec 的 buffer 管理方式做了不少修正,这个版本干扰因素必须先扣掉。

4. scrcpy 与 Rockchip 编码器之间到底发生了什么

4.1 scrcpy 的投屏链路和硬件编码器的角色定位

要彻底理解这次泄漏,得先把 scrcpy 的运行链路讲透。scrcpy 能实现低延迟投屏,靠的是设备端 Android 系统用 MediaCodec 把屏幕内容编码成 H.264 视频流,再通过 ADB 通道传给 PC 端解码显示。整个过程里,设备端有两个环节会碰 DMA-BUF:一是 SurfaceFlinger 把屏幕的 GraphicBuffer 传给编码器作为输入,二是编码器内部要为自己分配存放编码帧数据的 DMA-BUF。

Rockchip 硬件编码器是典型的多进程共享模型,驱动通过 ION/DMA-BUF 把物理内存映射给用户态进程使用。正常流程下,编码器每编完一帧,调用方拿到编码后的数据并返回给系统,这一帧对应的输入计数和输出计数都应该清掉,驱动维护的内部引用计数归零,缓冲区随即可被复用或释放。一旦某个环节少了一次 release,或者驱动在某种异常状态下忘了把 buffer 回收到空闲队列,这个缓冲区就会永久挂在进程的 fd 表里,变成“僵尸块”。

4.2 泄漏产生的常见路径:重连、断流、动态码率切换

我用 strace 跟踪 media.hwcodec 的 fd 变化,再配合 dumpsys media.codec 观察编码器状态,最终看到的泄漏路径主要出现在下面几个场景。

第一个场景是 scrcpy 网络抖动的瞬间。工位机所在厂区 WiFi 环境很不稳定,scrcpy 会频繁尝试重连。正常情况下的重连会先 stop 掉当前编码器实例再重新 start,但实际跟踪发现,每次重连后 media.hwcodec 的 open fd 数量都会比之前多一截,而不是回到基准值。这说明前一个编码器实例的有些 buffer 并没有随着 stop 调用被完全释放。

第二个场景是动态码率调整。scrcpy 会根据网络带宽实时调整编码参数,而 Rockchip 编码器驱动在某些版本上处理码率切换时存在一个已知的竞态:编码器工作线程还在处理旧配置的输入帧,用户态已经把 codec 的输入 surface 释放了,驱动里的引用计数减不到零,buffer 直接泄漏。这个和具体内核版本强相关,我在社区里翻到过类似的反馈,不是孤例。

第三个场景是分辨率切换。工位机偶尔会接到不同外接屏幕的投屏请求,分辨率一变,编码器就得重新分配一组更大的 buffer,旧 buffer 理论上应该被释放。实测中旧 buffer 的释放并不彻底,每次分辨率切换都残留几十 MB,日积月累就会非常可观。

4.3 工位机场景下的“帮凶”:长时间运行 + 低内存配置

同样的泄漏路径,在用户手机上可能没那么严重,因为手机内存普遍 8GB/12GB,泄漏几十 MB 根本不起眼,而且用户也不会一整天持续投屏。但工位机是 4GB 内存的嵌入式设备,scrcpy 是常驻连续运行的,一天 24 小时几乎不停,每小时的泄漏速率哪怕只有五六十 MB,十个小时就是五六百 MB,到了晚上内存必然告急。

再加上工位机主业务 App 本身就常驻一些二维码识别、语音播放等模块,内存基线已经不低,再叠加 scrcpy 泄漏的内存,整个系统的内存水位被推到了非常危险的边缘。等到 lowmemorykiller 开始批量杀进程,几乎所有前台服务和系统界面都逃不掉,这时表现出的“黑屏卡死”其实就是系统在内存耗尽前做的一系列挣扎动作,只是用户感知上觉得 App 才是万恶之源。

5. 止血、根治与长期防治:我们最后是怎么收场的

5.1 临时止血:先让产线不要继续断线

根因定位到这一步,第一优先级是恢复产线稳定,不能为了等完美补丁让几十台工位机一直死循环。我和团队商量后,从两个方向同时止血。

第一个方向是把 scrcpy 的帧率限制到 10fps,码率上限调低,同时关闭 scrcpy 的自动重连机制,改为中控台侧脚本检测断线后手动或定时重连。这样做的目的是降低触发竞态的频率,让编码器不至于频繁被 stop/start 和动态切换参数。实测下来,单纯限帧率就能把泄漏速率降低约 70%,系统从两小时卡死延长到七八小时才出现明显恶化。

第二个方向是直接改投屏链路,舍弃 scrcpy 的画面采集方式。我们队里临时用 Android 原生 MediaProjection + 软编 X264 方案做个了内网投屏工具,CPU 占用确实高一些,但内存曲线非常稳定,跑了一整天也没有 DMA-BUF 暴涨的迹象。这个方案适合小规模先顶着用,产线几十台机器勉强顶得住,再往后还是得靠彻底修复。

5.2 根治:升级 BSP 内核驱动与 scrcpy 版本

止血之后,根治方案分成上游升级和本地修复两路推进。上游路径是把 Rockchip BSP 里的 ION/DMA-BUF 和编码器驱动升级到官方修复版本,重点是更新 rk_vcodec_enc 和相关内存管理模块;同时把 scrcpy 从定制老版本升级到 2.4 以上的官方稳定版,期间对比验证新版本在重连和动态码率切换下 fd 数量是否还异常增长。我们另外也把 MediaCodec 调用代码里的自定义修改点逐一 review,去掉了一份画质增强补丁,因为该补丁改了编码器的输入 surface 引用方式,泄漏风险更高。

这里的经验是:定制的系统级组件越少越好,尤其是编解码这种底层链路,任何一层的“小改动”都可能掩盖驱动层的引用计数问题。升级后先在测试机上连续跑满 72 小时,确认 DMA-BUF 数量稳定、MemAvailable 曲线平稳,才敢往产线几十台机器上批量刷。

5.3 监控和预警:把 DMA-BUF 泄漏消灭在萌芽阶段

无论升级多彻底,现场环境千奇百怪,还是要有监控预案。我给这批工位机加了两个轻量巡检。

第一个是内存水位巡检,每分钟读取 /proc/meminfo 的 MemAvailable,连续十分钟低于 500MB 就告警,同时自动抓取 dmesg 尾部、dumpsys meminfo、dma_buf 统计,方便事后追溯。

第二个是 DMA-BUF 专项巡检,通过 debugfs 统计 media.hwcodec 进程持有的 bufinfo 数量和总大小,设定阈值,如果两小时内持续增长超过 200MB 就自动重启 scrcpy 服务,而不是让整个系统走到崩溃边缘。

#!/system/bin/sh while true; do MEM=$(cat /proc/meminfo | grep MemAvailable | awk '{print $2}') if [ "$MEM" -lt 500000 ]; then echo "$(date) low memory: ${MEM}kB" >> /data/monitor.log cat /sys/kernel/debug/dma_buf/bufinfo >> /data/dma_on_lowmem.log fi sleep 60 done

预警脚本不用写得多复杂,关键是有人会去看日志、去设阈值,否则跟没监控一样。

6. 这次排障留下的通用方法论:别被表象带节奏

整个排查过程花了两天时间,如果回头复盘,最想拿出来分享的其实不是 scrcpy 这个具体工具本身的问题,而是“当系统表象指向 App 的时候,怎么坚定地往下层走”。

我个人的体会是,面对工控类 Android 设备的卡死问题,先建立一份完整的系统级资源画像,包括 CPU 各核心占用、进程内存、内核内存、DMA-BUF 数量、内核日志和 IO 统计,先别急着打开 Android Studio 去看崩溃堆栈。崩溃日志只能告诉你哪一层先倒,告诉不了你谁是推倒它的那只手。这次要不是坚持用 bufinfo 和 ion heap 统计抓到了 DMA-BUF 数量的单调递增趋势,大概率还陷在反复加固 App 的泥潭里,产线停机损失只会更大。

另外还有一个小技巧,凡是遇到周期性问题,第一时间怀疑“泄漏类”而不是“偶发异常类”,然后做对照实验验证,比盲目读代码效率高很多。我们后来在另一个项目里用同样的方法论,只花了半天就定位到一台设备显示屏异常花屏,根因是 GPU 驱动的 framebuffer 引用没有释放,套路完全是相通的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询