configure阶段耗时分析:MIME类型匹配、Codec实例创建与资源预留

好,我们接着往下聊。上一章我们讲了 MediaCodec 的生命周期,这一章我们聚焦在 configure 阶段——这个阶段我把它叫做「启动深水区」。为啥?因为很多优化空间都藏在这里。你看起来只是一个 API 调用,但里面做的事情可不少。

说实话,我曾经在某个项目里因为 configure 阶段耗时太长,导致视频录制启动慢了好几秒。用户按了录制按钮,那个转圈圈转得人心慌。后来我一条条去拆解,才发现里面这么多门道。

configure 阶段的耗时,主要有三大块:MIME 类型匹配、Codec 实例创建、资源预留。我习惯用一张图来理解这个过程,你先瞅瞅:

configure阶段耗时全貌与优化路径 配置参数传入 ① MIME类型匹配 查表 → 筛选 → 排序 → 返回候选列表 ② 创建Codec实例 Java对象 → native层 → 组件工厂 ③ 资源预留 CPU/GPU/硬件加速器 → 缓冲区池 耗时高节点 native_create 硬件协商 优化策略 • 异步配置(不要在主线程做) • 预创建实例(复用池) • 降低分辨率/规格拆分 • 配置缓存(MIME映射表) • 精简Format参数 常见问题 • MIME不精确导致检索缓慢 • omx组件创建超时 • 硬件资源锁定失败

1. MIME类型匹配:看似简单的水下冰山

当你调用 MediaCodec.createDecoderByType("video/avc") 或者直接传 MediaFormat 进去的时候,系统要做什么?说白了,它要去查表。

Android 维护着一张「MIME 类型 ↔ Codec 组件」的映射表。你给的 MIME 越精确,匹配越快。但很多时候我们传的是一个很宽泛的类型,比如 "video/*",系统就得遍历所有组件,一个个比对。

我在项目中遇到过,视频录制启动慢,打 trace 一看,MIME 匹配占了大头。怎么回事?因为我传的 MediaFormat 里只写了 MIMETYPE_VIDEO_AVC,没有指定 profile 和 level。系统得把所有 H.264 编码器都拉出来过滤一遍,再按分辨率筛选。你想想看,手机上可能装了十几个第三方 Codec,遍历一遍那能不慢吗?

优化小技巧:尽量精确指定 MIME 的子类型,比如视频编码时带上 KEY_PROFILE 和 KEY_LEVEL。这样系统可以快速命中,不用遍历。

另外一点,我记得有些厂商的 ROM 在 MIME 匹配时还做了额外的权限检查。比如某些专有 Codec 需要 license。这也会增加耗时。说白了,这个阶段最好提前做好缓存,如果你知道要用什么编码格式,直接复用上一次匹配结果。

2. Codec实例创建:从类名到native对象

匹配到具体组件之后,接下来就是创建实例。这个过程跨了 Java 和 native 两层。Java 层只是 new 一个 MediaCodec 对象,但底层要去加载 OMX 组件(或者 Codec 2.0 的组件)。

具体来说,MediaCodec.createByCodecName() 内部做了这些事:

  1. 解析组件名:比如 "OMX.google.h264.encoder",系统需要知道这个组件是谁提供的。
  2. 加载组件库:对于 OMX 组件,要去 dlopen 对应的 .so 文件。这个 I/O 操作本身就要几十毫秒。
  3. 初始化组件:调用 Init() 函数,分配内部数据结构。
  4. 回到 Java 层:封装成 MediaCodec 对象返回。

我曾经在 native 层加了 trace,发现有些第三方 Codec 库 strace 一下能看到 100ms+ 的 I/O 等待。为什么会这样?因为人家把解码器做成了插件,每次启动都要从磁盘读取配置文件。

避坑指南:我曾经踩过一个坑——在同一个进程里反复创建和销毁 MediaCodec,导致 native 层频繁 dlopen/dlclose。后来发现每次创建耗时从 50ms 涨到了 200ms。排查下来是 Codec 库没卸载干净,资源泄漏了。解决方法是用对象池,复用实例,别频繁创建。

嗯,这里要注意,Google 在 Android 10 之后引入了 Codec 2.0 架构,组件加载方式变了。但它底层还是逃不掉初始化动作。说白了,这个阶段的耗时很难完全消除,但我们可以想办法让它提前。

3. 资源预留:硬件加速器的战场

当 Codec 实例的 configure() 到 start() 之间,系统会为你分配实实在在的硬件资源。这个阶段也是耗时大头。

  • 视频编解码器:需要分配 DSP 或 GPU 上的硬件加速单元。如果资源被其他进程占着,就得等。
  • 内存缓冲区:分配 input/output buffer 的 native 内存。大分辨率下,这部分开销不小。
  • 匿名共享内存:有些 Android 版本用 ashmem 来传递数据。
  • Surface 绑定:如果要用 setInputSurface() 或者输出到 Surface,还会涉及图形缓冲区的协商。

我遇到过最典型的问题,是在低端机上做 4K 视频播放时,资源预留阶段直接崩了。为啥?因为硬件解码器只有一个,屏幕上也只支持一个 Surface 输出。多个实例抢资源,导致 configure 返回 ERROR。

还有一个容易被忽略的点——音频 Codec 的资源预留。它虽然轻,但如果你同时在编解码视频和音频,系统要为两个实例都预留资源。有些硬件平台混合编解码时资源是有上限的,超出就会退化为软件解码,那速度就慢了。

性能指标参考:我实测对比过,在骁龙 865 机型上,配置 1080p H.264 编码器 configure 阶段平均耗时如下:
阶段 耗时范围 占比
MIME类型匹配 5-15 ms 10%
Codec实例创建 20-80 ms 40%
资源预留 30-120 ms 50%

看到没?资源预留占了半壁江山。那怎么优化呢?

4. 优化实战:我在项目中的三板斧

这部分我直接给干货。我这些年积累下来的经验,总结成三个方向:

第一斧:异步配置,别堵主线程。
千万不要在主线程里做 configure。Android 的 ANR 阈值只有 5 秒,configure 如果资源紧张可能卡住 1-2 秒,就会触发 ANR。正确做法是封装到后台线程,或者用 MediaCodec.Callback 方式异步操作。

第二斧:预创建 + 复用池。
如果你知道应用启动后一定会播放视频,可以在 Application.onCreate() 时提前创建一个 MediaCodec 实例,初始化好就缓存起来。等真的要用时,刚好赶上。我做过一个项目,启动时预创建 10 个 Codec 实例放在对象池里,启动耗时从 800ms 降到了 150ms。

注意:预创建要小心,不要创建太多实例,否则会占用系统资源。一般建议池子大小不超过 3 个,而且用完要重置状态。

第三斧:精简 Format,减少协商开销。
传给 configure() 的 MediaFormat 里,只用必要的 key。我曾经看到有人把所有配置项都塞进去,包括什么 CSD-0、CSD-1 之类的。你想想看,这些额外参数会让资源预留阶段多做很多解析和校验。

举个例子,视频编码只需要几个 key:

// 精简配置:只保留必要参数
MediaFormat format = MediaFormat.createVideoFormat(
    MimeType.VIDEO_AVC, width, height);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_BIT_RATE, 2_000_000);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
// 不要添加不相关参数
// format.setByteBuffer("csd-0", ...); // 除非明确需要

5. 总结与避坑清单

configure 阶段的耗时,说白了就是「查表 + 建对象 + 抢资源」这三板斧。我建议你养成一个习惯:启动后第一时间打一次 trace,看看 configure 到底花了多久,然后对症下药。

最后列几个我踩过的坑,你可以直接拿去用:

勘误清单:
  • 不要在 configure() 之后换参数——重新配置一定会更慢。
  • 不要用 setVideoScalingMode() 缩小分辨率,那只是展示时缩放,底层还是按原分辨率预留资源。
  • 不要自己维护输入输出 Buffer,使用 getInput/OutputBuffer() 分配方式由系统管理最省事。
  • 如果碰到 configure() 返回 ERROR,先检查硬件资源占用情况,可以用 MediaCodecList 的 getCapabilities() 查看。