MediaCodec 简介与 start 阶段概述

我最早接触 Android 多媒体开发的时候,最头疼的就是 MediaCodec 的 start 方法。明明代码逻辑都写对了,可每次调用 start(),总感觉卡了那么几百毫秒甚至一两秒。尤其在做视频录制或者实时直播的时候,这种延迟简直就是灾难。后来我花了大量时间把整个启动流程拆开来看,才搞清楚里面的门道。

MediaCodec 是 Android 提供的底层音视频编解码 API,说白了就是系统给应用层的一个“编解码开关”。你不需要直接操作硬件驱动,也不用纠结底层是 OMX 还是 Codec2 框架,只要调几个方法,就能让芯片去干编解码的脏活累活。但问题就在于 —— 这个“开关”从拧下去到真正通电干活,中间有一大堆步骤。

MediaCodec 到底是个啥?

可以先把它理解成一个“黑盒子”。你喂给它原始视频数据,它吐出来压缩后的码流;或者反过来,喂码流,吐原始画面。Android 里几乎所有音视频相关的 App(相机、播放器、直播推流)背后都在用这个 API。

它的生命周期很清晰:

  • createByCodecName / createDecoderByType —— 创建编解码器实例
  • configure —— 设置参数,比如分辨率、帧率、码率、色彩格式
  • start —— 启动编解码器,准备接收数据
  • dequeueInputBuffer / queueInputBuffer —— 往里送数据
  • dequeueOutputBuffer —— 取出结果
  • stop / release —— 停止并释放资源

其中 start 阶段 是整个生命周期里最不可控、最容易出耗时的环节。我自己的项目里,有一次在低端机上测到 start 耗时超过 800ms,直接导致首帧渲染延迟。嗯,后面我们详细拆。

start 阶段到底干了什么?

你以为 start() 只是一个状态切换?其实它背后是一整套“硬件就绪”流程。我按自己的理解把它分成几个核心步骤:

步骤描述典型耗时
1. 组件实例化在底层创建真正的编解码器组件,分配内部对象5-50ms
2. 底层驱动加载如果是硬件编解码,需要加载对应厂商的驱动模块(OMX 或 Codec2 插件)20-200ms
3. 缓冲区分配分配输入输出 Buffer,涉及到物理内存或 Ion 内存10-80ms
4. 参数内部协商编解码器根据你 configure 时传入的参数,做最终确认和修正2-20ms
5. 硬件初始化真正的硬件寄存器配置、编解码核心准备就绪50-300ms
6. 异步回调就绪等待底层通知“我已经准备好了”,这个回调会触发 onOutputFormatChanged0-50ms
总结一句话: start 阶段的耗时大头在“驱动加载”和“硬件初始化”上。这两个阶段受芯片型号、系统版本、当前负载影响很大。

你想想看,这些步骤很多都是在底层完成的,Android 应用层根本控制不了。所以为什么不同手机上 MediaCodec start 耗时差别巨大?就是因为硬件驱动实现差异。

我踩过的坑

我曾经在做一个直播推流 App 时,遇到一个奇怪现象:每过几分钟重新启动一次编码器,UI 就会卡一下。后来一测,发现是每次 start 都去重新加载 OMX 驱动模块,耗了 150ms 左右。我当时的解决方法是:保持一个 idle 状态的编码器实例,下次 start 时复用,而不是每次都 create + start。

还有一个问题——不要在主线程里调用 start()。我记得有个同事直接把 MediaCodec 的所有操作都写在 UI 线程里,结果 start 耗时太长导致 ANR。这种低级错误我后来每次培训都会提:所有编解码操作必须放到独立线程,最好用 HandlerThread 或者 ExecutorService。

重要提醒: start() 方法虽然是同步返回,但它内部可能发起异步的初始化请求。所以就算 start 返回了,也不代表编解码器能立刻干活。你需要等待 onOutputFormatChanged 回调才能真正开始送数据。

start 阶段的核心流程

为了更直观地看清这些步骤之间的关系,我画了一张流程图(下面这个 SVG 就是)。它把 start 内部的主要环节按“用户态”、“底层框架”、“硬件驱动”三个层面做了分组,箭头表示调用顺序。

MediaCodec start阶段核心流程与耗时来源 用户态 (App/Java层) createByCodecName configure start() 底层框架 (MediaCodec/OMX/Codec2) 组件实例化 驱动加载 (OMX/Codec2) 参数内部协商 硬件驱动 (HAL / 厂商模块) 缓冲区分配 (Ion/Gralloc) 硬件初始化 (寄存器等) 就绪回调 🕒 耗时最多的两个环节:驱动加载(红框)和硬件初始化(红框),其他步骤大多在 50ms 以内

从图上能看得很清楚:start() 调用后不是直接返回,而是先经过底层框架的组件实例化和驱动加载,然后下到硬件驱动层做缓冲区分配、硬件初始化,最后再通过回调告诉上层“我准备好了”。每个环节都可能成为瓶颈。

个人经验总结

我建议你在做性能优化的时候,先拿 Trace 或者 systrace 抓一下 start 阶段各个线程的耗时分布。用 adb 命令就行:

adb shell trace-start MediaCodecStart
# 在你代码里执行 create -> configure -> start
adb shell trace-stop

抓出来的 trace 里,重点关注 MediaCodec.start 以及下面的 OMXNodeInstance::allocateBuffer、Codec2Client::start 这些 tag。如果硬件初始化时间特别长,那基本就是芯片驱动的问题,只能通过提前预热或者复用实例来绕过去。

一个小技巧: 如果你要做连续多次编解码,尽量复用 MediaCodec 实例,而不是频繁 create + start + stop + release。我试过在同一个实例上反复调用 start/stop(不 release),第二次以后的 start 耗时能降低 60% 以上。

当然,start 阶段的耗时也不是一成不变的。不同 Android 版本、不同芯片、甚至同一台机器不同负载下,测量结果都可能相差很大。这也是我为什么一直强调:一定要在目标设备上实测,别光看文档。

嗯,这一章就先把整体流程和耗时来源理清楚。后面我们会深入每一个环节,看看代码层面到底发生了什么。