3. start 调用入口:时序与线程分析
很多朋友刚开始调 MediaCodec 时,总觉得 start() 就是个简单的“启动”动作。结果一旦卡住,就开始怀疑是 codec 没配好、还是底层驱动有坑。我来拆解一下 start() 到底干了些什么。
说白了,start() 是 MediaCodec 从“配好了”到“真正跑起来”的关键转折点。它背后涉及状态迁移、线程创建、Binder 通信、底层硬件初始化——任何一个环节出问题,都能让你多等几百毫秒甚至几秒。
3.1 start() 调用时序全景
我习惯把 start() 的整个执行路径分成三段:
- 用户态入口 – 应用调 start() 那一刻,MediaCodec.java 开始干活。
- 框架层同步 – 状态锁、状态机检查、消息投递到 Codec 线程。
- 底层真正启动 – CodecThread 执行 start 逻辑,最终通过 OMX/Codec2 驱动让硬件动起来。
你看下面这张图,整个流程一目了然。
图中右侧的“线程模型示意”框里,我标出了参与 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() 内部需要完成以下同步操作:
- 获取状态锁,检查当前状态是否为 CONFIGURED。
- 设置新状态为 STARTING(防止其他线程干扰)。
- 向 CodecThread 的 Handler 发送 START 消息。
- 使用 CountDownLatch 或 ConditionVariable 等待消息处理结束。
- CodecThread 在收到消息后,会调用底层接口(如 OMX::SendCommand(OMX_CommandStart)),然后等待硬件返回 OMX_EventCmdComplete。
- 底层返回后,CodecThread 修改内部状态为 STARTED,释放 latch。
- 调用线程从 start() 返回。
⚠️ 常见坑
如果你在 start() 之前做了大量的 IO 操作,比如读取文件、加载模型,那么调用线程可能被其他任务拖累。start() 本身需要快速返回,但如果主线程繁忙,消息投递的延迟也会累加进去。
我曾经在一个项目中遇到:start() 总是在某些设备上耗时 2~3 秒,后来发现是系统服务 mediaserver 被其他应用占满 CPU,导致 Binder 调用迟迟无法被响应。那一次让我学会了“不要把 start() 放在高优先级的渲染循环里”。
3.4 如何定位 start() 的耗时来源
给你一个我自己用得很顺的定位流程:
- 粗粒度测量 – 在 start() 前后打点,记录总耗时。
- 抓取 systrace – 重点关注
MediaCodec.start、CodecThread、OMX相关的 trace 段。 - 查看 Binder 耗时 – 用
adb shell dumpsys media.codec看最后一次操作的耗时统计。 - 区分硬件还是软件 – 如果底层是软件解码(如 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 日志,看看是否有供电、时钟或内存分配方面的延迟。