configure阶段耗时分析:MIME类型匹配、Codec实例创建与资源预留
好,我们接着往下聊。上一章我们讲了 MediaCodec 的生命周期,这一章我们聚焦在 configure 阶段——这个阶段我把它叫做「启动深水区」。为啥?因为很多优化空间都藏在这里。你看起来只是一个 API 调用,但里面做的事情可不少。
说实话,我曾经在某个项目里因为 configure 阶段耗时太长,导致视频录制启动慢了好几秒。用户按了录制按钮,那个转圈圈转得人心慌。后来我一条条去拆解,才发现里面这么多门道。
configure 阶段的耗时,主要有三大块:MIME 类型匹配、Codec 实例创建、资源预留。我习惯用一张图来理解这个过程,你先瞅瞅:
1. MIME类型匹配:看似简单的水下冰山
当你调用 MediaCodec.createDecoderByType("video/avc") 或者直接传 MediaFormat 进去的时候,系统要做什么?说白了,它要去查表。
Android 维护着一张「MIME 类型 ↔ Codec 组件」的映射表。你给的 MIME 越精确,匹配越快。但很多时候我们传的是一个很宽泛的类型,比如 "video/*",系统就得遍历所有组件,一个个比对。
我在项目中遇到过,视频录制启动慢,打 trace 一看,MIME 匹配占了大头。怎么回事?因为我传的 MediaFormat 里只写了 MIMETYPE_VIDEO_AVC,没有指定 profile 和 level。系统得把所有 H.264 编码器都拉出来过滤一遍,再按分辨率筛选。你想想看,手机上可能装了十几个第三方 Codec,遍历一遍那能不慢吗?
KEY_PROFILE 和 KEY_LEVEL。这样系统可以快速命中,不用遍历。另外一点,我记得有些厂商的 ROM 在 MIME 匹配时还做了额外的权限检查。比如某些专有 Codec 需要 license。这也会增加耗时。说白了,这个阶段最好提前做好缓存,如果你知道要用什么编码格式,直接复用上一次匹配结果。
2. Codec实例创建:从类名到native对象
匹配到具体组件之后,接下来就是创建实例。这个过程跨了 Java 和 native 两层。Java 层只是 new 一个 MediaCodec 对象,但底层要去加载 OMX 组件(或者 Codec 2.0 的组件)。
具体来说,MediaCodec.createByCodecName() 内部做了这些事:
- 解析组件名:比如 "OMX.google.h264.encoder",系统需要知道这个组件是谁提供的。
- 加载组件库:对于 OMX 组件,要去 dlopen 对应的 .so 文件。这个 I/O 操作本身就要几十毫秒。
- 初始化组件:调用 Init() 函数,分配内部数据结构。
- 回到 Java 层:封装成 MediaCodec 对象返回。
我曾经在 native 层加了 trace,发现有些第三方 Codec 库 strace 一下能看到 100ms+ 的 I/O 等待。为什么会这样?因为人家把解码器做成了插件,每次启动都要从磁盘读取配置文件。
嗯,这里要注意,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 的资源预留。它虽然轻,但如果你同时在编解码视频和音频,系统要为两个实例都预留资源。有些硬件平台混合编解码时资源是有上限的,超出就会退化为软件解码,那速度就慢了。
| 阶段 | 耗时范围 | 占比 |
|---|---|---|
| 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。
第三斧:精简 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()查看。