启动耗时来源总览:IO等待、帧内初始化、硬件资源分配、同步机制
每次调用 MediaCodec.configure() 到真正开始输出数据,中间那几百毫秒到底去哪了?
很多人第一反应是“解码器启动慢”,但你要是扒开源码看一遍,会发现这背后藏着四个大头:IO 操作、帧内初始化、硬件资源分配、以及各种同步锁。我把它们整理成了一张图,你先看一眼整体脉络。
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 堵塞。我发现很多团队只盯着“解码速度”,却忽视了启动路径上的同步瓶颈。
把这些串起来看
四个来源不是孤立的,它们是串行发生的(或者部分并行),所以总时间大约是它们累加的结果。我常用的一个分析手段是:
- 拿到 Systrace / Perfetto trace,看
MediaCodec.configure()到第一个dequeueOutputBuffer返回之间的跨度。 - 分层拆解:把 trace 里的 IO 操作(open/read/mmap)、Binder 调用、帧内配置、硬件初始化单独圈起来。
- 对比不同设备:同一段视频,在骁龙 8 Gen 2 上和骁龙 680 上,各环节的耗时差异能帮你定位瓶颈在哪。
我把常见情况整理成了一个小表格,方便你对照自查:
| 耗时来源 | 典型耗时范围 | 优化潜力 | 常见瓶颈 |
|---|---|---|---|
| IO 等待 | 30~100 ms | ★★★★☆ | 库加载、csd 数据读取 |
| 帧内初始化 | 80~250 ms | ★★★☆☆ | 格式协商、参数配置 |
| 硬件资源分配 | 50~200 ms | ★★★☆☆ | DMA 分配、驱动 open |
| 同步机制 | 20~80 ms | ★★★☆☆ | Binder + 锁竞争 |
知道了来源,其实就已经成功了一半。后面几个章节我会针对每个来源,讲具体的优化手段——比如用 CodecCache 绕开 IO、调整配置参数减少帧内协商、以及怎么跟硬件厂商要“快速启动模式”。但我们得先看清“慢在哪里”,再去动刀,对吧?