一、解码器内存占用分析

做视频硬解码,内存问题永远是绕不开的坎。我刚开始接触MediaCodec时,总觉得它是个黑盒子,内存占用完全不可控。后来踩了不少坑,才慢慢摸清楚它的内存模型。

说白了,解码器的内存占用主要分三块:

  • 输入缓冲区:存放压缩数据,就是H.264/H.265码流
  • 输出缓冲区:存放解码后的YUV/RGBA帧
  • 解码器内部状态:各种参考帧、运动矢量等

其中输出缓冲区是最吃内存的。举个例子,1080p的YUV420帧,一帧就要占用1920×1080×1.5≈3MB。如果解码器内部维护了8-16个输出缓冲区,光这部分就要24-48MB。

关键数据:不同分辨率下,单帧YUV420的内存占用

分辨率单帧大小16帧缓冲池
720p (1280×720)~1.4MB~22MB
1080p (1920×1080)~3MB~48MB
4K (3840×2160)~12MB~192MB

我在项目中遇到过,某款低端机解码4K视频时直接闪退。查了半天,发现是解码器申请了16个输出缓冲区,每个12MB,光这就192MB了。加上其他内存开销,直接触发了系统杀进程。

二、Buffer池化:别再频繁申请释放了

很多新手写解码器,喜欢每帧都new一个ByteBuffer,用完就释放。你想想看,解码器每秒输出30帧,每帧3MB,频繁GC的代价有多大?

我建议的做法是:预分配一个Buffer池

// 伪代码示例:Buffer池化
class DecoderBufferPool {
    private val pool = LinkedList<ByteBuffer>()
    private val bufferSize: Int
    
    init {
        // 预分配8个缓冲区
        for (i in 0 until 8) {
            pool.add(ByteBuffer.allocateDirect(bufferSize))
        }
    }
    
    fun acquire(): ByteBuffer {
        return pool.poll() ?: ByteBuffer.allocateDirect(bufferSize)
    }
    
    fun release(buffer: ByteBuffer) {
        buffer.clear()
        pool.offer(buffer)
    }
}

这样做的好处很明显:

  • 减少GC压力,避免卡顿
  • 内存分配次数从每秒30次降到几乎为0
  • 内存碎片更少,系统更稳定

小技巧:Buffer池的大小不是越多越好。我个人习惯设置为解码器输出缓冲区数量的1.5倍。比如解码器有8个输出缓冲区,池子就设12个。多了浪费,少了不够用。

三、纹理内存管理:SurfaceView vs TextureView

说到纹理内存,就不得不提SurfaceView和TextureView的选择。很多人觉得TextureView更灵活,就无脑用。但你知道吗?TextureView内部维护了一个独立的GL纹理,这本身就要占用显存。

我曾经在一个项目中,用TextureView显示4K视频,结果发现显存占用比SurfaceView多了将近100MB。为什么?因为TextureView需要把解码器的输出帧先拷贝到自己的纹理上,再渲染到屏幕。而SurfaceView直接共享了解码器的输出缓冲区,省去了这份拷贝。

注意:如果你不需要对视频帧做二次处理(比如加滤镜、旋转),请优先使用SurfaceView。它更省内存,性能也更好。

那什么时候用TextureView?

  • 需要对视频帧做动画、缩放、旋转
  • 需要与其他View叠加显示
  • 需要截取视频帧做后续处理

嗯,这里要注意:即使用了TextureView,也要控制纹理的尺寸。不要直接使用解码器输出的原始分辨率,可以适当缩小。比如4K视频显示在1080p的屏幕上,纹理尺寸设为1080p就够了,没必要用4K纹理。

四、OOM预防:别等到崩溃才后悔

OOM(Out Of Memory)是Android开发者的噩梦。在视频解码场景下,OOM尤其容易发生。我总结了几条经验:

  1. 监控内存水位:在解码循环中定期检查可用内存,低于阈值时主动降级
  2. 限制解码器数量:不要同时创建多个解码器实例,每个解码器都是内存大户
  3. 及时释放资源:MediaCodec的release()一定要在finally块中调用
  4. 使用软硬解码切换:当硬解码内存不足时,可以回退到软解码
// 内存监控示例
fun checkMemorySafety(): Boolean {
    val runtime = Runtime.getRuntime()
    val usedMem = runtime.totalMemory() - runtime.freeMemory()
    val maxMem = runtime.maxMemory()
    val threshold = (maxMem * 0.8).toLong()
    
    if (usedMem > threshold) {
        Log.w("Decoder", "内存使用超过80%,准备降级")
        return false
    }
    return true
}

避坑指南:我曾经在某个项目里,因为忘记释放解码器,导致连续播放20个视频后OOM。查了三天才发现是MediaCodec的release()没调用。从那以后,我所有解码器都加上了try-finally保护。

还有一个容易被忽略的点:Bitmap的内存管理。如果你从解码器输出帧创建Bitmap,记得用BitmapFactory.Options的inBitmap复用内存。否则每帧都创建新Bitmap,OOM只是时间问题。

视频硬解码内存优化全景图 内存占用分析 Buffer池化 纹理内存管理 OOM预防 输入缓冲区 输出缓冲区 解码器内部状态 (参考帧/运动矢量) 预分配缓冲区 减少GC压力 避免频繁分配 (池大小=1.5倍) SurfaceView TextureView 纹理尺寸控制 (避免4K纹理) 内存水位监控 限制解码器数量 及时释放资源 (软硬解码切换) 核心目标:在有限内存下,稳定流畅地解码视频 分析 → 池化 → 管理 → 预防,四步走搞定内存优化

最后说一句,内存优化没有银弹。不同设备、不同分辨率、不同编码格式,表现都可能不一样。我的习惯是:先分析,再优化,最后验证。用Profiler跑一遍,看看内存峰值在哪,再针对性地做池化、降级、复用。别一上来就堆代码,那样反而容易出问题。