3. start 调用入口:时序与线程分析

很多朋友刚开始调 MediaCodec 时,总觉得 start() 就是个简单的“启动”动作。结果一旦卡住,就开始怀疑是 codec 没配好、还是底层驱动有坑。我来拆解一下 start() 到底干了些什么。

说白了,start() 是 MediaCodec 从“配好了”到“真正跑起来”的关键转折点。它背后涉及状态迁移、线程创建、Binder 通信、底层硬件初始化——任何一个环节出问题,都能让你多等几百毫秒甚至几秒。

3.1 start() 调用时序全景

我习惯把 start() 的整个执行路径分成三段:

  1. 用户态入口 – 应用调 start() 那一刻,MediaCodec.java 开始干活。
  2. 框架层同步 – 状态锁、状态机检查、消息投递到 Codec 线程。
  3. 底层真正启动 – CodecThread 执行 start 逻辑,最终通过 OMX/Codec2 驱动让硬件动起来。

你看下面这张图,整个流程一目了然。

MediaCodec start() 调用时序与线程关系 应用层 框架层 底层 应用调用 start() 状态检查
CONFIGURED→STARTING 投递 START 消息
唤醒CodecThread
线程创建 & 绑定Looper OMX / Codec2
allocate + configure
设置状态 STARTED
等待 inputBuffer
线程模型示意 · 调用线程(主线程/任意) · CodecThread(工作线程) · CallbackThread(回调) · Binder通信线程 · 底层驱动线程 常见耗时点 ① 状态锁竞争 ② 线程创建/等待 ③ Binder 跨进程 ④ 硬件初始化

图中右侧的“线程模型示意”框里,我标出了参与 start() 的各个线程。注意:CodecThread 并不是在 start() 时才创建——它早在 configure() 时就已经准备好了。start() 只是向它发了个消息,然后等它把底层初始化完。

3.2 关键线程:谁在做什么?

我个人习惯把 MediaCodec 的线程拓扑记清楚,这样出问题时不至于抓瞎。

线程名称 创建时机 start() 中的职责 可能的耗时
调用线程 应用创建 执行 start(),获取锁,投递消息 如果锁被占,会 blocking
CodecThread configure() 中 收到 START 消息,调用底层 start 等待消息、底层 I/O 阻塞
Binder 线程 进程初始化 将 start() 调用传送到 mediaserver 需要等待 Binder 响应
硬件驱动线程 驱动加载时 执行编解码器硬件初始化 硬件唤醒、时钟同步

💡 经验之谈

我曾经在分析一个直播推流延迟问题时,发现 start() 耗时超过 500ms。后来用 systrace 一抓,原来是 CodecThread 在等待一个长时间运行的 Binder 调用——底层驱动在初始化时要做一次固件下载。解决方案是提前在后台线程中预热 codec 实例,把硬件准备提到上一个生命周期完成。

3.3 时序中的“隐形”阻塞点

很多人以为 start() 是非阻塞的,看源码确实有 callback 模式。嗯,你要仔细看一下“非阻塞”到底是指什么。

实际上 MediaCodec.start() 是同步的(阻塞调用线程),直到底层返回成功或失败。而所谓的“非阻塞”是指 start() 之后你不需要自己等 buffer——系统会自动回调。但 start() 本身仍然会卡住调用线程几百毫秒。

看看以下代码片段,你就明白了:

// 典型调用时序
MediaCodec codec = MediaCodec.createByCodecName("OMX.google.h264.encoder");
codec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
// ↑ configure 返回时,CodecThread 已创建,但处于 idle 状态
long t0 = System.nanoTime();
codec.start();
// ↑ 这一行可能阻塞 50~500 ms
long t1 = System.nanoTime();
Log.d(TAG, "start() took " + (t1 - t0) / 1e6 + " ms");

为什么会阻塞那么久?因为 start() 内部需要完成以下同步操作:

  1. 获取状态锁,检查当前状态是否为 CONFIGURED。
  2. 设置新状态为 STARTING(防止其他线程干扰)。
  3. 向 CodecThread 的 Handler 发送 START 消息。
  4. 使用 CountDownLatch 或 ConditionVariable 等待消息处理结束。
  5. CodecThread 在收到消息后,会调用底层接口(如 OMX::SendCommand(OMX_CommandStart)),然后等待硬件返回 OMX_EventCmdComplete。
  6. 底层返回后,CodecThread 修改内部状态为 STARTED,释放 latch。
  7. 调用线程从 start() 返回。

⚠️ 常见坑

如果你在 start() 之前做了大量的 IO 操作,比如读取文件、加载模型,那么调用线程可能被其他任务拖累。start() 本身需要快速返回,但如果主线程繁忙,消息投递的延迟也会累加进去。

我曾经在一个项目中遇到:start() 总是在某些设备上耗时 2~3 秒,后来发现是系统服务 mediaserver 被其他应用占满 CPU,导致 Binder 调用迟迟无法被响应。那一次让我学会了“不要把 start() 放在高优先级的渲染循环里”。

3.4 如何定位 start() 的耗时来源

给你一个我自己用得很顺的定位流程:

  1. 粗粒度测量 – 在 start() 前后打点,记录总耗时。
  2. 抓取 systrace – 重点关注 MediaCodec.start、CodecThread、OMX 相关的 trace 段。
  3. 查看 Binder 耗时 – 用 adb shell dumpsys media.codec 看最后一次操作的耗时统计。
  4. 区分硬件还是软件 – 如果底层是软件解码(如 OMX.google.*),start() 一般很快(<10ms);如果是硬件编解码,可能会因为固件加载、VPU 上电而变慢。

这里有一个我总结的经验值表,来自对不同芯片平台的测量:

场景 典型 start() 耗时 主要瓶颈
软件编码 (x264/x265) 1~5 ms 状态切换、线程唤醒
硬件编码 (Qualcomm) 30~150 ms Binder + 固件加载
硬件编码 (MTK) 50~300 ms VPU 上电、内存分配
硬件解码 (H264/H265) 20~80 ms Decode pipe 初始化

关键结论

start() 的耗时大头几乎不在 Android 框架层,而在底层 Binder 通信和硬件驱动初始化上。如果你发现 start() 超过 200ms,先查底层驱动的 dmesg 日志,看看是否有供电、时钟或内存分配方面的延迟。