第4章 视频解码内存泄漏:MediaCodec生命周期、Surface回调、参考帧未释放的现场采集

章节目标:掌握视频解码场景下OOM的现场特征、三类核心泄漏根因的定位方法,以及从现象到修复的完整排查链路。

4.1 现象识别——解码OOM的典型“指纹”

视频解码OOM并非随机发生,它有清晰的“指纹”模式。以下现象同时出现越多,MediaCodec相关泄漏的可能性就越高:

  • 播放时间关联:应用在连续播放10~30分钟后发生OOM,且时间点随播放时长前移(复现越来越快)。
  • 多路放大效应:同时播放2路以上视频时,OOM出现时间呈指数级缩短,单路可能不崩溃。
  • 内存阶梯上升:每切换一个视频片段或旋转一次屏幕,Native Heap上涨3~8 MB,不回落。
  • 分辨率敏感:1080p及以上视频更容易触发,480p视频可较长时间运行。
解码OOM现象分级判定表
观察项 轻度(可疑) 中度(高度疑似) 重度(基本确认)
单路播放内存增量 每5分钟上涨 < 2 MB 每5分钟上涨 2~5 MB 每5分钟上涨 > 5 MB
旋转屏幕后内存 上涨 < 1 MB 且能回落 上涨 1~3 MB 不回落 上涨 > 3 MB 持续累积
多路播放触发时间 >30分钟 10~30分钟 < 10分钟
注意:如果内存曲线呈现“锯齿状上涨”(每次解码新片段时跳升,渲染完成后不回落),应优先排查MediaCodec实例泄露和参考帧堆积,而非通用堆泄漏。

4.2 数据采集——锁定泄漏现场的现场工具链

当现象匹配后,按以下顺序采集现场证据,避免关键信息丢失。

步骤1:实时监控实例数与内存

使用以下命令每5秒采样一次,观察MediaCodec实例和Native Heap的变化趋势:

# 采集MediaCodec实例数量(Android 10+)
adb shell dumpsys media.codec | grep -E "Codec|Instance|Alloc" | tee codec_instances.log

# 采集进程Native Heap
adb shell dumpsys meminfo <pid> | grep -E "Native Heap|Total" | tee native_heap.log

# 连续采样脚本(每5秒)
while true; do
    echo "--- $(date +%H:%M:%S) ---" >> codec_trace.log
    adb shell dumpsys media.codec 2>/dev/null | grep -c "Instance" >> codec_trace.log
    adb shell dumpsys meminfo <pid> 2>/dev/null | grep "Native Heap" >> codec_trace.log
    sleep 5
done

步骤2:捕获MediaCodec生命周期日志

设置日志过滤器,捕获MediaCodec的创建、配置、启动、停止、释放事件:

adb logcat -v threadtime -s MediaCodec:* MediaCodecStats:* NuPlayer:* | tee media_codec_lifecycle.log

关键日志标记:

  • MediaCodec: configure — 记录配置参数与mime类型
  • MediaCodec: start — 解码器启动
  • MediaCodec: stop — 解码器停止(应配对出现)
  • MediaCodec: release — 实例释放(缺失表示泄漏)

步骤3:堆转储分析Surface引用链

当怀疑Surface回调泄漏时,获取堆转储并分析Surface及关联对象的引用路径:

# 触发堆转储
adb shell am dumpheap <pid> /data/local/tmp/heap_dump.hprof
adb pull /data/local/tmp/heap_dump.hprof .

# 使用MAT或Memory Analyzer分析
# 搜索 "android.view.Surface" 和 "android.media.MediaCodec"
# 关注 GC Root 路径中是否存在 Activity / Fragment 的强引用链
现场采集铁律:在OOM发生前至少采集3组dumpsys meminfo和2次堆转储(一次在播放稳定期,一次在内存高点)。不要在OOM后重启进程再采集,那会丢失native堆现场。

4.3 原因定位——MediaCodec内存泄漏故障树

综合现场数据,使用以下故障树对根因做逐级排查。三个分支分别对应MediaCodec生命周期、Surface回调、参考帧未释放三类泄漏。

MediaCodec内存泄漏故障树分析 MediaCodec 内存泄漏故障树分析 视频解码 OOM MediaCodec 生命周期泄漏 Surface 回调泄漏 参考帧未释放 未调用 release() 重复创建未释放 回调未正确注销 Surface 未回收 参考帧队列膨胀 渲染消费慢堆积 关键排查指标 ① dumpsys media.codec 中 Instance 数量持续增长 → 生命周期泄漏分支 ② Surface 关联的 Native Memory 在 heap dump 中无法被 GC 回收 → Surface 回调泄漏分支 ③ 解码器 OutputBuffer 消费速率 < 解码速率 且 参考帧队列持续膨胀 → 参考帧未释放分支 ④ 同时命中多个分支时,按泄漏量从大到小排序修复(优先修复生命周期分支)

判断规则与排查表

根据故障树分支,结合采集数据按下表快速定位根因:

MediaCodec 泄漏根因排查表
故障树分支 判定证据(日志/内存特征) 确认方法
MediaCodec 生命周期泄漏
  • dumpsys media.codec 中 Instance 数量随播放次数线性增长
  • logcat 中 MediaCodec: start 和 MediaCodec: release 不配对
在每次播放结束处加日志,确认 release() 是否到达
Surface 回调泄漏
  • 旋转屏幕后 Surface 关联对象在 heap dump 中仍被 Activity 强引用
  • Native Heap 中 Surface 相关 alloc 持续上升
MAT 分析 Surface 及 SurfaceTexture 的 GC Root 路径
参考帧未释放
  • 解码器私有内存(/proc/pid/smaps 中 native heap)持续膨胀
  • 渲染线程帧消费时间 > 解码帧间隔
对比 dumpsys gfxinfo 帧时间与解码帧率
经验规则:如果同时出现多个分支症状,优先修复“生命周期泄漏”——它通常贡献了60%~80%的泄漏量,且修复成本最低。

4.4 修复动作——针对三类根因的手术刀方案

修复一:MediaCodec 生命周期泄漏

  • 强制配对释放:在 stop() 和 release() 调用周围使用 try/finally 块,确保异常路径也能释放。
  • 实例池管理:使用对象池或弱引用持有 MediaCodec 实例,避免重复创建新实例。
  • 状态机校验:在调用 start() 前检查当前状态是否为 Configured,避免在 Executing 状态下重复 start。

修复二:Surface 回调泄漏

  • 弱引用回调:将 Surface.Callback 和 OnFrameAvailableListener 内部类改为静态内部类 + WeakReference 持有外部对象。
  • 显式注销:在 onPause() 或 onDestroy() 中调用 surfaceView.getHolder().removeCallback(this)。
  • Surface 生命周期对齐:在 surfaceDestroyed() 回调中立即将 MediaCodec 的 output surface 置为 null,并调用 releaseOutputBuffer() 清空等待帧。

修复三:参考帧未释放

  • 解码速度节制:根据 Surface 的帧消费反馈动态调节 queueInputBuffer() 的速度,使用 dequeueOutputBuffer() 返回的缓冲数量作为背压信号。
  • 输出缓冲及时回收:在 onFrameAvailable 或渲染线程中尽快调用 releaseOutputBuffer(renderIndex, false),避免输出队列堆积。
  • 关键帧间隔优化:对于低端设备,建议编码端增大关键帧间隔(GOP),减少解码器对长期参考帧的依赖。
修复验证小技巧:在修复代码中添加“泄露看门狗”——每隔30秒检查 MediaCodec 实例数,如果超过预期最大值(例如单路播放为1,双路为2),主动触发一次 dump 并报警。

4.5 复测——验证泄漏是否真正关闭

修复完成后,按照以下标准执行复测,确认泄漏已消除:

  1. 长稳测试:单路播放1080p视频连续2小时,每5分钟记录一次 Native Heap 大小,要求全程波动在 ±1 MB 以内。
  2. 旋转压力:每10秒旋转一次屏幕,连续100次,每次旋转后内存应回落到基准值。
  3. 多路并发:同时播放3路720p视频运行30分钟,Native Heap 不再呈现阶梯式上升。
  4. 实例计数:复测期间 dumpsys media.codec 中 Instance 数量应恒定为预期值(单路=1,双路=2)。
复测通过判定标准
测试项 合格值 警戒值 失败值
2小时Native Heap增量 ≤ 2 MB 2~5 MB > 5 MB
旋转100次后内存 ≤ 基准值 + 1 MB 基准值 + 1~3 MB 基准值 + > 3 MB
多路实例数 等于路数 等于路数 + 1 大于路数 + 1

4.6 预防——建立解码内存健康体系

从流程和监控两个维度建立预防机制,避免同类问题再次出现:

  • 代码审查检查清单:所有 MediaCodec 相关合并请求必须包含以下检查项:
    • 是否所有 exit 路径都调用了 release()?
    • Surface Callback 是否使用弱引用注册?
    • 是否有针对 dequeueOutputBuffer 返回 INFO_OUTPUT_BUFFERS_CHANGED 的处理?
  • CI 集成内存基线:在自动化测试中插入 dumpsys meminfo 采样,对比每次构建的 Native Heap 基线,超过阈值则告警。
  • 运行时监控仪表板:线上版本集成轻量级指标采集,上报 MediaCodec 实例数、Native Heap 使用率、Surface 消费帧率三个核心指标。
  • 定期故障演练:每季度使用故障注入工具模拟三类泄漏场景,验证监控和告警链路是否正常。
预防红线:不要在 onFrameAvailable 或 surfaceCreated 回调中执行 MediaCodec 的 start() 或 configure() 操作。这些回调