一次 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;Graphics和Unknown。
整机和 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
临时规避与长期方案
临时规避
- 3576SE 长时间稳定性测试时不要启动 scrcpy 或 Android Studio 设备镜像。
- USB 可以保持连接,仅使用普通
adb logcat不会创建视频编码链路。 - 使用完 scrcpy 后先关闭窗口并确认设备端进程消失,再拔线。
- 必须投屏时降低分辨率和帧率,并关闭音频:
bash
scrcpy --max-size=1280 --max-fps=15 --no-audio
- 如果设备提供软件 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、媒体编码器和虚拟显示等系统级资源。