一次 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、媒体编码器和虚拟显示等系统级资源。

相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen1 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui