ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Android Studio打包AAR全流程指南:从JAR到AAR的工程实践与避坑手册

Android Studio打包AAR全流程指南:从JAR到AAR的工程实践与避坑手册 上个月帮另一个业务组做联调对方只丢过来一句话“把支付模块打个包发我。”源码不能给纯JAR带不了res资源和ManifestAndroid SDK的jar包形式根本撑不起一个完整模块最后用的就是AAR。这件事操作起来并不难Android Studio里面点几下就能出产物但真正把AAR用好、让同事一行依赖直接接进工程中间其实藏了不少门道。依赖没打进去、混淆规则没跟上、AGP版本不一致导致构建失败这些坑我全踩过。这篇文章就把Android Studio打包AAR这件事从头到尾拆开讲清楚AAR和JAR到底差在哪、怎么从新建Module一路打出产物、AAR文件内部什么结构、打包过程中的高频坑以及拿到AAR之后别人该怎么集成。1. 为什么是AAR而不是JAR先搞清楚格式差异1.1 JAR与AAR的本质区别很多新手对AAR的第一反应是“不就是一个zip压缩包吗和JAR有什么区别”从打包产物来看AAR确实是一个zip格式的文件扩展名从.jar变成了.aar但两者承载的内容完全不同。JARJava Archive本质上就是一堆.class文件的集合它不关心Android资源、不携带Manifest、不包含so库的ABI目录结构。如果你的模块是纯Java逻辑、不涉及任何Android API那JAR是够用的比如一些算法库、网络协议解析库、纯工具类库。可一旦模块里用到了layout布局、drawable资源、AndroidManifest声明的Activity或权限JAR就无能为力了。AARAndroid Archive是专门为Android Library设计的打包格式它同样基于zip但内部结构为Android生态做了完整适配。一个标准AAR里至少包含编译后的classes.jar、res资源目录、AndroidManifest.xml、R.txt索引、proguard混淆规则还可以携带so库、assets资源。这意味着你交付的不再是“孤零零的代码”而是一个“可以被Android工程直接当模块使用的完整单元”。我把两者核心差别整理成了表格对比会更直观对比维度JARAAR文件本质class字节码集合zip归档内部含class资源ManifestAndroid资源不支持支持res/assetsAndroidManifest无有可参与清单合并so库不支持支持jni/目录混淆规则无可内置proguard.txtR资源ID无附带R.txt统一管理使用场景纯Java逻辑Android模块化、SDK交付1.2 哪些场景应该用AAR我实际使用AAR最多的场景有三类。第一类是模块化架构下的二方库交付。团队A维护一个基础组件团队B的业务工程要接两边不想共享源码仓库。直接把module目录发给对方对方还得调整自己的Gradle配置、处理依赖冲突费时费力。打成AAR发过去丢到libs目录或者走内部Maven对方一行依赖搞定。第二类是SDK封装。给第三方App提供登录、支付、推送能力肯定不能把源码交出去AAR配合混淆规则是最稳妥的交付形态。第三类是本地预编译加速。同一个App里几个模块之间耦合很深但变更频率低我一开始踩过的坑就是直接把AAR的classes.jar抽出来用结果运行时疯狂报资源找不到后来才彻底理解了AAR的边界在哪里。1.3 AAR的边界哪些东西不会被打进去AAR并不是万能的。它最让人误解的一点是AAR不会把你依赖的第三方库也打进去。比如你的library用了OkHttp你在build.gradle里写了implementation com.squareup.okhttp3:okhttp:4.9.0打出来的AAR里不会包含OkHttp的class也不会有OkHttp的代码。AAR只会记录一个依赖声明真正消费这个AAR的工程需要自己声明OkHttp依赖。这一点从设计上是合理的避免同一个库被重复打进多个AAR导致APK体积膨胀。但从打包者的角度必须心里有数交付AAR的时候必须把依赖清单同步给使用方否则对方运行起来直接NoClassDefFoundError。另外AAR的代码在打包阶段不会被预先混淆。library模块里的minifyEnabled默认是false即使你开了release类型的混淆AGP在构建AAR时也不会把混淆后的class放进去。混淆是“使用方App打包”时统一执行的。你的library如果提供的是SDK给外部用就要靠consumerProguardFiles把规则传递给下游。2. 从零到一用Android Studio打出第一个AAR2.1 创建Library Module两种方式Android Studio里创建Library模块有两条路。一条是图形化操作File - New - New Module在弹出的窗口里选择Android Library填好Module名一般叫xxlibrary或者xx_sdk之类Finish之后会生成一个包含build.gradle、AndroidManifest.xml、空Activity等文件的标准模块。这个模块在Project视图下会显示一个安卓书本样式的图标和App模块区分开。另一条是改造已有模块如果你手头有一个App module想把它变成库直接打开它的build.gradle把第一行插件声明从com.android.application改成com.android.library。// 改造前 plugins { id com.android.application } // 改造后 plugins { id com.android.library }改完之后需要同步一下然后注意一个关键差异Library模块没有applicationId因为你不再是一个独立的App不需要包名去申请安装。同时Library模块的AndroidManifest.xml里也不能再声明launcher入口Activity否则构建时会报错或者被主工程直接忽略。2.2 关键Gradle配置namespace、compileSdk和输出名创建完模块后build.gradle里的配置是决定打包成败的核心。我一般会先检查三个点。第一是namespace。AGP 7.0之后namespace必须显式声明它代表这个库的包名决定了R类和BuildConfig的生成路径。AGP 8.0之后如果漏了namespace构建直接报错而且它不能从manifest里自动读取。比如我写库时namespace声明为com.example.paysdk那后续代码里引用R类就要用com.example.paysdk.R。第二是compileSdk和minSdk。这里有一个很容易被忽略的细节AAR里虽然不会记录你用的compileSdk版本号但AGP会在AAR的META-INF里写入一份metadata标记。消费方App的compileSdk如果不低于你这个值会有兼容性问题如果低于构建直接报“requires libraries and applications that depend on it to compile against version XX or later of the Android APIs”。所以我给外部团队交付库时会主动问一句对方的compileSdk是多少避免对方接进去就炸。第三是输出文件名。默认打出来的AAR叫module名-release.aar比如mylibrary-release.aar。如果你想自定义版本号可以在android闭包里直接配archivesName这个写法在AGP 7.0到8.x都能用android { namespace com.example.mylibrary compileSdk 34 defaultConfig { minSdk 21 targetSdk 34 consumerProguardFiles consumer-rules.pro } archivesName mylib-1.0.0 }这样打出来的AAR就是mylib-1.0.0-release.aar版本信息一目了然。老版本的写法libraryVariants.all调整outputFileName同样有效但新项目我建议直接用archivesName代码更精简。2.3 执行打包任务界面点选和命令行两条路配置写完sync成功接下来就是打包。最快的方式是打开Android Studio右侧的Gradle面板找到你的library模块展开Tasks - build双击assembleRelease或者assembleDebug。assembleDebug适合本地自测assembleRelease是正式交付产物。构建完成后AAR文件输出在模块目录/build/outputs/aar/下面。命令行方式更适合写脚本、走CI。在项目根目录执行./gradlew :mylibrary:assembleRelease如果你在开发机上还没有配环境变量也可以用Android Studio自带的Terminal窗口跑。执行完之后用ls看一眼产物目录ls -la mylibrary/build/outputs/aar/会看到两个文件mylib-1.0.0-release.aar和mylib-1.0.0-release-sources.jar。后者是源码包默认配置下打AAR时AGP会顺手帮你把Java/Kotlin源码打成一个sources.jar方便IDE在集成时跳转到源码。如果不想生成它可以在android闭包里把这个任务排除掉不过日常交付我反而建议留着对使用方体验好很多。打debug版本和release版本有一个细微差别如果你在defaultConfig里没有额外配置BuildTyperelease版AAR其实也没有做代码优化它和debug版最主要的差异是buildConfig里的DEBUG字段值不同以及是否可调试。很多人在排查问题时会反复确认自己导出的是release包还是debug包这个习惯其实挺重要调试时要让使用方带上debug的AAR日志和断点信息才完整。3. 用解压工具看AAR内部必须知道的结构3.1 解开AAR看看里面到底装了什么AAR本质是zip拿到手之后第一件事用解压工具或者命令行解开它unzip mylib-1.0.0-release.aar -d aar_contents解开之后你大概会看到下面这些内容根据模块不同略有差异AndroidManifest.xml classes.jar res/ R.txt proguard.txt jni/ assets/ lint.jar META-INF/逐个说一下它们的作用。AndroidManifest.xml是library自己的清单文件它已经被编译成了二进制的AXML格式直接当文本打开会乱码但在AAR集成时主工程会读取它并做清单合并。classes.jar就是你的代码javac或kotlinc编译后的class字节码被打包在这个jar里命名叫classes.jar和APK内部的classes.dex不同它还没有经过D8转化为dex格式。res目录存放你的资源文件layout、drawable、values这些按原始目录结构保留。R.txt是一个文本索引里面列出了所有资源的类型、名称和ID值主工程编译时会基于它生成自己的R类引用。proguard.txt是我前面说的consumerProguardFiles指定的混淆规则文件AAR里会原样打包。jni目录只在包含so库时出现里面按ABI子目录arm64-v8a、armeabi-v7a等存放动态库。assets和lint.jar是可选内容assets在最终打包时会合并进主工程的assetslint.jar是AGP生成的对library做静态检查用的Lint规则。3.2 classes.jar不是给Android直接用的很多人第一次看到AAR里的classes.jar会误以为可以直接解压出来塞进App的libs目录用。这个操作我试过项目启动直接抛异常原因在于classes.jar里的字节码还停留在“模块编译”阶段真正进入APK之前主工程构建链会把AAR解包把classes.jar里的class交给D8/R8进行dex化同时把R.txt、res、AndroidManifest和主工程的内容合并。所以AAR不是简单解压即用的仓库它是一个“中间件”必须放进Gradle构建流程里被消费。这个机制也解释了为什么在Android Studio里直接“双击打开AAR文件”只能看到静态结构不能直接运行。它能被主工程识别的唯一途径就是通过Gradle把它注册为依赖。3.3 资源冲突与清单合并AAR隐藏的两个机制当主工程依赖了多个AAR和模块时所有library的资源和清单会在构建期被合并到一起。合并的过程就会带来两个经典问题。第一个是资源名冲突。我的library里如果定义了一个string叫app_name主工程或者另一个library里也定义了一个同名的string资源合并时轻则报错重则安静地覆盖运行时页面数据显示成别人的文案。约定俗成的做法是给library的所有资源加前缀比如paysdk_btn_login、paysdk_color_primary这样能极大降低冲突概率。这个习惯最好在模块创建第一天就建立等资源写多了再改前缀是一件非常痛苦的事。第二个是Manifest合并。主工程会把自己的AndroidManifest.xml和所有依赖library的清单合并。如果library里声明了一个 它会被自动合并到最终App的清单里如果library声明了权限同样会被合并。但多个模块对同一个属性有不同声明时就需要在library的清单节点上用tools:replace标记告诉合并器“这个属性以我为准”。实际操作中我遇到最多的场景是theme和label尤其是SDK库要声明一个穿透主题给Activity用的时候不加tools:replace经常会撞车。3.4 依赖不会自动打进AAR前面说的关键认知再强调一遍AAR内部结构里没有“dependencies”目录就是因为它压根不打包第三方依赖。library在build.gradle里写的那堆implementation依赖只会被记录到Maven的POM文件如果发布到Maven或者根本不记录本地文件传阅。使用方集成的时候如果缺了某个依赖报错信息不会来自AAR本身而是运行时崩溃。所以我在交付AAR时一定会附带一个说明写上完整的三方依赖清单gson:2.10.1 okhttp:4.10.0 rxjava:2.2.21或者更正规一点直接把依赖写入POM使用方通过仓库方式依赖时就能自动拉取。走本地AAR文件喂给对方的方式这个步骤绝对不能省对方在Android Studio里如果能看到NoClassDefFoundError或者ClassNotFoundException第一个排查方向就是去匹配依赖清单。4. 打包过程中最常见的坑和对策4.1 打出来的AAR没有把依赖带进去对方一集成就崩这个坑我在第3.4节里已经埋了伏笔。实际表现是我这边一切正常AAR也成功导出了对方把AAR塞进工程编译过了一运行就抛NoClassDefFoundError定位到是我library里的工具类引用了第三方库。我当时的第一个反应是“这AAR是不是没打全”然后把AAR解压翻了个底朝天确实没找到第三方库的class。后来查文档才确认这是AAR的设计规则不是构建异常。对策交付时把依赖清单写在README里或者用maven-publish发布到内部仓库让Gradle自行解析依赖。最常见的内部仓库方案是在build.gradle里配置publishingpom.withXml节点手动补全依赖使用方通过maven { url xxx }引用时依赖会自动传递。4.2 混淆规则写错了地方发布出去后使用方开启混淆就出事library模块一旦只配置了buildTypes.release.proguardFiles打出AAR后使用方即使开启了minifyEnabled也只会去找library的proguard.txt回忆一下第3.1节的AAR结构根目录下那个proguard.txt是通过consumerProguardFiles配置进来的。如果这个文件是空的那library里所有类在App混淆阶段可能被重命名导致主工程反射调用崩溃。对策defaultConfig { consumerProguardFiles consumer-rules.pro }consumer-rules.pro里写清楚哪些类需要keep。尤其是对外暴露的API入口、被反射调用的类、Java/Kotlin混编时的注解类一网打尽。4.3 AGP、Gradle、JDK版本之间的三角关系AAR构建失败最鬼畜的一类问题是代码没写错配置看着也正常但assembleRelease任务一跑就报一堆GradleAPI错误。这通常是AGP版本和Gradle版本、JDK版本不匹配。我吃过一次大亏项目切了AGP 8.2后忘了升级JDK构建直接抛UnsupportedClassVersionError。当前比较稳妥的版本匹配关系如下AGP版本最低Gradle版本推荐JDK版本7.4.x7.5JDK 118.0.x8.0JDK 178.2.x8.2JDK 178.7.x8.9JDK 17Android Studio里配置JDK路径在File - Project Structure - SDK Location或者直接在Gradle JVM下拉框里切。判断自己项目AGP版本看根目录build.gradle里的com.android.tools.build:gradle版本号就行。4.4 lint检查失败导致release构建中断library模块里如果写了某种lint告警默认情况下lint是“提示但不中断”但一旦代码里出现Error级别的问题assembleRelease会在lintVitalRelease任务上挂掉。最常见的是NewApi错误比如minSdk是21但代码用了23才有的API。对库来说因为不知道下游App的minSdk是多少这个问题会被lint死死咬住。对策在android闭包里显式关闭lint阻断lint { abortOnError false checkReleaseBuilds false }老项目里如果你看到的写法是lintOptions { abortOnError false }在AGP 8里lintOptions已经被标记deprecated建议迁移到lint块。虽然lintOptions目前还能用但升级AGP时迟早要改。4.5 改了代码但AAR没变构建缓存的迷惑行为还有一个很阴间的体验我明明改了library里的方法执行assembleRelease后把AAR发出去对方说没看到改动。检查发现是Gradle增量构建缓存把旧产物缓存住了特别是你改了AAR输出名但没改模块内部代码或者两个任务并发执行时互相污染outputs目录。对策打包前先clean一下。./gradlew :mylibrary:clean :mylibrary:assembleRelease或者加--rerun-tasks强制忽略缓存重跑全部任务。我固定流程是cleanassembleRelease连打虽然慢个一两秒但能杜绝不少玄学问题。另外如果AAR是要发出去的正式包我一般会在文件管理器里把build/outputs/aar整个目录删掉再打从根源上避免旧文件残留。4.6 Library模块里不能用switch引用R.id除非想编译报错这个坑在新手写library时特别常见。Library模块的R类字段不是final常量因为在构建阶段资源ID还没有最终确定要等主工程合并完才能分配。不是final的话Java的switch-case无法编译报错信息是“Attribute value must be constant”case后面只能跟常量表达式。对策在library代码里都用if-else if替代switch。Kotlin的when在这里不受影响因为Kotlin编译时不会把它强转为switch字节码所以用Kotlin写library的会少踩这个坑。但如果你的library是Java写的并且刚好有一堆onClick判断这个错误几乎一定会遇到。5. 拿到的AAR怎么用三种常见引用姿势5.1 最省事project模块依赖如果你们是同一套代码仓库、同一个项目里拆的模块直接用project依赖最省心。在主App的build.gradle里写implementation project(:mylibrary)这种方式的优点是不需要管AAR文件位置、不需要担心版本同步改完library代码立刻生效开发期迭代效率极高。缺点是双方必须共享同一份源码做不到真正的“二进制交付”。很多团队内部组件库维护时开发期用project依赖联调发版时切到仓库依赖就是折中方案。5.2 把AAR文件放进libs目录flatDir老写法和files新写法发给外部团队或者在本地试包时最常见的就是把AAR丢进app/libs目录。这里有两种用法我建议优先用新的files方式。第一种写法AGP 4.2之后实测较稳implementation files(libs/mylib-1.0.0-release.aar)只要AAR在app/libs下这样写就能直接编进去。没有任何额外配置干净利落。第二种是老教程里常见的flatDir方式需要在settings.gradle的dependencyResolutionManagement配置仓库dependencyResolutionManagement { repositories { flatDir { dirs libs } } }然后dependencies里写implementation(name: mylib-1.0.0-release, ext: aar)这套写法在老版本AGP里可用但新AGP对(name, ext)组合的支持一直不太稳定尤其带上传递依赖解析时很容易出问题。所以新项目我统一用files方式宁可多打几行也懒得被这个坑折腾。5.3 正经一点发布到本地Maven仓库如果你的AAR不只给一个人用而是要给多个团队、多个工程集成那libs目录就不合适了。发布到本地Maven仓库是成本最低的中间方案。在library的build.gradle里配上maven-publish插件plugins { id com.android.library id maven-publish } afterEvaluate { publishing { publications { release(MavenPublication) { from components.release groupId com.example artifactId mylib version 1.0.0 } } repositories { maven { url uri(../local-repo) } } } }然后执行./gradlew :mylibrary:publishReleasePublicationToMavenRepositoryAAR会发布到项目根目录的local-repo文件夹下。使用方在settings.gradle里加上仓库地址dependencyResolutionManagement { repositories { google() mavenCentral() maven { url uri(../local-repo) } } }接着在主工程dependencies里正常引用implementation com.example:mylib:1.0.0这个方法最大的好处是依赖能自动传递。之前说的“AAR不携带依赖”的坑在Maven发布模式下被POM文件解决了——使用方引一行Gradle会自动去解析POM里声明的OkHttp、Gson这些传递依赖。这也是团队内部最推荐的交付姿势。5.4 更进一步推到远程仓库本地仓库只适合单机或小范围试玩。真正要做成二方库、三方SDK应该推到Nexus、Artifactory、GitHub Packages这类远程仓库。发布逻辑和本地Maven几乎一样只要把publishing.repositories里的url换成远程地址再配上账号密码或token就行。远程仓库的好处是版本管理统一、天然支持传递依赖、可以不把源码暴露给使用方。这里提醒一句如果走远程仓库版本号一定要认真定。很多团队的SDK发到内网仓库后版本号用-SNAPSHOT满天飞最后使用方缓存了旧版本查半天。我习惯的做法是release版本号用三段式如1.2.0每次发布递增绝不覆盖已经发布的版本。6. 进阶方向源码AAR与多模块交付6.1 源码AAR用sources.jar解决调试联调的痛苦前面提到打包AAR时默认会生成一个sources.jar比如mylib-1.0.0-release-sources.jar。这里面装的是library的Java/Kotlin源码主要给IDE在反编译时跳转用的。本地引用AAR时Android Studio有时会自动关联同目录下的sources.jar你在代码里Ctrl点击库里的方法能看到源码实现不用每次跑去看.class反编译。如果是Maven仓库依赖IDE会自动去拉sources-artifact前提是发布时发布了sources的artifact。对SDK提供方来说给不给sources.jar是一个策略问题。内部二方库建议开放定位问题效率高对外SDK看商业考虑有些商业SDK只发混淆后的class源码包不发布。6.2 多个library合并成一个AAR的两种思路当你维护的library越来越多经常遇到“这个模块依赖那个模块、最后要一起交付”的情况。两种思路供参考。第一种是把源码合并成一个library module。把需要合并的代码统一切到一个模块里资源统一加前缀依赖统一声明。这个方案最干净构建链路简单不会出现AAR互相依赖后资源或清单冲突。缺点是模块边界被打破从架构上不再优雅适合几个库之间本身逻辑耦合度就很高的情况。第二种是使用fat-aar一类的插件把依赖的AAR/JAR内容直接拆包塞进主AAR。几年前我试过这种方式当时确实能解决“一个AAR交付完整功能”的痛点但兼容性问题非常突出AGP版本一升级插件可能就废掉。而且它天然破坏了第1.3节说的“AAR不携带依赖”原则容易导致多个AAR都内嵌同一份第三方库APK体积和类冲突双爆炸。我现在的态度是不推荐新项目用fat-aar宁可多发布几个AAR让使用方依赖解析去处理也别搞魔法打包。6.3 模块化架构下AAR的定位从一个更宏观的视角看AAR是Android模块化体系里“二进制复用”的载体。组件化开发可以以源码级module协作但跨团队、跨仓库、跨版本交付时AAR就是最标准的交接格式。它让每一个模块都有了明确的版本号、独立的编译周期和清晰的外部依赖描述主工程不再需要关心模块内部是怎么写的只要依赖一行坐标。具体到落地我比较推荐的做法是平时开发用project依赖保证迭代效率每次发版用CI脚本构建好AAR并发布到内部仓库主工程锁定版本号。这样既有开发期的热更新速度又有交付期的稳定可追溯。我自己在打包AAR这条路上踩过的坑不少从最开始不知道依赖不会传递到后来混淆规则写错地方导致合作方线上崩溃每一次都是花时间一个个排查出来的。现在团队内部已经把AAR打包发布流程规范成了固定动作配置consumerProguardRules、发布到Maven仓库、附上依赖清单、锁版本号。把这几个动作做扎实AAR这块基本就不会再出幺蛾子。
返回列表