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 之后推荐用异步。
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 之后判一下状态。
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);
}
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 | 类型 | 说明 |
|---|---|---|
| MIME | String | 例如 "video/avc"、"audio/mp4a-latm" |
| width / height | int | 视频分辨率 |
| bitrate | int | 编码目标码率(bps) |
| frame-rate | int | 编码帧率 |
| color-format | int | 输出颜色格式(如 COLOR_FormatYUV420SemiPlanar) |
| i-frame-interval | int | 关键帧间隔(秒) |
| csd-0 / csd-1 | ByteBuffer | 解码需要的 codec-specific data(如 H.264 的 SPS/PPS) |
| max-width / max-height | int | 解码器能支持的最大分辨率(只读) |
我踩过的一个坑:color-format 不匹配。某些硬件解码器默认输出 COLOR_FormatSurface,但你要是直接取 ByteBuffer 做处理,就会拿到空数据或者灰度图。应该用 MediaCodec 的 createInputSurface 做合成,或者把 color-format 明确设成 COLOR_FormatYUV420Flexible。
1.6 MediaCodec 知识体系总览
下面这个图是我自己整理的 MediaCodec 核心结构。你看从状态机、缓冲区、异步回调到底层硬件驱动,每一层都可能引发卡顿。后面咱们会一层层拆开讲。
搞清楚了上面这些基础概念,才能去排查为什么一帧数据半天出不来、为什么 buffer 总是满的、为什么某台设备上一直崩溃。接下来我们会深入每个环节的细节。你准备好了吗?