第2章 start调用前准备:configure阶段的影响归纳总结
上一章我们看了start()的整体流程,这一章咱们聚焦在它前面的那道「关口」——configure。很多人以为configure只是设个格式、传个surface完事。但说实话,configure做得好不好,直接决定start()能多快把流水线跑起来。我自己调过的项目里,有一半的启动卡顿都根在configure阶段埋下了雷。
configure到底干了什么?
别被名字骗了。configure不是简单的参数赋值。它内部至少要完成以下几件事:
- 解析并校验输入参数:音频格式、视频尺寸、关键帧间隔、码率控制模式……每个参数都要走合法性检查。比如MediaFormat里填了错的profile,configure直接就给你抛异常。
- 创建内部组件实例:Codec内部会实例化解码器/编码器的具体实现(OMX组件或C2组件)。这个过程需要从注册表里查找、加载so库。
- 协商输入输出端口配置:组件要告诉你它能支持多少个buffer、buffer最小/最大尺寸、颜色格式偏好等。configure会做一次双向协商。
- 分配部分资源:内存池?可能先预分配一部分。Surface?如果传了Surface,它会提前注册到BufferQueue上。
- 设置初始状态:将Codec状态转移到Configured。这时候组件还没进入Running状态,但已经准备好接收数据了。
关键点:configure阶段做的事情越多,start()需要重新协商的内容就越少。但configure本身也会耗时。咱们的目标是「找到平衡点」——不让configure过度延迟,也不让start()因为configure做得太少而反复重配。
影响configure耗时的核心因素
我多年踩坑经验,可以归纳成下面这几个维度。每个维度都对应着实际代码里的参数或者系统行为。
| 维度 | 典型参数/场景 | 耗时等级 | 我的备注 |
|---|---|---|---|
| 音视频编解码器类型 | H.264 vs HEVC vs VP9 | 中等 | 越是新的编码标准,组件协商越复杂 |
| 分辨率与帧率 | 1080p vs 4K vs 8K | 高 | 我测过8K视频,configure比1080p慢了3倍 |
| Surface模式 vs 纯软解码 | surface=false / surface=Surface | 较高 | Surface模式要握手图形buffer |
| 是否指定具体组件名 | setCodecName或不指定 | 低~中 | 不指定的话系统要遍历所有匹配组件 |
| 参数有效性检查深度 | 编码时profile/level组合 | 低 | 传递非法参数会触发回退重试,翻车 |
| 同一Codec实例被复用 | reset后再次configure | 中 | 部分组件可能保留残留状态,耗时增加 |
避坑指南:我曾经在4K编码场景里随手填了"profile=0",结果configure/start来回折腾了500多ms。后来发现是android-10之前的组件对profile=0处理有bug,导致协商超时。所以:「填参数时尽量明确,别依赖默认值」。
configure阶段的关键路径时序
咱们从代码层面走一遍,看看configure真正在耗时的几个环节。下面是一个伪代码时序图(实际对应C++层的ACodec或CCodec):
- Step 1 - 参数校验:调用MediaCodec.native_configure() -> 进入JNI层。检查各类参数是否合法,耗时通常<1ms,但如果有大量非法组合可能翻倍。
- Step 2 - 创建组件实例:调用GetComponentInstances()或MediaCodecService创建具体组件。这里是耗时的重灾区——需要动态加载so库,解析XML配置文件,甚至拉起mediametrics服务。我测过某些OEM机器上这一步能到200ms。
- Step 3 - 端口协商:组件返回支持的端口属性,Codec再根据MediaFormat里设置的值做匹配。如果不匹配,有些组件会报错,有些会直接改你的参数(比如改你的color-format)。耗时5~50ms不等。
- Step 4 - 资源预分配:比如为输出Surface绑定BufferQueue slot,或者给解码器分配初始数据缓冲区。这个阶段受SurfaceFlinger响应速度影响很大。
- Step 5 - 状态迁移:从Uninitialized到Configured。内部会更新状态机,触发异步消息。耗时很短,但阻塞。
咱们把上面流程画成一张SVG图,方便记忆。这张图也是我课程里反复会参考的路线。
实测数据:不同场景下的configure耗时
我自己在Android 12(芯片平台是骁龙8Gen1)上跑了几组实验,结果如下。注意这是纯configure时间,还没调用start。
| 场景 | 分辨率 | 编码器/解码器 | configure耗时 (ms) | 备注 |
|---|---|---|---|---|
| H.264 解码 | 1920x1080 | c2.qti.avc.decoder | 6~9 | 软解模式,使用OMX.google |
| H.264 解码 | 1920x1080 | c2.qti.avc.decoder + Surface | 18~25 | 硬解Surface模式 |
| H.265 解码 | 3840x2160 | c2.qti.hevc.decoder + Surface | 35~50 | 4K需要更大buffer协商 |
| AAC 解码 | 不适用 | c2.android.aac.decoder | 1~2 | 音频通常很快 |
| H.264 编码 | 1920x1080 | c2.qti.avc.encoder | 12~18 | 编码器通常更重 |
注意:上面数值来自特定设备,不同OEM、不同Android版本可能有巨大差异。我建议你在做性能优化时,在目标机型上摸底,别盲信通用数据。
如何优化configure阶段的耗时?
嗯,这里总结几条我实际用过且有效的策略:
- 提前做好参数预校验:在业务逻辑里先检查分辨率、格式、profile是否合理,避免configure阶段因为参数不合法而反复内部重试。我曾经踩过resize到奇数尺寸的坑,configure直接卡了200ms才报错。
- 尽量使用已知的组件名:如果确定要使用硬件解码器,直接用MediaCodec.createByCodecName("c2.qti.avc.decoder"),跳过系统遍历所有组件的步骤。这样可以节省10~30ms。
- 如果只是解码,不要设置不必要参数:比如编码中的bitrate、frame-rate,解码器根本不关心。设置太多参数可能引发额外的协商。
- 重用Codec实例时注意reset:如果同一个MediaCodec对象要切换格式,先调用reset(),等2帧时间再configure。否则有些组件状态残留,configure内部要做清理,反而更慢。
- 考虑延迟创建Surface:如果你用Surface模式,但Surface本身还没准备好(比如Activity还没onResume),可以先把configure里的setSurface设为null,等真正显示时再用MediaCodec.setOutputSurface切换。configure阶段省去Surface握手,一般能快10~20ms。
小结
configure不是启动的准备动作,它就是启动的一部分。很多同学习惯把start耗时单独优化,却忽略了configure里埋下的坑。说白了,configure做得越「干净」、参数越「确定」,start就启动得越顺滑。
我常用的检查手段:在configure前后加Log.e("MediaCodecTiming", "configureStart=" + System.nanoTime() + ", configureEnd=" + System.nanoTime()),然后看差异。如果单次configure超过100ms,建议用Systrace抓一下是哪一步在磨蹭。
接下来咱们就要正式进入start()本身了。但别急,先消化好这个过程——configure带来的影响,会像影子一样跟着整个生命周期。