4. 内部状态机转换:uninitialized → executing

做多媒体开发的人都知道,MediaCodec 的 start() 调用不是瞬间完事的。它背后是一套严谨的状态机跳转,从 Uninitialized 一路走到 Executing。我把这条路径上的每一步都掰开揉碎,看看时间到底花在哪。

4.1 状态机跳转全景

先搞清楚整个生命周期:

  • Uninitialized — 刚 new 出来,啥都没干
  • Initialized — 内部分配了基础对象,但还没配
  • Configured — 配置完 format,等待启动
  • Executing — 跑起来了,能处理数据

从 Uninitialized → Executing,中间会经历两次关键的系统调用:configure() 和 start()。别小看这两个调用,里面藏着的耗时操作,才是性能瓶颈的真身。

核心结论:80% 的 start 耗时发生在 Configured → Executing 这个阶段,尤其是底层组件初始化。

4.2 每一步的耗时拆解

我把每一步都用手上的 trace 数据整理了一下,你可以对照自己项目里的 log 看看。

状态转换触发方法典型耗时 (ms)主要耗时点
Uninitialized → Initializednew MediaCodec()0.5~2对象构造、注册进程端
Initialized → Configuredconfigure()3~15MediaFormat 校验、Codec 选择、资源预申请
Configured → Executingstart()10~80组件启动、buffer 池分配、硬件/软件引擎创建

有意思的是,第二行和第三行加起来几乎占了整个启动链路的 95% 以上。尤其是第三段,我测过一些低端手机,能跑到 80ms,用户直接感觉卡了一下。

4.3 深入 Configured → Executing 的“暗箱”

为什么这段最慢?我开了 trace 才看清内部干了啥:

  1. 底层组件创建 — 如果是硬件解码器,要去跟驱动握手;软编解码则要加载动态库,做指令集检测。
  2. Buffer 池初始化 — 申请输入/输出 buffer,有些设备还会提前映射 GPU 内存。
  3. 线程启动 — Codec 内部会创建几个工作线程,等待 buffer 流转。
  4. 参数协商 — 把你在 configure() 里传的 format 往下传,驱动或库要确认是否支持。

这里我贴一段从系统 log 里抓到的关键时序(简化版):

// 伪代码示意 start() 内部流程
status_t MediaCodec::start() {
    // [1] 状态检查 + 锁
    sp<AMessage> msg = new AMessage(kWhatStart, mCodecLooper);
    // [2] 发送到 looper,等待执行
    status_t err = postAndWait(msg);
    // [3] 内部实际处理
    //     - 设置状态为 kStateExecuting
    //     - 调用 mCodec->start()
    //     - 启动 input/output 轮询线程
    return err;
}

你可能会觉得奇怪:不就发个消息等结果吗?实际上 postAndWait 里面的同步开销不大,真正吃时间的是 mCodec->start() 那个方法。在底层,那是另一个世界。

4.4 我踩过的坑

我曾经在项目中遇到过 某款芯片解码 H.265 时 start 耗时高达 120ms。查了三天,发现是 configure() 里传的 KEY_FRAME_RATE 和实际流不匹配,驱动反复做分辨率重协商。去掉那个参数后降到 25ms。

所以你的配置参数尽量精准,别让底层去做 guess work。

避坑指南:别在 configure() 里传过多冗余参数,尤其是帧率、比特率这类辅助提示。某些底层驱动会据此做大量判断,反而拖慢 start。

4.5 状态机转换流程图

下面这张图把整个跳转路径和各自的主要耗时因素画清楚了,建议你保存下来做复盘对比。

MediaCodec 状态机转换:Uninitialized → Executing Uninitialized Initialized Configured Executing new MediaCodec() 耗时:0.5~2 ms 对象构造 / 注册 configure() 耗时:3~15 ms 参数校验 / Codec选择 start() 耗时:10~80 ms 组件启动 / buffer分配 配置 → 启动 内部子步骤 线程创建 & 启动 Buffer池分配 硬件驱动协商 参数下传 & 确认 虚线:内部子步骤(Configured → Executing 阶段)

4.6 实战建议

  • 预热: 如果你的 App 要频繁开关 Codec(比如视频通话),可以在后台提前 configure() 一次,但不要 start(),保留 Configured 状态,需要时立刻 start()。这样跳过前两步的耗时。
  • 对症下药: 如果 start 时间集中在 10ms 以下,可以不必费神;如果超过 30ms,先去查硬件初始化过程,看驱动是否在协商环节有问题。
  • 离线预分配: 我尝试过在 Application 初始化时主动调用 MediaCodecList 获取支持的信息,让系统把一些缓存资源提前 warm up,不过效果因品牌而异。

技巧: 用 adb shell dumpsys media.codec 可以 dump 出当前 Codec 实例的状态和耗时统计,比直接看 log 更直观。配合 systrace 定位具体哪个 binder 调用拖了后腿。

说到这你应该明白了:状态机不是白纸黑字的抽象概念,而是实实在在的时间开销分布。下回别人问起 start 为什么慢,你就把这张图扔给他——问题的根,往往在 Configured → Executing 那一步的四个子步骤里。