MediaCodecList 查询开销:那些你容易踩的坑
说实话,很多人刚开始用 MediaCodec 时,都会忽略 MediaCodecList 的查询开销。我之前带的一个项目,在视频通话初始化阶段总卡顿几秒钟,查了半天才发现问题出在 getCodecInfos 和 getCapabilitiesForType 这两个方法上。今天我就把自己踩过的坑和优化思路说清楚。
1. 核心问题:为什么查询会慢?
你想想看,MediaCodecList 的本质是什么?它是向底层服务查询当前设备上所有可用的编解码器信息。每次调用 getCodecInfos(),系统都要遍历整个编解码器列表,返回一个 MediaCodecInfo 数组。如果你再对每个 codec 调用 getCapabilitiesForType(),那更是一场灾难——这个方法内部会创建新的能力对象,并触发 Binder 调用去读取详细的 Profile、Level、ColorFormat 等。
我在项目中遇到过,一部中低端手机,单纯执行一次 getCodecInfos() 加上对 20 多个 codec 调用 getCapabilitiesForType(),耗时能超过 300ms。如果放在主线程,直接就是掉帧甚至 ANR。
| 操作 | 平均耗时 (ms) | 说明 |
|---|---|---|
| getCodecInfos() 一次 | 10~30 | 返回所有 codec 的名称列表 |
| getCapabilitiesForType() 一次 | 5~15 | 单个 codec 的详细能力查询 |
| 遍历全部 25 个 codec 并查询 | 150~400 | N 次 Binder 调用 |
注意:千万不要在主线程中循环调用 getCapabilitiesForType。如果你需要反复查询,考虑缓存结果。
2. getCodecInfos 内部的性能陷阱
getCodecInfos() 是 MediaCodecList 的实例方法。很多人会这样写:
MediaCodecList list = new MediaCodecList(MediaCodecList.ALL_CODECS);
MediaCodecInfo[] infos = list.getCodecInfos();
for (MediaCodecInfo info : infos) {
// ... 处理
}
这里有两个陷阱:
- 每次 new MediaCodecList 都会重新查询底层。 即使底层 codec 列表没变,系统也不会帮你缓存。我习惯把 MediaCodecList 实例做成单例,或者用静态方法
MediaCodecList.getCodecInfos()——其实后者内部也是 new 了一个临时对象,同样有开销。 - 返回的 MediaCodecInfo[] 包含 所有 类型的 codec。 包括视频、音频、甚至一些系统内部 codec。如果你只需要某个特定类型(比如 video/avc),那么遍历全部是浪费。
我的建议:只创建一次 MediaCodecList 实例,然后缓存它。如果你只关心某一类编码,可以用 MediaCodecList(MediaCodecList.REGULAR_CODECS) 过滤掉一些实验性 codec。
3. getCapabilitiesForType 隐藏的“雷”
这个方法是真正的性能杀手。为什么呢?我们看看它的签名:
MediaCodecInfo.CodecCapabilities caps = info.getCapabilitiesForType("video/avc");
每次调用,系统都会:
- 根据 MIME type 在内部查找对应 component。
- 通过 Binder 向 mediaserver 发起一次查询请求。
- 解析返回的数据,构造
CodecCapabilities对象(包含 profile、level、color format 数组等)。
说白了,这就是一次跨进程调用!如果你的设备上有 30 个 codec,每个都查一遍,那就是 30 次 Binder IPC。我在实际项目里测过,某些定制 ROM 上单次就能到 12ms,累计就是 360ms。
更隐蔽的是:即使你只想要 isFeatureSupported 或某个 color format 是否支持,系统仍然会把全部能力数据拉回来。没有懒惰加载。
4. 我的优化策略
经过几次线上问题的教训,我总结了一套可落地的方案:
4.1 复用 MediaCodecList 实例
// 全局单例,只创建一次
public class MediaCodecHelper {
private static MediaCodecList sList;
public static MediaCodecList getCodecList() {
if (sList == null) {
sList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
}
return sList;
}
}
4.2 缓存 getCapabilitiesForType 结果
// 用 Map 缓存:key = codecName + mimeType
private final Map<String, CodecCapabilities> capsCache = new HashMap<>();
public CodecCapabilities getCaps(String codecName, String mimeType) {
String key = codecName + "@" + mimeType;
if (capsCache.containsKey(key)) return capsCache.get(key);
MediaCodecInfo info = findCodecByName(codecName);
if (info == null) return null;
CodecCapabilities caps = info.getCapabilitiesForType(mimeType);
capsCache.put(key, caps);
return caps;
}
4.3 只查询真正需要的 codec
不要遍历全部。先按 MIME 过滤:
// 使用 MediaCodecList 的 findDecoderForFormat / findEncoderForFormat
MediaCodecList list = getCodecList();
String codecName = list.findDecoderForFormat(format); // 只返回第一个匹配的
if (codecName != null) {
MediaCodecInfo info = list.getCodecInfos()...; // 再找 info
CodecCapabilities caps = getCaps(codecName, mimeType);
}
关键指标:优化后,整个查询阶段耗时从 300ms 降到 2~5ms(首次缓存后几乎为 0)。
5. 一定要在子线程做
即使你做了缓存,第一次初始化还是可能有点耗时。我个人习惯在 Application.onCreate() 里异步初始化:
new Thread(() -> {
// 触发查询和缓存
MediaCodecHelper.getCodecList();
MediaCodecHelper.getCaps("OMX.google.h264.decoder", "video/avc");
// ... 需要预热的 codec
}).start();
这样真正用到时都是缓存命中,主线程零卡顿。
6. 别忘了释放资源
嗯,这里有个小细节。虽然 MediaCodecList 本身不需要 close,但如果你缓存了大量的 CodecCapabilities,注意内存占用。一般几百 KB 没问题,但不要无限制地缓存所有组合。我曾经见过一个同学把每种 MIME 都缓存好几个,结果占用了几 MB——其实只有两三个常用类型值得缓存。
一句话建议:把 MediaCodecList 查询当成 I/O 操作来对待——异步、缓存、减少调用次数。这样你的 MediaCodec 实例化阶段就能快出不少。