一次 Android 工位机黑屏卡死排查:根因不是 App,而是 scrcpy 与 Rockchip 编码器的 DMA-BUF 泄漏

一次 Android 工位机黑屏卡死排查:根因不是 App,而是 scrcpy 与 Rockchip 编码器的 DMA-BUF 泄漏

背景

设备为 3576SE 工位机,芯片平台为 Rockchip RK3576,内存约 4GB。现场现象是:只打开 React Native App,停留在首页或任意普通业务页面,运行一段时间后整机黑屏、触摸无响应,最终只能断电重启。

由于 App 包含人脸识别、USB 打印机、串口读卡器、APK 自动更新等功能,最初怀疑方向包括:

  • Java/JS 堆内存泄漏;
  • CameraX 或人脸 SDK 没有释放图像帧;
  • USB 打印机重连线程异常;
  • 网络轮询或 APK 下载持续占用资源。

但完整日志和后续对照实验表明,这次故障的主因不是 App 页面,而是 scrcpy 投屏使用 Rockchip 硬件编码器时,视频 DMA-BUF 缓冲区持续累积

故障日志中的决定性证据

1. 系统因内存和 Swap 耗尽杀死前台 App

故障发生时,lmkd 给出了非常明确的原因:

text 复制代码
2026-09-02 10:20:21.068 lowmemorykiller I
Kill 'com.matsuokaworkstation' (6339), uid 10139, oom_score_adj 0
to free 69552kB rss, 345460kB swap;
reason: device is low on swap (78796kB < 199540kB)
and thrashing (203%)

紧接着可以看到:

text 复制代码
Process com.matsuokaworkstation (pid 6339) has died: fg TOP
Process 6339 exited due to signal 9 (Killed)
WIN DEATH: com.matsuokaworkstation/.MainActivity

这不是普通 Java 异常,也不是 App 主动退出,而是系统在严重内存压力下向前台 App 发送了 SIGKILL。同一时段,桌面、设置、WebView、媒体服务等大量系统进程也被连续杀死,因此最终表现为整机黑屏和无法操作。

2. Java 堆没有出现典型泄漏或 OOM

App 的 GC 日志为:

text 复制代码
Background concurrent mark compact GC freed 197646(16MB) objects,
50% free, 16MB/32MB

日志中没有发现:

  • OutOfMemoryError
  • FATAL EXCEPTION
  • App ANR;
  • Native crash 或 SIGSEGV

这不能绝对证明 App 不存在任何 native 泄漏,但可以排除"Java 堆不断增长并最终 OOM"这一典型路径。

3. DMA-BUF 占用了约 3.1GB

系统低内存快照显示:

text 复制代码
Total RAM: 约 4GB
Free RAM: 57,744K
DMA-BUF: 3,121,276K
GPU: 171,884K
Used RAM: 4,281,297K

DMA-BUF 占当时已用内存约 73%。这说明大量内存被图形或视频硬件缓冲区占用,而不是位于 App 的 Java 堆中。

4. Rockchip Codec2 缓冲池持续单向增长

android.hardware.media.c2@1.1-service 中同一个缓冲池持续增长:

text 复制代码
09:50  1076 buffers, 2,247,720,960 bytes used
09:55  1163 buffers, 2,429,460,480 bytes used
10:00  1213 buffers, 2,533,908,480 bytes used
10:05  1257 buffers, 2,625,822,720 bytes used
10:10  1306 buffers, 2,728,181,760 bytes used
10:15  1360 buffers, 2,840,985,600 bytes used
10:20  1528 buffers, 3,191,930,880 bytes used

半小时内又增加了约 944MB,而且已使用缓冲区数量只增不减。

每个缓冲区大小为:

text 复制代码
3,191,930,880 / 1528 = 2,088,960 bytes
2,088,960 = 1920 × 1088

1920×1088 正是 1920×1080 视频画面经过编码器高度对齐后的典型尺寸。

5. 缓冲区属于 scrcpy 投屏链路,不属于 App

系统明确记录了 scrcpy 虚拟显示:

text 复制代码
DisplayDeviceInfo{"scrcpy",
1920 x 1080,
renderFrameRate 60.0,
type VIRTUAL,
owner com.android.shell}

设备上同时存在两个 scrcpy 相关的 app_process,PID 为 13023 和 13038。硬件编码器日志中出现:

text 复制代码
C2RKMpiEnc I IDR frame produced

C2RKMpiEnc 是 Rockchip MPP 硬件编码组件。

更关键的是,系统对业务 App 的媒体资源统计明确写着:

text 复制代码
No Video Codec Entry for Application[pid(6339): uid(10139)]
Peak { AudioDec[ SW: 1 ] } Peak Pixels: 0

也就是说,故障发生时业务 App 没有打开视频编码器,视频像素峰值也是 0。这与现场"从未进入人脸识别页面"的操作完全一致。

6. scrcpy 退出后,3.19GB 缓冲区立即释放

故障末尾的释放顺序如下:

text 复制代码
10:20:38.518 adbd: USB connection terminated
10:20:38.540 scrcpy: Audio capture error
10:20:38.610 Virtual display released
10:20:38.679 scrcpy: Cleaning up
10:20:38.704 Codec2: Client died, release the component
10:20:38.734 1528 total buffers - 0 used buffers
10:20:39.465 C2RKMpiEnc: onRelease

编码器客户端死亡后,原本占用 3.19GB 的 1528 个缓冲区立刻变成 0。这个时间关系把内存增长、scrcpy、Rockchip 编码器和最终释放完整串了起来。

为什么拔掉 USB 后仍可能继续占用

正常情况下,USB/ADB 断开后,设备端 scrcpy server 应该退出并释放虚拟屏幕和编码器。电脑上留下一个空的 scrcpy 窗口,本身不会继续消耗设备内存。

但本次日志显示,业务 App 在 10:20:21 已被系统杀死,设备直到 10:20:38 才记录到 ADB USB transport 终止,随后 scrcpy 才开始清理。这说明设备端 scrcpy server 在故障前一直存活。

可能的链路是:

text 复制代码
USB 物理连接异常或被拔出
    ↓
Rockchip USB gadget / adbd 未及时完成断连通知
    ↓
设备端 scrcpy 仍持续向 C2RKMpiEnc 提交画面
    ↓
电脑端不再正常消费编码结果
    ↓
Codec2 缓冲区没有回收并不断累积
    ↓
DMA-BUF 达到约 3GB,系统进入 Swap thrashing
    ↓
lmkd 连续杀进程,最终整机黑屏

因此,判断 scrcpy 是否已经停止,不能只看电脑窗口或 USB 线,应该检查设备端进程和虚拟显示:

bash 复制代码
adb shell ps -A | grep -i scrcpy
adb shell dumpsys display | grep -i -C 3 scrcpy

需要主动结束时,可在拔线前执行:

bash 复制代码
adb shell pkill -f scrcpy

21 小时无 scrcpy 对照实验

为了确认是否是 App 自身常驻泄漏,重启设备后进行了对照实验:

  • 不启动 scrcpy 或 Android Studio 设备镜像;
  • USB 仅用于普通 ADB;
  • App 停留在首页或普通业务页面;
  • 每分钟采样一次;
  • 截至分析时已持续约 21 小时,共 1227 次有效采样。

结果如下:

指标 开始 结束 全程范围
DMA-BUF 101MB 101MB 始终为 101MB
App PSS 193MB 188MB 186~194MB
App RSS 310MB 308MB 305~313MB
App Swap PSS 0.58MB 0.56MB 基本不变
Native Heap 已分配 64.5MB 60.4MB 58.8~65.8MB
整机已用 RAM 1.22GB 1.23GB 1.20~1.24GB
整机空闲 RAM 2.74GB 2.72GB 始终高于 2.70GB

测试期间:

  • DMA-BUF 没有增长 1KB;
  • App 内存没有单向增长;
  • App PID 保持不变;
  • 没有出现 OOM、ANR、LMKD 杀进程或 App 崩溃。

对照结果非常明显:

text 复制代码
启用 scrcpy:约 30 分钟内 DMA-BUF 2.25GB → 3.19GB → 整机卡死
关闭 scrcpy:约 21 小时内 DMA-BUF 始终为 101MB → 系统稳定

这基本确认了故障触发条件在 scrcpy 与 Rockchip 编码链路,而不是 App 停留首页或普通业务页面。

无需常驻电脑的内存采样方法

为了在拔掉 USB 后继续记录,使用 ADB 在设备端启动一个临时后台 Shell 进程:

bash 复制代码
adb shell "nohup sh -c '
  i=0
  while [ \$i -lt 4320 ]; do
    date +%Y-%m-%dT%H:%M:%S
    dumpsys meminfo | grep -E \"Total RAM|Free RAM|Used RAM|Lost RAM|ZRAM|DMA-BUF|GPU\"
    dumpsys meminfo com.matsuokaworkstation | grep -E \"TOTAL PSS|TOTAL RSS|TOTAL SWAP PSS|Native Heap|Dalvik Heap|Unknown\"
    echo SAMPLE_END
    i=\$((i+1))
    [ \$i -ge 4320 ] && break
    sleep 60
  done
  echo MONITOR_FINISHED_72H_1MIN
' >> /sdcard/Download/matsuoka_mem_monitor.log 2>&1 < /dev/null &"

该方案的特点:

  • 每分钟采样一次;
  • 4320 次约等于 72 小时;
  • USB 断开后继续运行;
  • 设备重启后停止,不会自启动;
  • 不修改 App,也不是 Android JobScheduler 或 cron;
  • 72 小时日志预计仅数 MB。

重新连接设备后拉取日志:

bash 复制代码
adb pull /sdcard/Download/matsuoka_mem_monitor.log

排查这类问题时应该看什么

App 内存

bash 复制代码
adb shell dumpsys meminfo com.matsuokaworkstation

重点观察:

  • TOTAL PSS
  • TOTAL RSS
  • TOTAL SWAP PSS
  • Native Heap
  • Dalvik Heap
  • GraphicsUnknown

整机和 DMA-BUF

bash 复制代码
adb shell dumpsys meminfo | grep -E "Total RAM|Free RAM|Used RAM|ZRAM|DMA-BUF|GPU"

如果 App 堆很平稳,但 DMA-BUF、GPU 或 Lost RAM 持续增长,应把排查重点转向相机、视频编码、虚拟显示、Surface、硬件驱动等 native 链路。

媒体编码缓冲池

bash 复制代码
adb logcat -s BufferPoolAccessor2.0 C2RKMpiEnc lowmemorykiller

重点关注:

  • 同一个 buffer pool 的总数是否单向增长;
  • used buffers 是否长期不下降;
  • 单个 buffer 大小是否与屏幕或相机分辨率吻合;
  • 客户端退出后缓冲区是否立即归零。

scrcpy 是否残留

bash 复制代码
adb shell ps -A | grep -i scrcpy
adb shell dumpsys display | grep -i -C 3 scrcpy

临时规避与长期方案

临时规避

  1. 3576SE 长时间稳定性测试时不要启动 scrcpy 或 Android Studio 设备镜像。
  2. USB 可以保持连接,仅使用普通 adb logcat 不会创建视频编码链路。
  3. 使用完 scrcpy 后先关闭窗口并确认设备端进程消失,再拔线。
  4. 必须投屏时降低分辨率和帧率,并关闭音频:
bash 复制代码
scrcpy --max-size=1280 --max-fps=15 --no-audio
  1. 如果设备提供软件 H.264 编码器,可以避开 Rockchip 硬件编码器。先查询编码器:
bash 复制代码
scrcpy --list-encoders

然后按设备实际名称指定,例如:

bash 复制代码
scrcpy --video-encoder=c2.android.avc.encoder --max-size=1280 --max-fps=15 --no-audio

长期方案

  • 升级 scrcpy 并复测;
  • 升级设备固件、Rockchip BSP、Codec2/MPP 组件;
  • 将日志和复现步骤提交给设备厂商,重点说明 C2RKMpiEnc、DMA-BUF 单向增长以及 USB 断开后清理延迟;
  • 如果必须长期远程控制,评估软件编码器或不经过设备端硬件 MediaCodec 的方案。

容易误判的日志

故障日志中还出现了 App 每约 2 秒一次的:

text 复制代码
TrafficStats: tagSocket(...)

它只表示某个网络 Socket 被纳入流量统计,不能单独证明内存泄漏。代码全局检查也没有发现每 2 秒调用业务后端接口的页面;生产代码中的后端定时轮询只有消息列表,每 5 分钟一次。

网络连接频繁创建值得单独排查,例如调试包的 Metro/WebSocket 重连,但它无法解释 3GB 的视频 DMA-BUF,也不是本次整机黑屏的直接原因。

总结

这次问题最重要的经验是:前台 App 被 LMKD 杀死,不等于 App 就是内存泄漏源。

真正的因果链是:

text 复制代码
scrcpy 虚拟显示(1920×1080@60Hz)
    ↓
Rockchip C2RKMpiEnc 硬件编码
    ↓
1920×1088 DMA-BUF 缓冲区持续累积
    ↓
DMA-BUF 达到约 3.1GB
    ↓
系统 Swap thrashing 超过 200%
    ↓
lmkd 连续杀死 App、桌面和系统组件
    ↓
整机黑屏、触摸失效,只能断电

通过"不启用 scrcpy"的长期对照实验,App 在相同首页或普通业务页面下保持稳定,进一步确认了根因。排查 Android 整机卡死时,除了 App 堆内存,还必须同时检查 DMA-BUF、GPU、Swap、媒体编码器和虚拟显示等系统级资源。

相关推荐
Privasa-隐私实验室31 分钟前
跨端技术选型|Flutter搭建隐私加密App,安卓与iOS双端差异适配实战
android·笔记·安全·flutter·ios·隐私安全·aes-256
limuyang28 小时前
PDF Viewer KMP(基于 chrome 的 PDFium 内核)
android·kotlin
心平气和量大福大12 小时前
android-控件-搜索框SearchView
android·gitee
2601_9621771312 小时前
【MySQL篇】聚合查询,联合查询
android·java·mysql
壮哥_icon12 小时前
【Android】批量 VectorDrawable 动态分别修改 fillColor 和 strokeColor 的踩坑与终极解决方案
android
齐天qaq13 小时前
Android 系统 APK 分区与权限
android
心平气和量大福大13 小时前
android-控件-多选框CheckBox
android·gitee
我命由我1234514 小时前
Linux - Linux/POSIX 路径斜杠折叠规则
linux·运维·服务器·android studio·android jetpack·android-studio·android runtime