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~400N 次 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");

每次调用,系统都会:

  1. 根据 MIME type 在内部查找对应 component。
  2. 通过 Binder 向 mediaserver 发起一次查询请求。
  3. 解析返回的数据,构造 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();

这样真正用到时都是缓存命中,主线程零卡顿。

MediaCodecList 查询优化决策流程 开始创建 MediaCodecList 陷阱1:调用 getCodecInfos() 全量获取 陷阱2:对每个 codec 调用 getCapabilitiesForType 是否需要重复查询? 是(无缓存) 否(缓存可用) 复用 MediaCodecList 实例 缓存 CodecCapabilities 结果 Binder IPC 重复调用 → 慢 耗时 150~400ms,应挪到子线程 终极方案:异步预热 + 缓存 + 按需查询

6. 别忘了释放资源

嗯,这里有个小细节。虽然 MediaCodecList 本身不需要 close,但如果你缓存了大量的 CodecCapabilities,注意内存占用。一般几百 KB 没问题,但不要无限制地缓存所有组合。我曾经见过一个同学把每种 MIME 都缓存好几个,结果占用了几 MB——其实只有两三个常用类型值得缓存。

一句话建议:把 MediaCodecList 查询当成 I/O 操作来对待——异步、缓存、减少调用次数。这样你的 MediaCodec 实例化阶段就能快出不少。