Android 高效打包:使用fat-aar-android合并第三方aar实战指南
1. 为什么我们需要合并第三方AAR?
在Android开发中,把一些通用功能模块打包成AAR文件,就像把常用的工具装进一个工具箱,方便在不同的项目里直接拿来用,这能极大地提升开发效率。但现实往往比理想骨感,当你兴冲冲地想把一个集成了多个第三方库的模块打包成AAR时,Gradle会毫不留情地给你泼一盆冷水,报出一个让你头疼的错误。
这个错误信息通常长这样:
Direct local .aar file dependencies are not supported when building an AAR.
翻译过来就是:在构建AAR时,不支持直接的本地.aar文件依赖。Gradle会告诉你,如果你强行打包,生成的AAR会是“坏的”,因为它不会包含那些本地AAR依赖里的类和资源。
我刚开始遇到这个问题时也是一头雾水。我的模块明明在项目里运行得好好的,为什么一打包就出问题?后来才明白,这其实是Android Gradle插件(AGP)的一个设计限制。当你把一个模块作为库(Library)打包时,它默认只会打包自己编写的源码和资源,对于通过implementation files('libs/xxx.aar')这种方式引入的本地AAR文件,它只会记录一个依赖关系,而不会把它们的“实体”(即class和res文件)塞进最终的AAR包里。
这就导致了一个尴尬的局面:你把辛苦打包好的AAR交给别人用,对方除了依赖你的AAR,还必须把你用到的所有第三方AAR也一个个手动引入项目,否则就会因为找不到类而崩溃。这完全违背了我们封装模块、简化集成的初衷。想象一下,你给同事一个“万能螺丝刀”,结果他还要自己准备一堆批头才能用,这体验实在太差了。
那么,有没有办法让我们的“工具箱”真正变得完整、开箱即用呢?答案是肯定的,这就需要用到我们今天的主角——fat-aar-android。它的核心思想很直接:既然Gradle不帮我们打包依赖,那我们就自己动手,在构建过程中,把那些第三方AAR(无论是本地的还是远程的)全部“解包”,然后把它们的代码、资源、清单文件等,像和面一样,揉进我们自己的AAR里。最终产出的,就是一个“胖胖的”(fat)、自包含的AAR文件,用户只需要依赖这一个文件就够了。
2. 从老旧的fat-aar迁移到fat-aar-android
在寻找解决方案的路上,你可能会先遇到一个叫 fat-aar 的项目(GitHub地址:adwiv/android-fat-aar)。我最早用的就是它,但说实话,踩坑的经历让我记忆犹新。这个项目已经很久没有维护了,它适配的Gradle和Android Gradle插件版本都比较老。如果你用的是Android Studio的新版本和对应的新AGP(比如4.0以上),你会发现按照它的文档配置,要么编译不过,要么打出来的包有问题。
最让人头疼的是,它需要你手动修改项目里好几个Gradle脚本文件,插入一些特定的Task和配置。这个过程不仅繁琐,而且一旦项目结构或Gradle版本有变动,很容易就失效了,维护成本非常高。我当时就被它折腾得够呛,直到发现了更好的替代品——fat-aar-android(GitHub地址:kezong/fat-aar-android)。
fat-aar-android 可以看作是 fat-aar 的“精神续作”和全面升级版。它的最大优点就是对高版本Gradle和AGP的良好支持,目前已经支持AGP 3.0到7.1,以及Gradle 4.9到7.3,覆盖了绝大多数现代Android项目的开发环境。更重要的是,它的使用方式非常“优雅”,你不需要去动那些复杂的构建脚本,只需要像添加普通Gradle插件一样进行配置,然后把依赖关键字从implementation换成embed就行了,几乎是无感接入。
所以,如果你正在被AAR嵌套依赖问题困扰,或者还在使用老旧的fat-aar苦苦挣扎,我强烈建议你直接转向 fat-aar-android。接下来,我就带你一步步完成从零开始的配置和实战。
3. 手把手配置fat-aar-android
理论说再多,不如动手做一遍。让我们在一个干净的Android项目里,实际配置一下fat-aar-android。假设我们有一个名为 mylibrary 的模块,它需要打包成AAR,并且内部依赖了一个本地的 awesome-ui.aar 文件和一个远程的 okhttp 网络库。
3.1 第一步:在项目根目录添加插件依赖
首先,打开你项目根目录下的 build.gradle 文件(注意是Project级别的那个,不是Module的)。在 buildscript 部分的 dependencies 块里,添加 fat-aar-android 的插件依赖。
buildscript {
repositories {
mavenCentral() // 确保有mavenCentral仓库
google() // 通常也需要google仓库
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.1' // 你的AGP版本
// 添加fat-aar-android插件
classpath 'com.github.kezong:fat-aar:1.3.8'
}
}
这里我用的插件版本是 1.3.8,在撰写本文时这是最新的稳定版本。你可以在 fat-aar-android的GitHub发布页面 查看是否有更新的版本。添加完成后,记得点击Android Studio右上角的 Sync Now 同步一下项目。
3.2 第二步:在模块中应用插件
接下来,打开你需要打包的模块(比如 mylibrary)下的 build.gradle 文件。在文件的最顶部,应用这个插件。
// mylibrary/build.gradle
apply plugin: 'com.android.library'
apply plugin: 'com.kezong.fat-aar' // 应用fat-aar插件
注意,这个插件是应用在 com.android.library 插件之后的,因为它的作用是增强库模块的打包能力。
3.3 第三步:用embed声明需要合并的依赖
这是最关键的一步。在你模块的 dependencies 块里,所有你希望合并到最终AAR中的依赖,无论是本地的AAR、远程的库,还是项目内的其他子模块,都需要使用 embed 关键字来代替 implementation 或 api。
dependencies {
// 1. 嵌入本地libs目录下的aar文件
// 假设 awesome-ui.aar 放在 mylibrary/libs/ 目录下
embed(name: 'awesome-ui', ext: 'aar')
// 同时,为了兼容性,通常需要添加这行,防止其他配置找不到aar
compileOnly fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
// 2. 嵌入远程仓库的依赖(如Maven Central上的)
embed 'com.squareup.okhttp3:okhttp:4.11.0'
// 3. 嵌入项目内的另一个模块(假设有个叫 `core` 的模块)
embed project(':core')
// 4. 对于不希望合并,只想在编译时使用的依赖,继续用implementation
implementation 'androidx.appcompat:appcompat:1.4.1'
// 比如AppCompat,通常宿主App都会提供,我们就不需要打包进去,避免冲突
}
我来解释一下这几行配置:
- 第一行是嵌入本地AAR的标准写法。
name指定文件名(不带后缀),ext指定文件扩展名。这行告诉插件:“去libs目录下找到awesome-ui.aar,把它解开并合并进来。” - 第二行
compileOnly是一个小技巧。因为embed指令可能在某些构建环节不被识别为标准的文件依赖,加上这行可以确保IDE和Gradle的其他部分能正常找到这个AAR文件,避免编译错误。 - 第三行和第四行展示了如何嵌入远程库和项目模块,语法和
implementation一模一样,只是换了关键字。 - 最后,对于像
androidx.appcompat这种几乎每个App都会自带的基础库,我们选择用implementation。这样它不会被打进AAR,最终用户的项目在依赖你的AAR时,需要自己提供这个库(或者由宿主App提供),这可以避免库的重复和冲突。
配置完成后,再次点击 Sync Now。如果一切顺利,你的项目就应该可以正常编译了。
4. 执行打包与查看结果
配置好之后,打包AAR的过程和平时没有任何区别。你有几种方式可以触发打包:
- 使用Android Studio的Gradle面板:在右侧的Gradle工具窗口中,找到你的模块(例如
:mylibrary) ->Tasks->build,然后双击assemble或assembleRelease。assemble会同时生成Debug和Release版本的AAR,而assembleRelease只生成Release版本。 - 使用命令行:在项目根目录打开终端,执行命令:
(在Windows上是./gradlew :mylibrary:assembleReleasegradlew.bat :mylibrary:assembleRelease)
打包过程会在Gradle的构建输出中打印详细的日志。如果遇到错误,请仔细阅读日志信息,最常见的问题是依赖冲突或配置错误。
打包成功后,生成的AAR文件在哪里呢?它位于你模块的 build/outputs/aar/ 目录下。例如,mylibrary 模块的Release版AAR路径通常是 mylibrary/build/outputs/aar/mylibrary-release.aar。
如何验证合并是否成功? 最直接的方法就是“拆开看看”。AAR文件本质上是一个ZIP压缩包。你可以把后缀名从 .aar 改为 .zip,然后用解压软件打开(或者直接用命令行 unzip)。重点查看里面的 classes.jar(可以用JD-GUI等工具反编译查看类)和 res 目录。如果成功,你应该能在 classes.jar 里找到你嵌入的第三方库的类(比如OkHttp的类),在 res 目录下看到第三方库的资源文件。
5. 进阶配置与疑难杂症处理
基本的配置能解决大部分问题,但在实际企业级项目中,情况往往更复杂。下面我分享几个进阶场景和对应的解决方案,这些都是我在实际项目中踩过坑后总结出来的。
5.1 处理多级依赖传递
默认情况下,embed 只合并你直接声明的第一级依赖。但很多时候,你嵌入的AAR(比如 awesome-ui.aar)它自己可能还依赖了另一个 common-utils.aar。如果你希望把这些间接依赖也一并打包进来,就需要开启传递依赖功能。
在你的库模块的 build.gradle 文件中,添加一个 fataar 配置块:
fataar {
transitive = true
}
将 transitive 设置为 true 后,插件在打包时就会递归地把你嵌入的依赖所声明的依赖(在pom文件中)也下载并合并进来。这非常适合制作一个完全独立、不依赖任何外部环境的“全家桶”SDK。
但开启传递性也可能带来问题,比如依赖冲突。如果两个被嵌入的库都依赖了不同版本的Gson,就会打架。这时,你可以使用 exclude 来排除特定的传递性依赖:
embed('com.some.library:core:1.0.0') {
transitive = true // 为这个库开启传递性
exclude group: 'com.google.code.gson', module: 'gson' // 排除它带来的gson
// 或者直接关闭这个库的传递性
// transitive = false
}
5.2 过滤不必要的ABI架构以减少包体积
如果你嵌入的本地AAR或远程库包含了原生代码(.so文件),那么默认情况下,打出来的胖AAR会包含所有支持的CPU架构(如 armeabi-v7a, arm64-v8a, x86, x86_64)。这会让你的AAR文件变得非常臃肿。
通常,我们只需要保留 arm64-v8a(主流新设备)和 armeabi-v7a(兼容旧设备)即可。但是,直接在模块的 android -> defaultConfig 里配置 ndk { abiFilters ... } 可能对合并进来的 .so 文件无效。
一个有效的方法是,在打包完成后,手动从AAR中删除不需要的架构。这可以通过在模块的 build.gradle 中注册一个自定义的Gradle任务来实现,这个任务在 assemble 任务之后执行,解压AAR,删除特定目录,再重新打包。不过更简单的做法是,利用 fat-aar-android 插件对资源合并的控制,在嵌入依赖时就直接排除。
目前插件没有直接提供ABI过滤参数。一个实践中的变通方法是,如果你能控制第三方AAR的来源,可以尝试只提供包含所需ABI的版本。如果不行,那就只能接受较大的包体积,或者像前面提到的,在最终生成AAR后,通过脚本进行二次处理。
5.3 解决资源冲突问题
资源冲突是合并AAR时另一个高频问题。比如,你的模块里有一个 R.string.app_name,你嵌入的第三方AAR里也有一个同名的 R.string.app_name,在合并时Gradle就会报 duplication resources 错误。
解决方案有两种:
- 预防为主(推荐):为你模块内的所有资源名称添加统一的前缀。例如,你的公司域名是
com.mycompany,那么可以把所有字符串、颜色、布局等资源名都加上my_前缀,比如my_app_name,my_primary_color。这需要建立团队规范。 - 编译时忽略:如果冲突的第三方库无法修改,可以在项目的
gradle.properties文件中添加一行:
这会让Android构建系统忽略资源重复的错误。但要注意,这有风险!最终打包进APK的资源是“不确定”的,取决于构建顺序,可能会使用第一个找到的资源,导致UI显示异常。这种方法只能作为临时解决方案。android.disableResourceValidation=true
5.4 处理特殊的类丢失问题
在少数情况下,你可能会遇到一个诡异的问题:合并后的AAR,里面缺失了某个特定的类(比如一个名为 C 的类),而其他类都正常。就像我们参考内容里提到的那个Issue。
这种情况通常不是插件的普遍bug,而是由一些极端情况导致的,例如类名冲突(大小写敏感问题)、混淆规则的影响,或者是特定第三方SDK的特殊打包方式。解决思路如下:
- 首先,确认原版第三方AAR里这个类是否存在。用解压和反编译工具仔细检查。
- 其次,检查你的混淆规则(
proguard-rules.pro)。是否有可能误删了这个类?可以尝试暂时关闭混淆,看类是否还在。 - 最后,如果问题依然存在,可以去 fat-aar-android 的GitHub Issues页面搜索相关关键词,看看是否有其他人遇到并解决了类似问题。如果找不到,可以按照Issue模板详细描述你的环境(Gradle版本、AGP版本、插件版本、复现步骤),向社区求助。
6. 最佳实践与重要提醒
经过多个项目的实践,我总结出一些使用 fat-aar-android 的最佳实践,能帮你避开很多坑:
- 区分开发与发布依赖:在开发调试阶段,对项目内的子模块依赖使用
implementation project(':core'),这样修改core模块代码能实时热更新。在准备发布AAR时,再临时将这句改为embed project(':core')进行打包。可以尝试通过Gradle的buildType或flavor来管理这两套不同的依赖配置。 - 谨慎选择嵌入的依赖:不要把所有
implementation都改成embed。像androidx.*,com.google.android.material这些基础UI库,几乎100%的宿主App都会自带。把它们打进去只会徒增体积并引发潜在的版本冲突。只嵌入那些你的SDK独有、且宿主App不太可能提供的库。 - 处理好R文件:合并后,所有被嵌入库的R类常量(如
R.layout.xxx)会被合并到主模块的R类中。这意味着在你的SDK代码里,引用第三方库资源时,应该使用你主模块的R文件,而不是原来第三方库的R文件。插件会自动处理这部分重定向,但你在写代码时要有这个意识。 - 注意ProGuard混淆:如果你开启了代码混淆,那么被
embed进来的库的代码也会被一起混淆。你需要确保你的混淆规则能正确处理好这些库,避免混淆掉必要的类或方法,导致运行时崩溃。通常需要把第三方库的公共API接口保持(keep)住。 - 版本管理:将
fat-aar-android插件的版本号在根build.gradle中定义为变量,方便统一升级。同时,密切关注其GitHub仓库的更新,及时获取bug修复和新功能。
最后我想说,fat-aar-android 是一个非常强大且实用的工具,它完美地解决了Android库开发中的一个痛点。虽然它在处理一些极端复杂场景时可能需要额外的工作,但就其带来的便利性而言,这些投入是绝对值得的。希望这篇指南能帮助你顺利地将它应用到你的项目中,打包出干净、完整、用户友好的AAR文件。如果在实践中遇到新的问题,多查查官方文档和社区讨论,大部分难题都能找到答案。
更多推荐

所有评论(0)