Android Studio打包AAR全攻略:原理、混淆与依赖处理
做Android开发这几年我给自己项目打过AAR也接过别人丢过来的千奇百怪的AAR但真正静下心把“Android Studio打包AAR”这件事从头到尾理清楚还是在我第一次把SDK交付给合作方之后。当时对方只收到一个.aar文件却连问了我十几个问题这个AAR为什么没有把第三方依赖带进去混淆规则怎么生效原来的包名怎么变了我才意识到很多人对AAR的理解停留在“把代码打成压缩包”这一步但真正的坑全在背后的构建细节里。这篇文章就围绕Android Studio打包AAR这件事展开从最基础的Library Module创建到assembleRelease命令的细节、AAR内部结构、混淆规则、依赖处理、常见报错排查全部按我实际跑过的流程来写。适合刚接触组件化或SDK封装的同学也适合已经打过几次AAR但经常在依赖、资源、so文件上翻车的朋友。1. 打包AAR到底是什么能解决什么问题1.1 一句话解释AARAAR是Android Library的归档文件全称是Android Archive。你可以把它理解成“专门为Android模块设计的压缩包”里面不仅包含编译后的代码还包含Android资源、清单文件、资源索引表、so库等一整套Android模块需要的东西。这和普通JAR有本质区别。JAR只能装Java/Kotlin编译后的class文件和若干资源文件但放不下res/、assets/、AndroidManifest.xml这些Android专属内容。AAR则像一个“半成品模块”你把它塞进某个App工程它就能像本地Module一样参与资源合并、清单合并、代码混淆最终成为App的一部分。我打个比方JAR像一袋速冻饺子馅你已经调好味字节码但还得自己准备皮Android资源、说明清单文件AAR则是一整盒速冻饺子皮、馅、包装说明都齐了App工程只需要做“下锅煮熟”这一步。1.2 什么时候需要打包AAR这个问题比怎么打包更重要。我见过不少团队内部好几个模块同步开发本来用implementation project(:module)直接引用Module就挺方便非要打AAR再丢进libs目录结果每次改动都要重新打包、传输、替换效率很低。AAR真正适合的场景是你要把SDK交付给外部团队但不能暴露源码公司内部有基础组件库需要同时提供给多个App使用又不希望各App直接改组件源码你要做一个插件化或动态化方案某个功能模块需要以二进制形式下发App工程结构必须解耦模块之间不能有源码级依赖。简单说AAR是“发布二进制产物”的方案而Project引用是“源码协作开发”的方案。二选一要看你的协作边界在哪如果只是自己项目内部拆模块直接引用Module往往更快。2. 环境准备和前置知识2.1 安装Android Studio并完成基础配置打包AAR本身不复杂但前提是Android Studio环境得靠谱。很多新手上来就问“为什么我的AAR打不出来”最后发现是SDK没装全或者Gradle JDK版本不对。我的建议是直接去官网下载最新稳定版Android Studio安装时把Android SDK、Android SDK Platform-Tools、Android SDK Build-Tools这些基础组件勾上。装完之后如果不习惯英文界面可以在Settings Plugins里搜索中文语言包安装重启后就变成中文菜单了后续按中文路径操作会顺手很多。SDK版本这块容易踩坑compileSdk、targetSdk和minSdk三个值要心里有数不要再无脑抄网上的配置。compileSdk决定你编译时能用哪些API打包出来的AAR被别的工程引用时对方工程的compileSdk不能低于你用的版本否则会报找不到API的错误。minSdk则是你AAR能运行的最低版本尽量不要定太高否则影响接入方的设备覆盖范围。2.2 AAR和JAR的本质区别前面提过AAR和JAR不是一回事这里再展开说几个关键差异对比项JARAAR包含代码有有classes.jar包含Android资源基本不支持支持res/assets包含AndroidManifest.xml不支持支持包含so库不直接支持支持jni/目录扩展名.jar.aar被App引用方式编译期依赖合并资源后参与编译打包从实际体验来看JAR更适合纯Java逻辑的通用库比如JSON解析、网络库只要你这个模块涉及Activity、Fragment、layout、drawable、so就必须用AAR才能把这些Android资源完整传递给调用方。2.3 理解Gradle构建产物和构建变体打包AAR之前你要对Gradle的构建体系有个基本认知。每个Module在Gradle里都有一套buildTypes如debug、release和productFlavors如免费版、付费版它们组合成多个“构建变体”Build Variant每个变体编译后都可以生成一个AAR。这就是为什么很多人会在build/outputs/aar目录下看到mylibrary-debug.aar和mylibrary-release.aar两个文件。它们对应不同构建变体的产物。默认情况下release会做代码优化和混淆的准备工作debug更适合本地调试。理解这一点后面用命令打包时就不会搞混。3. 从零创建一个Library Module并完成配置3.1 新建Module的完整步骤操作上很简单打开你的Android项目在菜单栏选择File New New Module...在弹窗里选Android Library输入模块名和包名点击Finish。创建好后Android Studio会自动在settings.gradle里加上一行include :mylibrary表示这个模块会参与项目构建。此时你在工程目录下会看到mylibrary文件夹里面有src目录和build.gradle、proguard-rules.pro或者新版模板里的consumer-rules.pro等文件。有一点要注意Library Module和应用Modulecom.android.application长得几乎一样但它没有applicationId也不能直接“运行”。它的产物就是一个AAR文件。如果你在Library里试图配置applicationId会直接报错因为只有应用模块才有应用ID的概念。3.2 build.gradle关键配置解读新建出来的Library模块build.gradle一般长这样plugins { id com.android.library } android { namespace com.example.mylibrary compileSdk 34 defaultConfig { minSdk 21 targetSdk 34 consumerProguardFiles consumer-rules.pro } buildTypes { release { consumerProguardFiles consumer-rules.pro } } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 }逐行理解一下这些配置直接影响AAR的效果namespace是新版AGP要求的包名命名空间相当于以前AndroidManifest里的package属性compileSdk和minSdk前面说过了不再重复consumerProguardFiles是重点——它指定一个混淆规则文件这个文件会被打进AAR当App引用这个AAR并开启混淆时这些规则会自动参与R8/ProGuard你不需要让调用方再手动配置混淆。dependencies这里要特别谨慎implementation声明的依赖默认不会传递到引用方。如果AAR内部用了某个SDK且这个SDK接口暴露在了AAR的对外API里调用方编译时往往会报“找不到类”。这种情况下应该用api声明让依赖跟着AAR的元数据传递出去。后面第5节我会专门讨论依赖传递和fat AAR的问题。3.3 代码、资源和so怎么组织Library模块的源码组织方式和普通App完全一样Java/Kotlin代码放src/main/java资源放src/main/resso库放在src/main/jniLibs目录并按照armeabi-v7a、arm64-v8a等ABI分文件夹assets则放src/main/assets。一个容易被忽略的点如果你在Library里用了R类注意它不能在switch-case里配合资源ID使用。因为在Library模块中R类不是final的所以打包成AAR后引用方的资源ID可能发生变化switch就编译不过了。这是Android官方老生常谈的坑我把它放在前面提醒你。4. 三种主流打包方式实操4.1 方式一通过界面菜单打包最省事的方式是直接点击Android Studio右侧的Gradle面板展开mylibrary Tasks build双击assembleRelease或assembleDebug任务跑完后AAR就生成在mylibrary/build/outputs/aar/目录下。如果你只想编译当前模块也可以在上方Build菜单里选Make Module mylibrary。这种方式会带着依赖模块一起编译输出产物里包含的就是当前模块的AAR文件。界面操作的优点是无脑、直观适合临时打个包看效果。缺点是不好复用尤其是你改了代码后忘记重新打包调了半天发现调用方用的还是旧AAR这种低级错误我犯过不止一次。所以打包最好养成“改代码以后Clean一下再打”的习惯。4.2 方式二Gradle命令行打包命令行打包是我在实际工作中用得最多的方式干净利落而且方便写进自动化脚本。打开Android Studio底部的Terminal或者在项目根目录打开命令行输入./gradlew :mylibrary:assembleRelease任务跑完产物在mylibrary/build/outputs/aar/mylibrary-release.aar。有几个命令行细节值得记住:mylibrary是Gradle中的任务路径:mylibrary:assembleRelease表示只构建这个模块assembleRelease会触发该模块依赖的编译、资源处理、清单合并等整条链路如果你想打Debug版把assembleRelease换成assembleDebug如果项目是多模块工程上面这个命令不会重新编译所有模块只会编译mylibrary及其依赖速度相对快。如果要保留多个版本的AAR我会在命令行里配合-P参数传递版本号比如./gradlew :mylibrary:assembleRelease -PversionName2.1.0然后在build.gradle里读取这个参数给输出AAR加上版本后缀。4.3 方式三自定义Gradle Task批量产出当你需要一次性打出多个变体、多个渠道的AAR时手动双击或单条命令就不够高效了。我习惯在Library的build.gradle里加一个自定义Task把清理、编译、复制、重命名一条龙做完。android { libraryVariants.all { variant - variant.outputs.all { output - output.outputFileName mylibrary-${variant.name}-${project.version}.aar } } } tasks.register(buildAllAars) { dependsOn clean, assembleRelease, assembleDebug doLast { copy { from build/outputs/aar into ${rootDir}/outputs } } }这样在命令行执行./gradlew buildAllAars就能清空旧产物、编译release和debug两个变体并统一复制到项目根目录的outputs文件夹里。实际项目中我还见过有人把这一步挂到CI流程里提交代码后自动出AAR包开发同事直接去下载效果非常好。4.4 多版本多渠道AAR怎么打如果AAR需要区分不同环境或不同付费策略可以通过productFlavors实现。android { flavorDimensions channel productFlavors { free { dimension channel } paid { dimension channel } } }配置完以后构建变体数量会翻倍比如freeRelease、paidRelease。命令行打包时对应任务名会带上Flavor名./gradlew :mylibrary:assembleFreeRelease ./gradlew :mylibrary:assemblePaidRelease产物会分别生成对应名称的AAR文件。如果你在Flavor里还配置了不同的BuildConfig字段比如API_BASE_URL那么每个AAR内部也会包含各自的值。这个功能很适合一个SDK同时为不同客户定制化构建的场景。5. AAR文件内部结构拆解与验证5.1 解压看AAR里面到底有什么我一直觉得做Android开发的人至少要亲手解压一次AAR才知道自己打的包到底包含了什么。生成AAR后把文件扩展名改成.zip或者直接用解压工具打开会看到类似下面这些内容AndroidManifest.xml二进制的清单文件声明了Library的权限、组件、元数据classes.jarJava/Kotlin代码编译后的字节码集合res/资源文件包括布局、图片、values等R.txt资源索引表记录每个资源的类型和名称方便App编译时统一索引assets/Assets目录如果存在jni/各ABI架构的so库如果存在proguard.txtconsumer混淆规则如果配置了consumerProguardFileslint.jar可能不存在Lint检查规则文件。看到这个结构后你就明白为什么AAR能承载完整的Android模块。再看“为什么JAR不能替代AAR”这个问题答案就很直观了你总不能把res文件夹单独丢给调用方吧。5.2 混淆规则如何内置并生效很多开发者第一次打AAR时都会困惑一个问题Library模块里要不要开minifyEnabled true我的建议是不要至少在绝大多数情况下不要。原因有两个Library混淆并不像App混淆那样能给你带来显著的包体收益因为Library最终会被并入App进行整体混淆如果Library提前做了混淆重命名它的对外API签名可能被打乱调用方一编译就是一堆“找不到符号”。正确做法是Library里关闭代码混淆但把调用方需要保留的规则写进consumer-rules.pro然后通过consumerProguardFiles打进AAR。当App开启混淆时R8会自动读取AAR里的混淆规则确保你对外暴露的类和方法不被重命名而内部实现仍然可以正常瘦身和混淆。我在实际项目里经常遇到调用方反馈“一混淆就崩”排查半天发现对方没有把我们提供的混淆规则加入proguard-rules.pro。其实只要我们把规则写进consumer-rules.pro这个问题根本不会出现。这就是AAR比JAR有优势的地方——规则随包走。5.3 第三方依赖怎么处理要不要打fat AAR这是AAR打包里牵扯最多的问题。直接结论是AAR默认不会把你用implementation引用的第三方依赖打包进AAR的classes.jar里。解析依赖发生在App构建时通过Gradle元数据和传递依赖机制完成。如果你把AAR文件丢给对方对方只是往libs目录一扔那么AAR里依赖的okhttp、retrofit这些库他是拿不到的编译必报“找不到类”。这个问题有几种解决路径写清楚依赖清单让调用方手动在build.gradle里添加同样的依赖如果你的AAR走的是Maven仓库implementation依赖会被记录在POM文件里通过Gradle能够自动传递前提是你对外API使用api而不是implementation如果对方只能拿文件、不能联网解析依赖那就得考虑把所有依赖打成一个“fat AAR”。所谓fat AAR就是通过脚本或插件把第三方库的代码和资源也合并进AAR里。社区里有现成的开源方案比如fat-aar-android也可以自己写Gradle Task去合并。但我个人不太推荐一上来就搞fat AAR它会让AAR体积暴涨还容易引发资源冲突和依赖重复。更优雅的方式是走Maven仓库用Gradle的传递依赖能力让调用方自动拉取所需依赖。如果条件实在不允许才考虑fat AAR。6. 在项目里正确引入AAR并处理冲突6.1 本地libs目录的两种引入方式拿到AAR后最常见的集成方式是把文件放到App工程的app/libs目录下。然后在build.gradle里写dependencies { implementation fileTree(dir: libs, include: [*.aar]) }这样会把libs目录下所有AAR都加进来。如果你只想引某一个也可以指定具体文件dependencies { implementation files(libs/mylibrary-release.aar) }前者适合AAR文件较多、依赖关系复杂的场景后者适合明确指定版本、防止误引入的场景。我个人的习惯是尽量指名道姓毕竟libs目录里如果堆了一堆旧版本AARfileTree会全部引入老版本类和新版本类冲突起来非常头疼。6.2 模块依赖与Maven仓库方式除了本地文件引入还有两种更“工程化”的方式。第一种是模块依赖。在同一个工程里在settings.gradle已经include :mylibrary的前提下App的build.gradle写dependencies { implementation project(:mylibrary) }这种方式适合开发联调阶段代码改了立即生效不用反复打包。等代码稳定后再切换成AAR引用。第二种是发布到Maven仓库通过maven-publish插件把自己的AAR推送到内部仓库然后App端用坐标引用。这种方式对多项目、多团队协作非常友好依赖传递也能正常生效。如果你所在团队规模不大至少可以先把AAR传到自建仓库或私有仓库用坐标管理版本比在群里丢AAR文件体面一百倍。6.3 清单合并与资源冲突处理AAR中的AndroidManifest.xml会参与App的清单合并。当AAR里声明了权限、Activity、Service等信息时App最终打包会把这些内容合并进去。如果两个库声明了同名组件或冲突的配置就需要在App的AndroidManifest.xml里使用tools:node或tools:replace来指定合并策略。比如activity android:name.MainActivity android:themestyle/AppTheme tools:replaceandroid:theme /资源冲突则是另一个高频问题。两个AAR里如果都有app_loading_bg这样的通用资源名App合并资源时会随机选一个编译时不报错运行时显示就可能出错。为了避免这个问题最好在Library的build.gradle里加上资源前缀android { resourcePrefix mylib_ }加了之后Library里的资源命名必须用mylib_开头否则构建会直接报错提醒你。强制统一前缀能很大程度避免资源名冲突尤其是在交付给第三方团队的SDK里这个习惯几乎是必须的。7. 常见问题与排查实录7.1 AAR生成后找不到文件这个问题通常在两种情况下出现一是根本没有执行过打包任务二是打到了别的变体下。排查的时候先确认任务是否执行成功然后去mylibrary/build/outputs/aar/目录看一眼。如果你执行的是assembleDebug那就不会出现mylibrary-release.aar只有mylibrary-debug.aar。还有一个细节如果你改了outputFileName或加了Flavor文件名会变成其他格式不要拿着旧名字去找。另一个容易忽略的原因是Android Studio还在同步或构建中文件还没生成完整。等Gradle面板右上角不再转圈了再去目录里找否则可能只看到一个0KB的空文件。7.2 打包后so文件丢失AAR里有so文件却打包不到App里这是我在做SDK封装时翻车次数最多的问题。先检查so是否放在src/main/jniLibs/{abi}目录下。如果你的Library是通过依赖方式引用某个so库打包时要注意该依赖并没有被传递给AppApp端肯定找不到对应so。此时要么把so复制到本模块的jniLibs里要么通过api依赖让so随着传递依赖暴露出来。还有一点App最终打包时如果本身没有配置ndk { abiFilters }默认会打进所有ABI的so但如果App配置了只保留某几种ABI而你的AAR恰好没有这些ABI的so运行到对应机型上就会崩报dlopen failed或者so not found。这里需要在App和AAR的ABI设置上保持一致避免踩空。7.3 资源和类名冲突前面已经说过资源前缀的重要性这里再补充一个常见的类名冲突场景两个AAR内部引用了同一个SDK的旧版本App构建时Gradle会按依赖解析规则保留较高版本但如果兼容性不好运行时会直接报NoSuchMethodError或ClassNotFoundException。定位思路是用./gradlew :app:dependencies查看依赖树找出冲突源头然后在App的build.gradle里通过exclude或force统一版本。类名冲突比资源冲突更隐蔽因为很多日志看半天都看不出是哪个库在捣鬼。养成打依赖树的习惯比瞎猜效率高很多。7.4 编译报错找不到符号或R类不是final“找不到符号”这个报错很经典一般有四个来源使用implementation导致依赖没传递、Library提前混淆把对外API搞乱了、调用方compileSdk过低、或者AAR里代码引用的类确实没被打入classes.jar。还有一种情况特别坑在Library代码的switch-case里用了R.id.xxx或R.string.xxxAndroid Studio在Library模块里不会把它标成错误但App编译时就会报“常量表达式”相关的错误。这是因为Library的R类不是final无法写在case里。解决办法就是改成if-else判断。7.5 常见问题速查表现象常见原因处理建议找不到AAR产物没执行打包任务/构建变体不对检查任务名和build/outputs/aar路径引用AAR后找不到第三方类依赖未传递改用api声明依赖或走Maven仓库运行崩溃且so找不到so未打入或ABI不匹配检查jniLibs目录和abiFilters配置资源显示错乱资源名冲突设置resourcePrefix统一前缀混淆后API方法缺失混淆规则未传递使用consumerProguardFiles内置规则R类使用报错Library中R不是final改用if-else判断资源ID说句掏心窝的话AAR打包本身不难难的是把交付边界划清楚。我自己踩过不少坑之后现在每次打AAR都会在包里附一份简单的说明写明AAR版本、依赖坐标、minSdk要求、混淆规则文件名。这些东西看着琐碎却能省掉大量来回沟通的时间。如果你后续要把AAR大规模推广给多个团队使用我强烈建议至少搭建一个内部仓库用版本号管理AAR而不是反复传文件。哪怕团队只有三五个人也比每天手动替换文件靠谱得多。最后再分享一个小技巧给AAR命名时把版本号写进文件名比如mylibrary-2.1.0-release.aar千万别用new_aar_v2最终版.aar这种命名否则一个月后你自己都会分不清谁是谁。