
做音视频开发这几年跟MediaCodec处理器打交道的时间一长就会发现一个规律很多看似玄学的问题比如首帧延迟高、部分设备硬解黑屏、编码器创建偶尔会慢得离谱追到根上往往都在初始化过程里。MediaCodec对应用层暴露的能力很干净但背后Java层、JNI层、C层、HAL层接了好几条链路任何一环慢了或者拉胯了最终都要卡在createXXXByType到configure这一小段窗口里。Codec2框架替代OMX之后初始化链路更是换了一套玩法这套机制不梳理清楚后面排查问题基本靠猜。这篇主要讲MediaCodec/Codec2的组件初始化过程从Java层入口一路挖到Codec2框架的组件仓库把关键节点、状态切换和日常开发中最容易踩的坑都过一遍。如果你是做系统定制、播放器SDK、视频编辑类应用或者只是在折腾编码参数适配的这篇对你应该都有帮助。1. 先搞清楚MediaCodec到底在初始化什么说到初始化你得先明白MediaCodec这个对象在初始化阶段要做成什么事才能理解为什么这条链路会这么长。MediaCodec本质上只是一个“客户端代理”它自己不负责具体的编解码工作而是要把上层传下来的MIME类型、容器格式、硬件能力要求翻译成底层能理解的参数然后找到对应的“引擎”把这个会话跑起来。一次完整的初始化要做四件事确定编解码器类型解码器还是编码器H264还是HEVC这是最基础的匹配逻辑找到能干活的具体组件硬件厂商提供了哪些Codec组件软件库兜底用哪个这需要查询组件仓库协商输入输出格式颜色格式、分辨率、帧率、码率这些参数不是设了就生效而是要经过组件能力校验拉起执行环境线程、内存分配器、Buffer队列这些资源要在初始化阶段准备好不然到编解码中途再申请就会卡顿甚至崩溃当我们调用MediaCodec.createDecoderByType(video/avc)时系统实际上并不是立刻创建一个编解码器实例而是先走一遍“发现组件”的流程。在Codec2框架下这一步会去查询所有已注册的组件仓库看哪个组件能处理video/avc然后再决定用哪个。所以说白了初始化过程的核心矛盾是应用层只给了一个MIME字符串但系统要把这个字符串变成一个能干活的具体实例中间要过好几层筛选和协商。2. 从Java层开始的调用链2.1 MediaCodec的构造函数与静态工厂大多数应用开发者接触到的第一个入口是MediaCodec.createDecoderByType(String type)这个静态方法内部做的是public static MediaCodec createDecoderByType(String type) { return new MediaCodec(type, /* name */ null, /* encoder */ false, /* codecInfo */ null); }构造函数会调native_setup这个native方法方法签名是native_setup(Object mediaCodec, String name, boolean nameIsType, boolean encoder, int pid, int uid, Nullable String opPackageName)。这里有一个容易被忽略的细节传入的name参数当nameIsType为true时它其实还是那个MIME字符串真正的组件名解析是在后面做的。也就是说Java层到native层之间没有做任何组件匹配逻辑只是把参数传递下去。2.2 JNI层的桥接作用JNI层的实现在frameworks/base/media/jni/android_media_MediaCodec.cpp核心函数是android_media_MediaCodec_native_setup。这一步值得注意的地方是它不只是简简单单地把Java参数转成C参数而是做了三件重要的事情把Java的MIME字符串转成status_t和AString丢失的信息要在这里显式记录同时设置了极简的媒体3部曲通信能力检查、资源创建从Java层拿到进程PID和UID传给native层用于后续的能力查询和资源限制JNI层还悄悄做了一件事检查调用者是否有权限使用这个Codec类型。权限这层在Android 10之后逐步加强尤其是对DRM相关格式比如video/avc01这种隐藏敏感类型如果应用没有富媒体权限会在这里被直接拒绝。2.3 EventHandler线程的创建native_setup里还有一个容易被忽略的步骤就是创建ALooper和AHandler。MediaCodec的所有异步回调比如OnFrameRendered、onOutputFormatChanged都是靠这个线程派发的。JNI层会把一个JMediaCodec对象和一个JMediaCodecListener包装起来挂到这个Looper上之后native层的事件都会通过这个通道回传给Java层。这一步对于初始化意味着什么它说明MediaCodec的初始化天然就是一个异步模型Looper线程先跑起来native层的编解码器创建完成后才能开始往这个通道上报事件。如果你在configure()之后立刻调用start()但OnFrameAvailable迟迟不来大概率就是Looper线程还在初始化过程中没有进入message loop状态。3. Native层的组件选择逻辑3.1 MediaCodec::Create的核心流程JNI层最终调用的native函数是MediaCodec::Create实现在frameworks/av/media/libstagefright/MediaCodec.cpp。这个函数是整个初始化链路里最关键的一层因为它决定了后面到底用Codec2还是用旧的OMX框架。spMediaCodec MediaCodec::Create( const AString name, bool nameIsType, bool encoder, const spMediaCodecList codecList, spAMessage msg) { // 先尝试创建Codec2的客户端代理 std::shared_ptrCodec2Client codec2Client Codec2Client::CreateCodec2Client(); if (codec2Client ! nullptr) { // 用Codec2的方式创建组件 spMediaCodec codec new MediaCodec(name, nameIsType, encoder, codecList, codec2Client); // ... } // 回退到旧的OMX/软件路径 }从Android 8.0开始系统会优先尝试通过Codec2客户端创建组件。Codec2Client::CreateCodec2Client这个函数会去检查设备上是否注册了可用的C2服务。这个检查不是一次性的静态扫描而是通过ICodec2::getStore()去拿组件仓库的代理。如果拿不到——比如硬件平台不支持C2或者服务没起来——才会走CreateOMX的老路。值得注意的是这里的选择并不是简单的“非此即彼”。即使是Codec2框架下有些软件实现的编解码器比如SW HEVC解码器也是作为C2组件存在的但它们并不是走HIDL服务而是直接内嵌在C2平台仓库里。所以“走Codec2”不代表一定是硬件编解码具体还要看组件仓库返回的是哪个具体组件。3.2 getCodecBase与MediaCodec的骨架拼接创建完成后MediaCodec对象会把组件基类MediaCodecBase挂到自己的状态机上。这一步的代码逻辑比较绕但它的本质是MediaCodec本身是一个壳负责状态机管理、Buffer管理、格式协商MediaCodecBase是一个抽象基类真正的编解码逻辑在派生类里MediaCodec::init会调用getCodecBase拿到具体的组件基类赋值给mCodecBaseCodec2框架下的MediaCodecBase实现类是MediaCodec2这个类持有std::shared_ptrCodec2Client::Component也就是实际的C2组件代理。所以你可以在MediaCodec2里看到所有最终要发给C2组件的参数都会被缓存下来等状态机进入Initialized之后再统一提交。3.3 Codec2Client的角色定位Codec2Client本质上是Codec2框架在MediaCodec内部的客户端代理它做了几层封装包了一层std::shared_ptrICodec2这是HIDL接口的客户端句柄提供CreateComponent、CreateStore等便捷方法内部转成HIDL调用缓存了C2ParamReflectorHelper的实例用于参数反射和校验在初始化阶段我们需要重点看的是Codec2Client::CreateComponent这个方法。它会调用HIDL层的store-createComponent()传入组件名或MIME符然后拿到组件实例后还要做一次参数协商读取组件的默认参数、能力参数缓存到本地std::shared_ptrC2Component里。这里有个容易踩的坑Codec2Client::CreateComponent传入的组件名有两种情况一种是“具体名”比如c2.android.avc.decoder另一种是“符号类型名”比如video/avc。如果是后者需要在组件仓库里做一次“按MIME类型匹配”的扫描。扫描顺序和优先级很讲究保证硬件组件优先软件组件兜底。这个优先级逻辑不在MediaCodec里而是在C2PlatformStore里控制后面会详细讲。4. Codec2框架的组件仓库与发现机制4.1 C2PlatformStore与C2ComponentStore的关系Codec2框架这一层的设计可以类比成一个操作系统里的软件包管理器。C2PlatformStore是全局唯一的管理入口它负责维护多个C2ComponentStore每个C2ComponentStore可以理解成一个软件源里面挂着一批组件。在代码里C2PlatformStore是一个单例内部维护了一个std::vectorstd::shared_ptrC2ComponentStore。初始化时它会注册两种类型的StoreC2SoftStore承载软件编解码器组件比如C2软解AAC、C2软解HEVCC2HwStore从HIDL服务拿到的硬件组件仓库对应ICodec2Store的实现还有一个隐藏的Store类型C2Store它的作用是提供全局参数查询和反射能力但它不直接持有编解码器组件。很多刚接触Codec2的人会把C2Store和C2ComponentStore混为一谈实际上前者是后者的一个特殊实现或者说是门面重点是前者的queryParam和createComponent并不一定真的能创建出编解码器。4.2 C2SoftStore的组件装载流程软件组件怎么变成可用的C2组件这一步是通过编译期注册和运行期遍历完成的。框架里有一个C2SoftStore它在构造时通过C2SoftCodecManager分发一个组件描述符列表每个描述符都对应一个具体的组件实现类。组件描述符里包含的信息有组件名和时间戳标识支持的MIME类型列表编解码方向编码/解码能力评分Rank硬件组件评分高软件组件评分低用来排序默认参数集合当你发起一个video/avc的解码请求时C2SoftStore会遍历自己注册的所有描述符找到MIME类型匹配、方向匹配、当前设备支持比如CPU架构兼容的那一个然后返回它的名称。这个过程不会真正创建组件实例只是“找到名字”真正的实例化在后面的createComponent。4.3 C2HwStore与HIDL边界硬件厂商的编解码器组件通过HAL层暴露。在Android 10的系统上标准路径是frameworks/av/media/codec2/hidl下的ICodec2、ICodec2Store等HIDL接口。这个边界的意义在于系统框架不知道硬件组件的内部实现只知道它们的接口。C2HwStore::createComponent会走一次HIDL调用把组件名传到硬件服务端服务端再在自己实现的组件库里面创建对应的组件实例。这个过程有一个隐藏成本HIDL调用本身就是一次跨进程Binder通信如果你在低端机上大规模并发创建编解码器这里会成为性能瓶颈。我在一个项目里实测过15台测试设备里有两台在同时创建3个以上硬解实例时C2HwStore::createComponent的耗时从平均1ms飙到30ms以上现象就是应用层秒开失败率明显上升。排查了好久才发现是HIDL调用排队的问题不是编解码器本身能力问题。4.4 组件查找匹配的策略细节组件查找不是简单的strcmp字符串匹配尤其是在按MIME类型查找时内部会做多个维度的过滤。源码里有一个经典的函数叫matches它会对候选组件做如下校验基础类型匹配video、audio分类要一致MIME子类型匹配avc、hevc、aac或者通配符方向匹配编码器只匹配encoder类型解码器只匹配decoder类型优先级排序Rank值高的排在前面同Rank时按照注册顺序的先后排序能力过滤如果应用设置了Feature要求比如FEATURE_SecurePlayback硬件不支持secure会让该组件直接出局这个filter逻辑如果你在使用MediaCodecList时碰到过isEncoder、getSupportedFormats这些API就会觉得很眼熟。实际上MediaCodecList在Java层的输出结果正是基于同样的组件查找逻辑整理出来的。换句话说你在应用层拿到的CodecInfo能力列表大概率就是C2PlatformStore组件扫描的结果。4.5 自定义C2Store的热加载问题做系统定制时厂商经常会新增自己的硬件编解码器组件。一种常见做法是新增一个HIDL C2Store服务并注册到设备然后在系统启动阶段让C2PlatformStore能发现它。这个加载时机有个大坑C2PlatformStore是单例它只在进程第一次被访问时执行一次性扫描后续新增的Store服务不会自动被感知。如果你的定制方案是先启动了MediaCodec服务再动态注册新的C2Store那么旧的MediaCodec进程无论如何都拿不到新组件必须重启相关进程才能生效。这个问题的排查技巧是在C2PlatformStore::GetInstance()调用处加日志打印当前注册的Store列表和组件数量对比vendor分区里的config配置确认Store有没有被扫进来。5. 状态机切换与初始化时序5.1 状态机的五态模型Codec2组件本身的状态机是五态模型初始化涉及的只有前两态UNINITIALIZED组件刚创建时的状态此时没有分配任何编解码资源INITIALIZING正在初始化分配内部资源但还不能启动编解码INITIALIZED完成初始化参数可配置等待启动RUNNING编解码进行中ERROR组件内部异常不可恢复在MediaCodec这一侧状态机是另外一套模型Uninitialized-Initializing-Initialized-Configured-Running。所以一套编解码会话在初始化阶段实际上要过两套状态机MediaCodec的状态机是壳Codec2组件的状态机是核心。5.2 初始化时序的关键节点整个初始化过程的时序用文字描述大概是1. Java层调用 createDecoderByType(mime) 2. native_setup 创建 JMediaCodec ALooper 3. MediaCodec::Create 尝试创建 Codec2Client 4. Codec2Client 从 HIDL/本地仓库拿到 ICodec2 代理 5. Codec2Client 调用 store-createComponent(name/mime) 6. C2PlatformStore 扫描所有 Store匹配组件 7. 找到候选组件名后调用具体 Store 的 createComponent 8. 组件实现创建 C2Component 实例状态到 INITIALIZING 9. 组件完成自检状态到 INITIALIZED 10. MediaCodec 壳完成 init状态到 Initialized 11. 应用层拿到 MediaCodec 对象开始 configure/start这里的时间点5到9是最关键也最容易出问题的区间因为真正耗时的操作都集中在这里组件实体类的构造、内部缓冲池初始化、硬件加速器的握手。5.3 状态机切换中隐藏的死锁风险Codec2框架对状态机的管理是带锁的。组件内部维护了一把mInitLock和一把mStateLock初始化状态切换时需要同时持有这两把锁。这本来不是问题但如果你在做系统定制时没注意锁顺序——比如在组件代码里先拿了mStateLock再去等别的资源——就可能出现死锁。我踩过一次很具体的坑在某个软件编码器组件里增加了耗时初始化的逻辑大概60ms左右初始化过程中会回调一个上层接口去查询分辨率支持范围而这个查询偏偏又要走同一个组件的状态锁。结果就是初始化线程拿着init锁等待queryResult返回queryResult线程又等待状态锁释放两个线程直接僵住。这种死锁很隐蔽因为只在特定条件下触发初始化耗时足够长、queryResult线程被调度延迟。排查时靠常规的看日志基本没用最后是借助debuggerd抓ANR trace才看到两个线程卡在锁上的ABBA场景。6. 参数协商C2Param与初始化深度绑定6.1 参数从Java层到C2层的传递路径初始化不只是“找到组件并创建实例”这么简单它还要完成一轮参数协商。你在Java层设置的KEY_WIDTH、KEY_HEIGHT、KEY_COLOR_FORMAT、KEY_BIT_RATE等最终都会以C2Param的结构形式传给C2组件。参数传递路径是Java层MediaFormat拆成键值对configure()调用native层打包成AMessageMediaCodec2::Configure把AMessage转成C2Param集合查C2ParamReflectorHelper校验参数的合法性调用C2Component::setParameter下发参数6.2 C2ParamReflectorHelper的反射机制如果你第一次看C2ParamReflectorHelper会觉得跟Java反射非常像。它维护了一张参数ID到“参数解析器”的映射表每个解析器知道这个参数的类型、默认值、取值范围、是否可读可写。初始化阶段使用反射机制的主要原因是为了兼容性不同厂商的C2组件可能支持不同的参数集合系统框架不能硬编码所有参数名必须让组件自己“宣告”它支持什么参数。这跟在编译期强行绑死参数表比起来灵活得多。一个典型的例子是C2VideoFormatInfo这个结构它里面有width、height、colorFormat、frameRate等字段。在初始化阶段框架会先通过反射拿到当前组件的默认C2VideoFormatInfo然后把你设置的参数合入最后再校验一遍是否超范围。如果你的分辨率超过组件能力上限会在这里收到一个C2_BAD_VALUE错误提示反映到应用层就是configure返回异常。6.3 参数协商时的常见“静默失败”C2参数协商有一个让很多人困惑的行为有些参数设置不合理并不会报错而是被“静默忽略”。这是因为某些参数在组件实现里是只读的但反射表里标注了isWritable false框架在协商时会直接跳过它不返回任何错误码。比如你设置了KEY_PROFILE为AVCProfileHigh10但硬件组件只支持AVCProfileBaseline到AVCProfileMain在部分厂商实现里这个不一致不会被当成错误而是把实际的profile强制改回默认值。结果就是你拿到的输出码流里profile和你设置的不一样而且全程没有任何日志提示。排查这类问题唯一靠谱的手段是在MediaCodec2::Configure里加日志把设置前后的参数都打出来并且对比ICodec2返回的C2Param里的实际值。我建议所有做编码器参数适配的同事都在自己的调试代码里加上这一段对比逻辑。7. 初始化失败时的排查思路7.1 组件找不到的情况最常见的初始化失败就是组件找不到。排查时先确认三件事组件名或MIME类型是否拼写正确video/avc写成了video/AVC在大多数系统里匹配是大小写敏感的组件是否能被当前进程访问vendor分区的组件在system进程里可能因为SELinux策略无法创建组件是否被列入黑名单某些软编解码器在设备mtk/qcom平台上会被系统直接禁用避免抢占资源之前碰到过一个项目某款设备上c2.android.aac.decoder一直创建失败应用层报ERROR_CANNOT_CONNECT但同一个APK在另一款同芯片设备上就正常。最后对比/vendor/etc/media_codecs.xml才发现A设备的配置里压根没注册这个组件是厂商裁剪适配时把这个软解组件裁掉了。7.2 HIDL服务连接失败当你看到日志里出现ICodec2::connect failed或者getStore() returned null这类字样时基本就是C2的HIDL服务没起来。可能原因有vendor.media.c2服务进程崩溃了需要看logcat -b crash和dmesgSELinux策略挡了跨进程访问看看avc: denied日志有没有出现服务起来太晚MediaCodec进程启动得更早连接失败后没有重试机制有一种只发生在开机阶段的玄学问题系统服务刚启动时MediaCodec被谁提前发起了初始化但此时C2 HIDL服务还在启动流程中没注册完。这时候创建组件就会失败而且系统不会自动重试。解决方法是把任何涉及MediaCodec的业务进程延迟到系统BOOT_COMPLETED广播之后再开始。7.3 创建耗时过长初始化耗时过长是另一种“软失败”。Codec2组件创建时硬件编解码器可能需要打开设备节点、初始化DSP固件、分配专用内存这些操作在个别设备上动辄几百毫秒。如果应用层没有做好异步处理用户在打开播放器时就可能看到黑屏300ms到1s。排查这类问题重点看三块libstagefright的调用耗时通过自定义TimedEvent打点C2组件的构造函数耗时说明资源分配太重HIDL Binder调用的排队情况说明服务端并发资源不够在软解场景下初始化耗时问题往往不是组件创建本身而是第一次start()时软解线程池才真正创建内部编码器上下文。换句话说你在初始化阶段根本看不出问题等到首帧输出才发现天都黑了。8. 实操心得与建议从这个初始化链路里我总结几个可以落地的经验第一应用层创建MediaCodec对象时不要把它当成一个廉价操作。每创建一个实例底层可能要跨进程、做参数反射、初始化内存池。正确姿势是复用对象避免频繁创建销毁。第二如果要批量创建多个编解码器最好串行创建而不是并发创建。并发创建高概率触发HIDL服务端的线程资源和锁竞争反而比串行更慢。第三做系统定制时修改组件注册信息后不生效先别急着查Java层代码直接在C2PlatformStore::GetInstance()上加日志看扫描结果往往十分钟就能定位到是配置还是服务注册的问题。这段内容虽然讲的是Codec2初始化但很多思路在OMX框架下也通用。核心就是别把初始化当黑盒要清楚每一步在做什么、有没有锁、有没有跨进程调用这样出了问题你才知道从哪下手。