26. 内存管理与泄漏排查:媒体内存分配策略、Native Heap分析、常见泄漏模式

内存泄漏,说白了就是“借了不还”。在Stagefright这种长期运行的媒体服务里,哪怕一次泄漏几KB,跑上几个小时,系统就可能卡顿甚至崩溃。我见过不少App因为MediaCodec没释放,最后被系统直接杀掉——嗯,这种问题排查起来特别头疼。

今天我们就来聊聊Stagefright的内存管理。我会结合自己的踩坑经历,帮你理清分配策略、分析工具,以及那些最容易出问题的泄漏模式。

26.1 媒体内存分配策略

Stagefright的内存分配,不是简单的malloc/free。它有一套自己的策略,主要分三层:

层级 分配方式 典型场景
应用层 Java Heap / ByteBuffer MediaCodec输入输出缓冲区
Native层 IMemory / MemoryHeapBase 解码后的原始帧数据
驱动层 ION / DMA-BUF 硬件编解码器直接访问

我个人习惯把重点放在Native层。因为大部分泄漏都发生在这里——Java层有GC兜底,驱动层有内核回收,唯独Native层,全靠开发者自觉。

26.2 Native Heap分析

Native Heap泄漏,是Stagefright里最常见的“隐形杀手”。为什么说它隐形?因为系统不会主动告诉你,只有等到OOM了,你才后知后觉。

26.2.1 常用分析工具

  • malloc debug:Android自带的Native内存调试工具。设置libc.debug.malloc=1,就能追踪每次分配和释放。
  • AddressSanitizer (ASan):编译时开启,能检测越界和use-after-free。不过性能开销大,适合调试版。
  • SimplePerf / Perfetto:用来分析内存分配热点,看哪个函数分配最多。
我的经验: 在项目里,我习惯先用malloc debug抓一次全量分配日志,然后对比两次快照。如果某个地址只增不减,那基本就是泄漏了。

26.2.2 实战:用malloc debug定位泄漏

假设你怀疑MediaCodec解码后内存没释放。可以这样操作:

# 开启malloc debug
adb shell setprop libc.debug.malloc 1
adb shell stop
adb shell start

# 复现问题后,抓取分配日志
adb shell dumpsys meminfo mediaserver

# 或者用更细粒度的方式
adb shell am start -n com.example/.MainActivity
adb shell kill -3 $(pidof mediaserver)

拿到日志后,重点关注Native Heap那一栏。如果Pss持续增长,说明有泄漏。我曾经在某个项目里,发现每次播放视频后,Native Heap都会涨2MB,最后定位到是MediaCodecdequeueOutputBuffer返回的buffer没及时release。

26.3 常见泄漏模式

根据我的经验,Stagefright里的泄漏,翻来覆去就那么几种模式。我总结了一下:

26.3.1 Buffer未释放

这是最常见的。解码器输出buffer,你用完了,但没调用releaseOutputBuffer。或者调用了,但参数传错了——比如传了false而不是true,导致buffer没真正还给解码器。

注意: 有些厂商的HAL实现有bug,即使你正确调用了release,底层也可能没释放。这时候需要抓HAL层的log来确认。

26.3.2 循环引用

Stagefright里大量使用sp<>智能指针。如果两个对象互相持有对方的sp,就会形成循环引用,导致两者都无法释放。

举个例子:

class MediaCodecSource : public MediaSource {
    sp<MediaCodec> mCodec;
};

class MediaCodec : public RefBase {
    sp<MediaCodecSource> mSource;  // 循环引用!
};

解决办法?用wp<>弱指针打破循环。或者手动在析构函数里置空。

26.3.3 匿名共享内存泄漏

Stagefright经常用MemoryHeapBase分配匿名共享内存。如果MemoryHeapBase对象销毁时,对应的fd没有关闭,就会泄漏。这种泄漏在/proc/self/maps里能看到大量/dev/ashmem映射。

我记得有一次,客户反馈播放4K视频时,系统内存暴涨。查了半天,发现是OMXCodec里有个MemoryHeapBase在异常路径下没释放。修复后,内存占用直接降了30%。

26.4 知识体系图

下面这张图,帮你理清Stagefright内存管理的核心脉络:

Stagefright 内存管理核心脉络 应用层 (Java Heap) Native层 (IMemory) 驱动层 (ION/DMA-BUF) 常见泄漏模式 Buffer未释放 releaseOutputBuffer遗漏 循环引用 sp<>互相持有 匿名共享内存泄漏 fd未关闭 分析工具 malloc debug AddressSanitizer SimplePerf dumpsys meminfo 核心原则:谁分配,谁释放;异常路径也要释放

26.5 避坑指南

最后,分享几个我踩过的坑:

  • 不要依赖智能指针的自动释放。在异步回调里,sp的引用计数可能因为多线程问题而异常。我建议在关键路径上手动调用clear()
  • 注意HAL层的实现差异。不同厂商的OMX实现,对buffer生命周期的管理可能不同。比如有的厂商要求你显式调用freeBuffer,有的则自动回收。最好在集成测试里覆盖一下。
  • 善用libmemtrack。它能帮你追踪GPU/多媒体相关的内存分配。我在排查一个视频卡顿问题时,就是靠它发现某个厂商的HAL层泄漏了连续物理内存。

一句话总结: Stagefright的内存管理,核心就是“谁分配,谁释放”。但难点在于异步回调、多线程、以及厂商实现的差异。用好malloc debug和dumpsys,大部分泄漏都能快速定位。


公众号:蓝海资料掘金营,微信deep3321