
1. 先理解问题的根源AAR 到底是个什么“包”聊到“aar引用打包问题”之前我们先把基础踩实。AARAndroid Archive本质上是 Android Library 的标准打包产物里面包含编译后的代码classes.jar、Android 资源res、清单文件AndroidManifest.xml、so 库jni、R 类以及 ProGuard 规则等。很多人把它私下叫“Android 的 jar 加强版”——实际上并不准确jar 里只有字节码AAR 是“字节码 资源 清单 原生库”的组合体所以它天然带着 Android 平台的各种信息不能像普通 jar 那样直接用 java -cp 跑起来。在实际项目里AAR 的引用场景分几类一是模块间依赖比如 app 模块依赖一个 library 模块Gradle 直接拿模块源码编译通常不涉及 AAR 文件本身的搬运二是外部 AAR 依赖比如从某合作方手里拿到一个编译好的 xxx.aar扔进 libs 目录在 build.gradle 里用 fileTree 或者 flatDir 声明引入三是二次封装比如我基于某个基础 AAR 包再包一层对外发布一个新的 AAR。绝大多数“引用打包问题”集中在后两类。尤其第三类最坑因为你会遇到一个经典连环问我引用了别人的 AAR打成自己的 AAR 之后为什么别人拿到手运行就崩为什么类找不到为什么资源引用不到这些问题不是玄学而是因为AAR 是一个“扁平的、面向直接消费者”的产物它默认不会把“自己依赖的其他 AAR”打包进肚子里。这是一个关键认知后面所有方案都围绕它展开。2. 为什么 AAR 依赖不会自动传递要理解这个现象得从 Gradle 的依赖传递机制说起。在 module 级别你声明implementation project(:xxx)或者implementation com.example:lib:1.0.0时Gradle 会计算出一个完整的依赖图A 依赖 B、B 依赖 CApp 直接依赖 A 时C 也会被拉进编译和打包范围——这就是传递依赖。但 AAR 文件本身不携带 Gradle 依赖描述信息的完整运行时解决方案。AAR 里有一个pom.xml或者 Gradle Module Metadata这个文件能告诉消费者的构建工具“我还依赖了哪些远程坐标groupId:artifactId:version”仅此而已。它不能告诉你“我的依赖在哪个本地路径”“我的依赖资源怎么嵌入”。一旦你把这颗 AAR 手动丢进 libs 目录用implementation files(libs/xxx.aar)引入Gradle 就把它当成一个“孤立文件包”不会主动解析它的 pom 依赖更不会自动去仓库下载对应的传递依赖。更细的坑在transitive默认值上。即便你用api声明一个远程 AAR如果这个 AAR 是用implementation来依赖的它传递出来的依赖也是runtime级别的消费者的编译期感知不到只有运行时才会报 NoClassDefFoundError。而如果你在 libs 目录手动引 AAR默认的transitive行为又独立于坐标依赖规则经常出现“明明 gradle 能看到这个 aar编译也过了打包后却没进 Apk”。注意这里说的“没进 Apk”不是说 classes.dex 里没有这个 AAR 的代码而是指 AAR 依赖的“其他 AAR”没有进入最终产物。AAR 自身的代码如果没进去那说明打包配置根本就没包含它属于另一类问题。3. 本地 AAR 依赖的两个高频坑位3.1 flatDir 仓库引入的检索陷阱很多项目用flatDir { dirs libs }来引入本地 AAR然后写implementation(name: xxx, ext: aar)。这套写法在 Gradle 4.x 时代很常见到 Gradle 7.x 的 Kotlin DSL 和 Groovy DSL 下也能用但暗藏两个隐患第一个隐患是flatDir 依赖不受仓库坐标版本管理。如果你 libs 目录里同时有xxx-1.0.aar和xxx-2.0.aarflatDir 会按文件名字典序或依赖声明顺序去取极易取到老版本而且很难在依赖报告中察觉。第二个隐患是flatDir 与远程仓库的解析优先级容易混乱同一坐标在 libs 存在在 mavenCentral 也存在时默认策略不是“以 libs 为准”而是依 Gradle 仓库声明顺序从前往后尝试一旦远程仓库里出现了同名坐标的老版本你会被静默降级。这里我的建议是如果 AAR 只是项目内部用就别用 flatDir把 AAR 放进一个独立的本地 Maven 仓库目录用maven { url .../local-repo }声明再用标准坐标方式引入。这样依赖解析、传递依赖、版本冲突处理都走标准通道能规避大量诡异问题。3.2 用 implementation 直接引 AAR发布时必踩的透传坑假设你在login-library模块里写了这样一段dependencies { implementation files(libs/biz-core.aar) implementation files(libs/biz-common.aar) }然后login-library被打成login-library.aar发布。消费者拿到这个 aar 后在项目里声明implementation files(libs/login-library.aar)编译能过吗——如果你的业务代码用到biz-core里公开的类编译大概率报“找不到符号”因为implementation 依赖对外是不可见的消费者模块根本不知道还有 biz-core 这层间接依赖。如果把上面改成api files(libs/biz-core.aar)编译期是能过了但运行时大概率崩因为 Gradle 在打包 APK 时只把“可见的、可传递的、解析完整的运行时依赖”打进去而文件型 AAR 依赖的传递本身不完整尤其当 biz-core 又依赖了 biz-common 时这条链会在打包阶段被截断。一句话总结本地 AAR 之间如果存在互相依赖别指望用 implementation 或者 api 把它们串起来就算完事。你需要的是把“依赖图”真正展开让所有叶子 AAR 都进入最终产物。4. 把 AAR 打包进 AAR 的两种方案你问我有没有方案能把 AAR 完整地“嵌进”另一个 AAR答案是有的而且不止一条路。4.1 方案一用 Gradle 的 embed 能力自行配置推荐Gradle 官方并不直接支持embed关键字但我们可以借用configurations自定义一个embed配置然后在打包任务里把该配置下的所有 AAR 文件解压并重新合并到目标 AAR 中。这个思路的关键点在于理解打包流程AAR 本质上是个 zip 文件它的根目录下有classes.jar、res/、AndroidManifest.xml、assets/等条目。把多个 AAR 合并就是把这些条目做同级合并。具体做法大体三步第一步定义 embed 配置configurations { embed } dependencies { embed files(libs/biz-core.aar) embed files(libs/biz-common.aar) implementation files(libs/biz-core.aar) implementation files(libs/biz-common.aar) }implementation保证编译期可见embed保证打包期会带上。第二步写一个合并任务task mergeAar(type: Zip) { def embedFiles configurations.embed.files // 先解压每个 aar 到临时目录再把 classes.jar 合并、资源目录合并 // 最终生成新的 aar 包结构 }这里面工作量大一些classes.jar 需要把多个 jar 解压成 class 目录再重新打成 jar遇到重复类要处理冲突规则res 目录要做资源合并重叠的资源名要决定保留哪个AndroidManifest.xml 要做节点合并R.txt和R.jar要重新生成。技术上是完全可行的但工程量不小适合有构建体系团队的场景。第三步替换默认打包产物。让bundleReleaseAar或者assembleRelease依赖这个 mergeAar 任务把最终产物挪到目的地。4.2 方案二把间接依赖全部转成本地 Maven 坐标更省心与其在打包阶段做“缝合怪”不如把依赖关系摆回正轨。具体做法是把 biz-core.aar、biz-common.aar 安装到你自己的私有 Maven 仓库本地仓库或公司内部仓库都行然后在模块里改用标准坐标dependencies { api com.example:biz-core:1.0.0 api com.example:biz-common:1.0.0 }这样当你把 login-library 打包成 AAR 发布时它的 pom 文件里会记录这两个坐标消费者的构建工具会自动去仓库拉取依赖。这是官方推荐的依赖管理方式也是网上各种“aar 引用打包问题”的终极解药。但方案二有它自己的前提你的消费者项目必须能够访问到这个私有 Maven 仓库地址。如果对方在离线的内网环境、或者不愿意配置仓库源方案二就失效了这时只能退回到方案一那种“嵌入式打包”。4.3 基于实测的“模拟项目X”经验我用模拟项目X做过两轮验证第一轮纯粹的 fileTree 引用和 flatDir 混合最终打包产物在 app 运行时直接 NoClassDefFoundErrordumpsys 里看 dex 文件业务的代码有但 biz-core 的类全没进去第二轮改用“自定义 embed 配置 解压重打包”在构建机上出包后把 AAR 用 apkanalyzer 扫描biz-core 的类、资源、so 全部进包消费者只需要单纯依赖这一个 AAR 就能跑起来。第二轮里的重打包脚本我建议别用 Gradle 内置的 Zip 任务直接复制目录因为会有资源合并冲突。稳妥做法是先把所有 AAR 解压到同一个指定目录然后手动处理 res 类型的合并规则classes.jar用 jar 工具合并jni/目录直接按文件复制若存在同 ABI 目录下的同名 so需要按版本覆盖策略处理。注意如果你决定采用方案一请把 mergeAar 任务与标准打包流程解耦别在android配置块内部写自定义逻辑否则 Android Gradle Plugin 升级后大概率这块会坏掉。5. 两种“半引用”场景也要留意5.1 引用 AAR 里的 so 文件这个问题经常和“aar 引用打包问题”同时出现。很多人以为 AAR 的 so 文件只要在 libs 里放好了打包 APK 时会自动进 jniLibs。其实并不是这样如果 AAR 是通过implementation files(...)引入的AGP 会识别 AAR 里的jni/目录并与 app 模块的jniLibs目录合并但如果 AAR 包里的 so 架构不全比如只有arm64-v8a而 app 指定了abiFilters arm64-v8a, armeabi-v7a运行时在 v7a 设备上会崩因为 v7a 目录里根本没有对应 so如果你在打包 AAR 时用了packagingOptions排除了一部分 ABI 目录消费者也会受影响。排查技巧用unzip -l xxx.aar | grep jni看 AAR 里的 jni 条目是否完整。不能只看 Apk 里有没有对应 ABI因为设备可能“向上兼容”加载也可能不兼容。5.2 引用 AAR 里的资源与 R 类资源型 AAR 的合并风险更高。举个例子biz-core.aar 里定义了一个res/values/strings.xml里面有个app_name你的 login-library 模块也定义了同名资源——在打包 AAR 合并阶段AGP 可能会有提示但不会报警consumer 端打包 APK 时如果两个资源值不一样会按 AGP 自身规则处理但一旦 settings 里开了android.enableResourceOptimizations之类的优化项资源 ID 的行为可能改变。这里建议在发布 AAR 前用 lint 检查DuplicateResources或者在构建脚本中显式指定资源合并策略。R 类的问题更隐蔽当 biz-core 里用R.string.xxx访问资源消费者打包 APK 时会重新映射资源 ID正常情况下不会有问题但如果 consumer 端开启了resource shrink而 biz-core 的资源没有被标注为 keepR 类引用会被裁剪掉运行时 ResString 访问直接爆异常。6. 一套 4 层校验法快速验证 AAR 引用是否成功在解决完问题之后怎么确认打包结果没问题我的经验是层层检查缺一不可。第一层检查 Gradle 依赖报告在项目根目录执行./gradlew :app:dependencies --configuration releaseRuntimeClasspath看 AAR 模块的坐标是否出现在列表里以及它的传递依赖状态。如果发现某个依赖旁边标着(n)含义是“已忽略”此时要去排查版本冲突或者 exclude 配置。第二层检查最终 APK 的 dex 结构用 Android SDK 自带工具apkanalyzer dex packages app-release.apk或者用 jadx 反编译看看你期望的类名是否存在于 classes.dex。这一步能直接证明“类有没有打包进去”。我的团队验证过ClassNotFoundException 的场景里dex 里搜不到对应类NoSuchMethodError 的场景里类在但它依赖的另一个库版本不对。第三层检查 AAR 包的内容对于“把 AAR 嵌进 AAR”的场景直接用 zip 工具查看unzip -p login-library.aar classes.jar classes-inner.jar jar tf classes-inner.jar | grep biz-core这个套路最快能看到 biz-core 的类是否被合并进 login-library 的 classes.jar。第四层运行时动态验证在 app 的启动阶段用反射尝试加载目标类并调用方法同时 catch 异常打印堆栈try { val clazz Class.forName(com.example.bizcore.BizEntry) clazz.getMethod(init, Context::class.java).invoke(null, applicationContext) } catch (e: ClassNotFoundException) { Log.e(DependencyCheck, class missing, e) } catch (e: NoSuchMethodError) { Log.e(DependencyCheck, method missing, e) }我用这个方法抓到过一个非常隐蔽的版本错位问题类名完全一致方法参数也一致但实际加载到的类是另一个版本里的同名类方法签名对不上静态检查完全看不出来只能在运行时暴露。提示如果你同时引用了多个同名不同版本的 AAR这种错位问题几乎必然发生。解决的方法是统一版本、统一仓库并禁止 libs 目录里放同名多版本文件。7. 关键避坑清单这条值得收藏以下每一条都是实操中踩过的坑不是网上抄来的不要在libs目录下放同名不同版本的 AAR 文件Gradle 的文件收集顺序不稳定今天取 A明天取 B构建缓存还会掩盖问题。不要用compile files(...)这个配置在 AGP 7.0 之后行为变化很大api和implementation的边界务必分清。如果你用了exclude去排除传递依赖等发布 AAR 时记得复查 pom 文件里有没有残留在排除规则否则消费者会发现“依赖没拉下来”。用transitive false之后记得手动把所有间接依赖列出来一个都不能少。打包 AAR 前先跑一遍./gradlew :library:lint有时候资源重复、so 缺失这些问题 lint 会直接给你列出来省去运行时报错的搜索成本。别在 CI 里用增量构建去打包 AAR建议每次发布前强制 clean 一次。我见过太多次“本地能跑CI 打出来缺东西”的情况基本都和增量缓存有关。多模块发布时用 Gradle 的maven-publish插件统一发布手动 copy AAR 文件的方式很容易漏掉 pom 文件没有 pom 的 AAR 发布出去等于半残废。如果你的 AAR 依赖了第三方开源库发布前注意看下最终 pom 文件是否包含对应依赖坐标很多“SDK 接入后崩溃”的问题根因都在这里。8. 常见报错现象速查表报错类型典型现象根因方向优先排查手段ClassNotFoundException运行时崩溃找不到某个类依赖 AAR 未进入最终 APKapkanalyzer 检查 dex再查依赖报告NoClassDefFoundError静态编译能过运行时崩依赖在编译期可见运行期不可见检查 implementation 与 api 的使用检查 ProGuard keep 规则NoSuchMethodError运行时调用方法报错依赖版本冲突加载到旧版/新版方法差异统一依赖版本查看依赖报告中的冲突标记资源找不到Resources$NotFoundExceptionAAR 资源被裁剪或合并失败关闭 resource shrink 验证检查 res 目录合并同名字符串被覆盖UI 显示错乱或取值不对资源合并时 value 冲突lint 检查 DuplicateResources显式设置 res 合并策略so 加载失败UnsatisfiedLinkErrorABI 不匹配或 so 缺失unzip 检查 AAR 的 jni 目录对比设备 ABI这张表是我在自己项目实践里沉淀下来的基本覆盖了八成的“aar 引用打包问题”。遇到问题时先定现象再定方向不要一上来就改 Gradle 配置乱试。9. 实战排障记录一次“本地成功、外部失败”的完整复盘最后分享一次完整复盘。某次我们在模拟项目X里开发一个登录 SDK内部引用了三个基础 AARbiz-core、biz-common、biz-network。当时图省事直接在一个 library 模块的 libs 里全放进去然后用implementation fileTree(include: [*.aar])引。内部自测一点问题没有打包成 login-sdk.aar 发给合作方之后对方接入启动时秒崩。第一轮排查我们让合作方开了minifyEnabled false再试还是崩说明不是混淆问题。第二轮用 apkanalyzer 查对方 APK发现 dex 里根本没有 biz-core 的类。第三轮查我们发布的 login-sdk.aar用 unzip -l 看里面确实只有 login 自己的 classes.jar没有 biz-core这就是根因。当时我们不用“方案二转坐标”的原因是合作方处于离线内网环境没有仓库权限。所以最后选择了一个工程化的中间方案保留 libs 引入方式但在发布前加一条 Gradle 任务把三个基础 AAR 合并到 login-sdk 的 AAR 里。具体就是我在方案一里说过的解压、重编 classes.jar、合并资源。后来这套任务在 CI 上稳定跑了两个月没有再生过类似问题。个人体会是AAR 引用问题的本质不是“怎么写依赖”而是“构建产物包含完整运行时”。只要你的消费者拿到的产物不需要再做额外依赖决策问题就已经解决了一半。如果某个方案让你在发布后还要反复叮嘱对方“记得再引这几个包”那这个方案迟早会反噬。所以现在我在团队里立了一条规则发布的 AAR 必须自包含要么用仓库坐标传递依赖要么把所有东西物理打进包体禁止半推半就。