硬件编解码器 vs 软件编解码器:实例化时的底层驱动加载与等待差异

为什么同一个 MediaCodec.createDecoderByType 调用,有的设备 5ms 返回,有的要卡 200ms?

差别就在底层走的路径完全不同。

我个人习惯把编解码器实例化拆成两个层面看:
用户态框架(MediaCodec + OMX/Codec2)和内核态驱动(硬件解码器固件 / VPP 状态机)。
硬件和软件在“驱动加载”这步,路子差得太远了。

一句话结论:
硬件编解码器实例化慢,是因为它需要“拉起硬件状态机+固件握手”;
软件编解码器实例化快,是因为它只需要“在用户态初始化内存池”。

硬件编解码器:驱动加载链路的“深水区”

记得有一年我在做某款手机的视频播放器优化,发现 Instgram 的 Camera 录制后回放总是卡顿。
我用 systrace 一抓,好家伙,MediaCodec 实例化居然占了 180ms。而对手设备只要 20ms。

再深入一看,硬件编解码器实例化时,底层干了这些事:

  1. HAL 层查询能力 – MediaCodec 框架向 ICodec 服务查询组件列表,这里会走 Binder 跨进程,一次 RTT 至少 1~5ms。
  2. 驱动加载固件 – 硬件编解码器驱动(比如 msm_vidc、hantro)要打开设备节点 /dev/videoX,触发内核加载固件。
    如果是首次使用,固件需要从文件系统读入、校验、并启动硬件状态机——这一步很容易吃掉 30~100ms。
  3. 硬件资源分配 – 分配内部 SRAM / VPU 核,初始化解码流水线。
    我在项目中遇到过,某些老款 SoC 上 VPU 的时钟门控会引入额外的延时,因为硬件要从省电模式“完全唤醒”,然后校准 PLL。
  4. OMX / Codec2 端口协商 – 驱动返回支持的色域、分辨率、帧率范围,框架据此设置 setInputSurface 等等。

这里有一个隐藏坑:首次实例化 vs 后续实例化。

  • 首次:走完整的固件加载 + 硬件唤醒,我测过最夸张的是一台展讯平台,花了 350ms。
  • 后续:如果同一个硬件实例没有被销毁,驱动层可能缓存了状态,第二次可能只要 5ms。
    但如果你 release() 后又重新创建,驱动又重新走一遍加载流程——这很常见。
我曾经踩过一个大坑:
在某个芯片上,每次实例化硬件编码器都会触发内核 dmesg 里打印“Loading firmware…  Done”,耗时稳定在 120ms。后来发现是固件文件在 ext4 分区上,而文件系统刚好处于 noatime + 碎片化状态。把固件预读到内存里,才降到 5ms。

软件编解码器:用户态的“轻骑兵”

说白了,软件编解码器不需要跟内核固件打交道。
它的实例化流程大概就这么几步:

  1. 查找组件 – 从 MediaCodecList 或 Codec2Config 里找到对应 name(比如 OMX.google.h264.decoder)。
  2. 加载动态库 – dlopen 软解库(如 libstagefright_soft_avcdec.so),如果库之前已经加载过,这步几乎免费。
  3. 创建解码器实例 – 调用库内 init(),分配用户态内存用于解码输出(YUV 缓冲区)。
    注意这里不涉及 DMA / iommu,仅仅是 malloc 或 android::MemoryHeapBase。
  4. 设置格式 – 框架调用 configure(),但此时并没有真正的数据流启动,只是一个参数校验。

整个流程通常 < 2ms。嗯,2ms 还是很稳的。

但软件编解码器也有它的“慢点”:

  • 第一次 dlopen 可能因为库体积大(比如 libavcodec 可能 10MB)而慢一点,但大部分 Android 设备上软解库都是系统预链接的,影响很小。
  • 设置复杂分辨率 时,某些软解内部会预分配超大内存(比如 4K 的 YUV 缓冲约 12MB),在低内存设备上可能触发 page fault 导致 10-20ms 波动。
    但这也远好于硬件驱动的百毫秒级阻塞。

对比速查表:实测数据(基于 Snapdragon 865)

项目 硬件编解码器 软件编解码器
首次实例化耗时 80~350 ms 0.5~3 ms
后续实例化(未 release) 5~20 ms < 1 ms
CPU 占用 低(≈5% 一个核) 高(20-100% 一个核)
功耗 低(专用硬件) 高(CPU 全速)
驱动加载依赖 固件、HAL、内核态 用户态动态库
稳定性 受驱动 bug 影响大 几乎无底层驱动风险
我的小窍门:
在冷启动场景(比如视频列表第一次滑动)使用软件解码器预热,等用户真正点击播放时再切换到硬件解码器。这样既避免了实例化长耗时的首帧卡顿,又能保证播放时的低功耗。

为什么软解实例化这么快?——逻辑分层图

下面这张图对比了实例化时两者走过的路径长度。你想想看,硬件那一路要穿透 4 层才能碰到硬件,而软解只要 2 层。

硬件与软件编解码器实例化路径对比 App:MediaCodec.createDecoderByType() MediaCodec / Codec2 框架 HAL / ICodec (Binder RTT) 内核驱动(固件加载 + 硬件唤醒) ⚠ 耗时峰值 80-350 ms App:MediaCodec.createDecoderByType() MediaCodec / Codec2 框架 用户态软解库 (dlopen + init) ✅ 无内核调用,耗时 < 3 ms

我总结的选型策略与避坑

知道了底层的差异,你就能做出更精准的判断:

  • 场景:列表快速滑动时预解码缩略图
    用软件解码器实例化。原因:不需要唤醒硬件,且短时间内实例化→解码→释放的循环非常频繁。如果用硬件,每次 release 后再创建又要重新加载固件,你会看到明显的卡顿。
  • 场景:直播首帧渲染
    用硬件解码器实例化,但需提前预热。
    我在项目中做过一个优化:在 App 启动后的后台线程里,偷偷创建一个硬件解码器实例(只 create/configure,不 start),然后马上 release。这个操作能提前把固件加载好、硬件状态机初始化好。等到真正需要播视频时再次创建,时间就缩短到 5ms 以内。
  • 场景:兼容性兜底
    如果硬件解码器 instance 创建失败(比如返回 ERROR_UNSUPPORTED),回退到软件解码器。实测这种场景下,回退路径的耗时主要花在“失败判断+重新创建”上,大约额外增加 10ms。完全能接受。
避坑提醒:
别在 UI 线程直接 createDecoderByType 创建硬件解码器!
我曾经在一个项目里看到开发者在 onSurfaceTextureAvailable 回调里直接创建解码器,结果因为硬件实例化耗时太长导致 ANR。正确的做法是在后台线程创建,或者用上述“预热”策略。

最后,说一个判断当前设备上某个编码类型是走硬件还是软件的快速办法:

adb shell dumpsys media.codec | grep -A 10 "OMX.xxx.decoder"

如果组件名包含 OMX.google 那就是软解,包含 OMX.qcom、OMX.MTK 等芯片厂商前缀的就是硬件。
嗯,这个区别在 Codec2 的组件名里也一样,比如 c2.android.avc.decoder 是软解,c2.qti.avc.decoder 是硬解。

一个小技巧:
在实例化前,先调用 MediaCodecList.findDecoderForFormat() 并检查组件名称,如果发现是硬件组件且你预估实例化会耗时,就立刻异步预热。这样用户几乎感知不到初始化延迟。