启动耗时来源总览:IO等待、帧内初始化、硬件资源分配、同步机制

每次调用 MediaCodec.configure() 到真正开始输出数据,中间那几百毫秒到底去哪了?

很多人第一反应是“解码器启动慢”,但你要是扒开源码看一遍,会发现这背后藏着四个大头:IO 操作、帧内初始化、硬件资源分配、以及各种同步锁。我把它们整理成了一张图,你先看一眼整体脉络。

MediaCodec 启动耗时 · 四大来源 IO 等待 帧内初始化 硬件资源分配 同步机制 · 初始码流加载 · 编解码库 .so 加载 (耗时占比: ~15-25%) · 像素格式协商 · 寄存器参数配置 (耗时占比: ~30-45%) · 初始化驱动句柄 · 分配硬件 DMA 内存 (耗时占比: ~25-35%) · Binder 跨进程调用 · 互斥锁等待 (耗时占比: ~10-15%) 总耗时通常: 200ms ~ 800ms 💡 不同芯片平台(高通 / MTK / Exynos)各环节占比差异明显 优化不能只盯一个点,得看你家设备哪块最慢 注意:以上数据基于 Android 12+ 主流设备实测范围,具体数值因分辨率、编码格式、SoC 而异

IO 等待 —— 藏得最深的第一道坎

先说说 IO。很多人以为 MediaCodec 启动就是“创建一个对象”,实际上它背后要干不少文件操作。

  • 初始码流加载:configure() 时你得传一个 MediaFormat,里面带着关键帧的 csd-0 / csd-1 数据。这些数据如果是从磁盘文件读出来的,那 read() 系统调用本身就有延迟。我记得有个项目把 H264 的 SPS/PPS 写死在代码里,启动速度直接快了 80ms。
  • 编解码库加载:说白了就是系统要去 /vendor/lib64/ 或者 /apex/ 下面 dlopen 对应的 .so 文件。这个动作的耗时取决于文件大小、文件系统碎片程度,甚至这个 so 有没有被 page cache 命中。我第一次碰到这个问题是在某个低端机上,每次冷启动 MediaCodec 都要多等 300ms,后来发现是 SD 卡上的库文件被频繁换出。
  • 配置文件的解析:Android 的 MediaCodec 层会去读 /vendor/etc/media_codecs.xml,里边描述了你手机支持哪些编码器、性能参数是什么。XML 解析虽然不算重,但也是 IO 的一部分,尤其是文件很大的时候。
⚠️ 我曾经踩过的一个坑:在一台定制的 android 设备上,MediaCodec 冷启动居然要 1.2 秒。查了半天,发现系统把解码器的 .so 库放在了一个 FUSE 挂载的文件系统上,每次加载都要走网络回环。后来把库搬到了 ext4 分区,启动时间直接降了 70%。

帧内初始化 —— 最容易被忽略的“隐形劳动”

你发出 start() 之后,解码器内部可不是直接就去解码帧的。它要先做一堆“房间打扫”的工作。

  • 像素格式协商:你告诉它“我要解码 H264,输出 NV12”。但它内部要确认硬件底层支不支持这个格式,要不要做格式转换。这个协商过程涉及大量的枚举和状态机跳转。
  • 寄存器参数配置:对于硬件编解码器来说,这一步它要往硬件寄存器里写一堆参数——分辨率、帧率、码率、会不会有 B 帧等等。配置不对就是硬解码失败或者花屏。
  • 内部 buffer 池初始化:编解码器需要预先分配输入缓冲区和输出缓冲区。虽然我们应用侧只看到了几个 Surface 或者 ByteBuffer,但在驱动层面,它是一个复杂的 buffer 管理池。池的大小、对齐方式、内存类型(物理连续还是虚拟连续)都要在这一步敲定。

你想想看,这一步的耗时占比通常能到 30~45%,是启动阶段最大的拖累。不过好消息是,大部分初始化结果可以缓存。

硬件资源分配 —— 跟底层驱动的“博弈”

这是真正跟硬件打交道的一步。MediaCodec 最终要通过 HAL(硬件抽象层)跟 SoC 里的硬件编解码单元(如高通的 Venus、MTK 的 VPU)通信。

  • 初始化驱动句柄:打开 /dev/video-dec 或者类似的设备节点。这一步要经过内核驱动的 probe 和 open 函数,里面涉及 ioremap、中断申请等等。我见过有些内核驱动写得不讲究,open 的时候去做了大量自检,一下拖了几十毫秒。
  • 分配硬件 DMA 内存:普通的 malloc 是不行的,硬件需要的是物理地址连续的 DMA buffer。系统得从 CMA(Contiguous Memory Allocator)里切一块出来。如果当前 CMA 区域被碎片化严重,分配时间会显著增加。
  • 硬件视频模式切换:有些 SoC 的解码单元和显示单元是共享的,启动解码器可能需要先把显示控制器切换到特定模式,这个切换也是耗时的。

说白了,硬件资源分配的耗时很大程度上取决于 SoC 方案和驱动质量。高通平台通常比较稳定,MTK 的低端芯片在内存分配这一步容易翻车。

同步机制 —— 多线程下的“隐形成本”

MediaCodec 层本身运行在应用进程里,但它要跟 mediaserver(或者现在的 media.codec 服务)通信。这一层是跨进程的。

  • Binder 跨进程调用:每次 configure()、start()、dequeueOutputBuffer() 看起来是同步调用,其实底层都经过 Binder 驱动。Binder 调用本身有线程池调度、序列化反序列化的开销。
  • 互斥锁等待:MediaCodec 内部(尤其是 OMX 组件时代)有大量的 mutex 和 condition variable。不同线程在 onInputBufferAvailable 和 onOutputBufferAvailable 回调中频繁争锁。如果你在低端机上遇到启动慢或者偶尔超时,很可能就是锁竞争导致的。
  • 状态机迁移:MediaCodec 有 Uninitialized -> Configured -> Executing 等状态,每个状态迁移都要保证原子性和一致性,这又伴随着一些同步操作。
💡 我个人的建议:如果你看到 MediaCodec 启动时间不稳定,尤其是波动很大的情况,优先怀疑同步问题。可以用 systrace 抓一下,看看有没有长等待的锁或者 Binder 堵塞。我发现很多团队只盯着“解码速度”,却忽视了启动路径上的同步瓶颈。

把这些串起来看

四个来源不是孤立的,它们是串行发生的(或者部分并行),所以总时间大约是它们累加的结果。我常用的一个分析手段是:

  1. 拿到 Systrace / Perfetto trace,看 MediaCodec.configure() 到第一个 dequeueOutputBuffer 返回之间的跨度。
  2. 分层拆解:把 trace 里的 IO 操作(open/read/mmap)、Binder 调用、帧内配置、硬件初始化单独圈起来。
  3. 对比不同设备:同一段视频,在骁龙 8 Gen 2 上和骁龙 680 上,各环节的耗时差异能帮你定位瓶颈在哪。

我把常见情况整理成了一个小表格,方便你对照自查:

耗时来源 典型耗时范围 优化潜力 常见瓶颈
IO 等待 30~100 ms ★★★★☆ 库加载、csd 数据读取
帧内初始化 80~250 ms ★★★☆☆ 格式协商、参数配置
硬件资源分配 50~200 ms ★★★☆☆ DMA 分配、驱动 open
同步机制 20~80 ms ★★★☆☆ Binder + 锁竞争

知道了来源,其实就已经成功了一半。后面几个章节我会针对每个来源,讲具体的优化手段——比如用 CodecCache 绕开 IO、调整配置参数减少帧内协商、以及怎么跟硬件厂商要“快速启动模式”。但我们得先看清“慢在哪里”,再去动刀,对吧?