MediaCodec 缓冲区管理与数据流模型

缓冲区管理,说白了就是 MediaCodec 怎么跟你的业务线程“倒腾”数据。很多卡顿问题,根子就出在缓冲区调度上。我早期做短视频 SDK 时,经常遇到画面僵住一秒钟、日志里全是 dequeueInputBuffer timed out —— 后来才明白,是没弄懂它内部那套“队列哲学”。

核心数据流:生产者与消费者

MediaCodec 内部维护了两条生产者-消费者队列:输入队列和输出队列。你的应用线程往里放数据,Codec 的工作线程从另一端取出。

关键角色

  • 生产者:你的应用(通过 dequeueInputBuffer 获得一个空的输入 Buffer,填好数据后 queueInputBuffer 放回队列)。
  • 消费者:Codec 内部异步线程(从输入队列取数据,编码/解码,往输出队列放结果)。
  • 反向生产者:Codec(往输出队列里产出数据)。
  • 反向消费者:你的应用(通过 dequeueOutputBuffer 取出已处理的数据,用完 releaseOutputBuffer 归还)。

我的经验:我曾以为 queueInputBuffer 后数据就立刻被 Codec 处理了。实际上它只是进了队列,Codec 的内部调度可能滞后。尤其是低端芯片上,从入队到真正开始解码可能有 2~3 帧的延迟。你得接受这个“管道延迟”。

队列容量与阻塞机制

Android 官方的文档写得很简略,但队列深度是有默认值的。通常输入队列能容纳 8~32 个 Buffer,输出队列类似。当队列满了——

  • 输入队列满:dequeueInputBuffer 返回 -1,或者阻塞(取决于你传的超时参数)。
  • 输出队列满:Codec 内部停止解码,直到你 releaseOutputBuffer 腾出一个空位。

嗯,这里要注意:如果输出队列被塞满,输入也会被堵死。整条流水线就卡住了。

队列默认容量(常见)阻塞行为
输入缓冲区池8 ~ 16dequeueInputBuffer 超时返回 -1
输出缓冲区池4 ~ 8Codec 内部阻塞解码,不会丢帧

同步模式 vs 异步模式

MediaCodec 提供了两种数据流访问方式。你想想看,如果你的业务线程总在等 Codec 干活,是不是很浪费?

  • 同步模式:用 dequeueInputBuffer 和 dequeueOutputBuffer 在主循环里轮询。简单直接,但容易引入卡顿——如果一次循环里做太多事,就可能漏掉输出。
  • 异步模式:通过 setCallback 注册回调,Codec 在有空闲缓冲区或新输出时通知你。我个人习惯用这种,尤其是做实时拍摄预览的时候。

避坑指南:我曾经在异步回调里直接做了 GPU 纹理上传,结果 UI 线程死锁。切记——回调运行在 Codec 自己的线程里,不能阻塞,也不要做耗时操作。最佳做法是:把 Buffer 索引抛到主线程的消息队列里,再统一处理。

数据流模型流程图

下面这张图把整个数据流串起来。你从左边看,App 生产数据,经过 Codec 内部管道,从右边消费结果。

MediaCodec 缓冲区数据流模型 生产者(应用) 输入缓冲区 1 输入缓冲区 2 ... dequeue MediaCodec 编码 / 解码 内部调度 queue 消费者(应用) 输出缓冲区 1 输出缓冲区 2 ... dequeue + release 渲染 / 编码输出 关键:队列满 → 整条流水线阻塞

缓冲区生命周期

每个 Buffer 在生命周期里会经历四个状态。我把它整理成一个表格,你对照着看代码会更清晰。

状态说明典型 API
已分配 (Idle)Buffer 属于 Codec 的池子,未被应用使用dequeueInput/OutputBuffer 可能返回该索引
已出队 (Dequeued)应用持有该 Buffer,可以读写数据获得后填充/读取
已入队 (Queued)应用将 Buffer 还给 Codec 等待处理queueInputBuffer / releaseOutputBuffer
错误/失效Codec 重置或停止后,所有 Buffer 无效必须重新配置或重新创建 Codec

数据流节奏与卡顿关系

我曾遇到一个典型的卡顿场景:在同步模式下循环调用 dequeueOutputBuffer 时,如果输出还没准备好,我就 Thread.sleep(5)。结果低端机型上 sleep 导致输入队列逐渐填满,最终整个解码堵死。

解决办法:改用异步模式,或者用 while (true) 循环但把 sleep 换成 tryAcquire 精确等待。你想想看,5ms 的 sleep 在 30fps 的视频里就等于损失了 1/6 帧的时间窗口——积少成多就卡了。

数据流优化建议

  • 保持输入队列不空(提前准备好 2~3 帧的待压数据)。
  • 及时释放输出 Buffer(拿到后尽快 release,避免占用 Output 槽位)。
  • 使用 MediaCodec.BUFFER_FLAG_END_OF_STREAM 要谨慎——结束后队列立即清空,小心后续帧丢失。

总结

MediaCodec 的缓冲区管理本质上是一个有限的环形队列池。数据在生产者和消费者之间流转,任何一侧速度过慢都会导致另一侧阻塞。

我个人习惯在项目初期就画一张类似上面的流程图,标注清楚每一帧数据从编码到渲染经过的所有队列和状态。这样一旦出现卡顿,马上能锁定是输入慢、输出慢还是内部调度瓶颈——省掉大量瞎猜的时间。