MediaCodec卡顿根因与性能调优案例 - 第1节

1.1 MediaCodec 到底是什么?

咱们搞 Android 多媒体开发的,天天跟音视频打交道。MediaCodec 这玩意儿,说白了就是 Android 提供的一个底层编解码接口。你可以把它理解成一个黑盒子——你把原始数据塞进去,它给你吐出解码后的 PCM 或 YUV,或者反过来把裸流压缩成 H.264、AAC。

我刚开始接触它的时候,觉得这 API 挺简洁的,createByCodecName 一调,start 一跑,数据不就是 queueInputBuffer / dequeueOutputBuffer 来回倒腾吗?后来线上出了几次卡顿和 ANR,才意识到里面的门道有多深。嗯,今天咱们先把基础打牢。

1.1.1 核心定位

  • 位置:位于 Android 多媒体框架的中间层,上层是 MediaPlayer / MediaExtractor,下层是 OMX 或 C2 插件。
  • 能力:支持音视频的编解码,也可以做纯视频处理(例如色调映射、缩放)。
  • 模式:同步模式(buffer 方式)和异步模式(Callback 方式),Android 5.0 之后推荐用异步。
📌 小经验:我个人习惯在调测新设备时先跑一次 MediaCodecList 打印所有支持的 MIME 类型,看看硬件编解码器到底有没有暴露出来。曾经遇到过某款低端平板,硬件解码 H.265 的 codec 名字叫 "omx.google.h265.decoder"——其实是软解,性能惨不忍睹。

1.2 基本状态机 —— 你必须掌握的生命周期

MediaCodec 内部是一个有限状态机。不理解这个,你写出来的代码很容易在异常流程里崩溃。想想看,你在 Stopped 状态调用 dequeueOutputBuffer,不抛 IllegalStateException 才怪。

状态 说明 可执行操作
Stopped 初始状态,或者 release 后的终结状态 configure, start
Configured configure() 成功后进入 start
Executing 正在编解码,可以收发 Buffer queueInputBuffer, dequeueOutputBuffer, flush, stop
Flushed flush() 之后的临时状态,所有 Buffer 被清理 继续 queueInputBuffer
End-of-Stream 标记 input buffer 的 BUFFER_FLAG_END_OF_STREAM 后达到 等待 output 全部出来,然后 stop
Error 内部错误(比如硬件掉线、不支持的参数) 只能 reset 或 release

我在项目中遇到过最典型的错误:没有检查 Error 状态。有一次在某个定制 ROM 上,摄像头采集帧率突变,MediaCodec 内部 buffer 分配失败直接进了 Error,但我还在循环 dequeueOutputBuffer,结果死循环卡死 UI。所以我个人习惯在每次 dequeue 之后判一下状态。

⚠️ 避坑指南:我曾经在异步模式下忘记处理 onError 回调,导致故障现场只有 "MediaCodec 死了",log 里啥也没留下。记住:任何时候都要监听 onError 并主动停止 codec,否则 buffer 队列会一直挂起。

1.3 两个核心缓冲区 —— 输入与输出

MediaCodec 的工作方式就是围绕一组 Buffer 转圈。

  • 输入缓冲区(InputBuffer):你把压缩数据(比如一个 H.264 NALU)写进去。
  • 输出缓冲区(OutputBuffer):解码后的裸数据(比如一帧 YUV)从里面读出来。

要注意的是:这些 Buffer 是 MediaCodec 内部管理的,不是你自己 new 出来的。你用 getInputBuffers() 拿到的是引用,但实际内存由底层分配。你想想看,如果你在 app 里搞了个大数组来回拷贝,那性能直接腰斩。

// 常规的同步用法示意(仅用于理解概念)
ByteBuffer[] inputBuffers = codec.getInputBuffers();
int inIndex = codec.dequeueInputBuffer(TIMEOUT_US);
if (inIndex >= 0) {
    ByteBuffer buf = inputBuffers[inIndex];
    buf.clear();
    buf.put(data);        // 把你的压缩数据塞进去
    codec.queueInputBuffer(inIndex, 0, size, pts, flags);
}

ByteBuffer[] outputBuffers = codec.getOutputBuffers();
int outIndex = codec.dequeueOutputBuffer(info, TIMEOUT_US);
if (outIndex >= 0) {
    ByteBuffer outBuf = outputBuffers[outIndex];
    // 处理 outBuf 里的解码数据
    codec.releaseOutputBuffer(outIndex, false);
}
🔥 性能点:对于 API 21+,我推荐用 getInputBuffer(int index) 和 getOutputBuffer(int index) 替代 getInputBuffers() 数组。前者每次拿的都是最新的 Buffer 对象,避免底层数据被缓存导致的内存泄漏风险。

1.4 同步 vs 异步 —— 选哪个?

你问我选哪个?异步,除非你有特殊理由用同步。

  • 同步模式:你在一个线程里轮询 dequeueInputBuffer / dequeueOutputBuffer,需要自己控制超时。代码好写,但容易 block。
  • 异步模式:注册 MediaCodec.Callback,框架会在合适的时候回调用 onInputBufferAvailable / onOutputBufferAvailable。省了轮询的 CPU,线程模型更清晰。

我曾经在一个低端机上用同步模式做实时视频直播,发现每次 dequeueOutputBuffer 耗时波动很大(偶尔 30ms 一次),因为底层硬件 buffer 调度受内核影响。换成异步后,回调驱动,CPU 占用从 25% 降到了 8%。

建议:如果你的场景需要低延迟(比如 <50ms),务必用异步模式。如果只是做离线转码,同步模式也无所谓——但记得把超时设成 0,不要傻等。

1.5 关键参数配置 —— Format 里藏玄机

MediaFormat 是编解码参数的核心。最常用的字段:

Key 类型 说明
MIMEString例如 "video/avc"、"audio/mp4a-latm"
width / heightint视频分辨率
bitrateint编码目标码率(bps)
frame-rateint编码帧率
color-formatint输出颜色格式(如 COLOR_FormatYUV420SemiPlanar)
i-frame-intervalint关键帧间隔(秒)
csd-0 / csd-1ByteBuffer解码需要的 codec-specific data(如 H.264 的 SPS/PPS)
max-width / max-heightint解码器能支持的最大分辨率(只读)

我踩过的一个坑:color-format 不匹配。某些硬件解码器默认输出 COLOR_FormatSurface,但你要是直接取 ByteBuffer 做处理,就会拿到空数据或者灰度图。应该用 MediaCodec 的 createInputSurface 做合成,或者把 color-format 明确设成 COLOR_FormatYUV420Flexible。

1.6 MediaCodec 知识体系总览

下面这个图是我自己整理的 MediaCodec 核心结构。你看从状态机、缓冲区、异步回调到底层硬件驱动,每一层都可能引发卡顿。后面咱们会一层层拆开讲。

MediaCodec 知识体系 状态机 Stopped / Configured Executing / Flushed 缓冲区 InputBuffer OutputBuffer 操作模式 同步(轮询) 异步(回调) MediaFormat MIME / 分辨率 / 码率 color-format / csd 底层引擎 OMX 插件 / Codec2 框架 / 硬件驱动 ⚠️ 关键洞察 卡顿的根本原因往往出现在四层之间的衔接处: Buffer 饥饿 / 状态转换死锁 / 色彩格式不匹配 / 硬件能力不足 调优方向一 Buffer 管理与复用 调优方向二 异步回调与线程模型 调优方向三 硬件适配与 Format 优化

搞清楚了上面这些基础概念,才能去排查为什么一帧数据半天出不来、为什么 buffer 总是满的、为什么某台设备上一直崩溃。接下来我们会深入每个环节的细节。你准备好了吗?