第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阶段关键流程图 参数解析与校验 2. 创建组件实例 3. 输入/输出端口协商 4. 资源预分配 5. 状态迁移至Configured 说明: 组件实例化(红色节点)是configure阶段最不可控的一步,涉及so加载和组件匹配。 如果不想走自动匹配,可以用MediaCodec.createByCodecName()指定组件,能节省遍历时间。

实测数据:不同场景下的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带来的影响,会像影子一样跟着整个生命周期。