十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Android AAR包打包与引用全攻略:Gradle配置、依赖管理与排错实战

Android AAR包打包与引用全攻略:Gradle配置、依赖管理与排错实战 简介针对Android Studio中本地库模块的打包与复用问题这份PDF教程系统地讲解了aar格式的完整知识链。内容先从官网定义入手说明aar是Android Library项目的二进制分发格式扩展名为.aar本质是一个ZIP压缩包内部包含AndroidManifest.xml、classes.jar、res/、R.txt等必需条目以及assets/、libs/*.jar、jni/、proguard.txt等可选内容随后给出将一个Module编译成aar的菜单操作路径并说明生成文件默认位于outputs/aar目录最后演示如何在其他工程中引用该aar包括将aar复制到app/build/libs、在build.gradle中添加implementation files(build/libs/xx.aar)以及在Java代码中import对应类并使用。整个资源共1个PDF文件压缩包约175KB内容精炼适合初级Android开发者快速上手模块化打包与依赖引入。目前已有4635人学习下载对正在做组件化改造或需要向团队分发功能模块的开发者尤其具有参考价值。1. aar包到底能装什么为什么模块化之后一定绕不开它把网络请求、图片加载或者账号体系抽成独立模块之后你会很快遇到一个分界线给别人交付产物时jar 包只能装 class 文件资源、清单、so 库统统带不过去。aar 包的出现就是来解决这件事的。它本质上是一个 zip 压缩包里面除了编译后的代码还能携带 res 资源目录、AndroidManifest.xml、JNI 库、R.txt 以及 assets 目录。换句话说jar 做不了的事aar 能接着做完。在 Android Studio 里新建模块时选 Android Library其实就是为生成 aar 做准备。但很多人第一次打包时会遇到一个困惑为什么同样一个模块在工程里可以直接依赖导出来后却 build 不出 aar因为 Gradle 默认只在 library 模块被独立构建时才去生成 aar而且配置不对时产物可能会混进 debug 和 release 多个变体。这些问题都不难解决关键是先把 aar 的生成路径、引用方式和依赖传递机制理清楚。这篇文章会从打包命令讲起一直写到多模块协作时怎么分发 aar、怎么排查资源冲突适合那些正在把项目拆库、或者第一次向外输出 Android 组件的开发者。2. 用 Android Studio 打包 aar 包最小可复现的 Gradle 配置2.1 先确认模块类型别在 application 模块里白等打包 aar 之前第一步是确认这个模块到底是什么角色。一个模块如果能在 Android Studio 里作为独立工程运行、有入口 Activity 和启动图标那它多半是 application 模块构建产物是 apk永远不会产生 aar。要输出 aar模块必须是 library 类型。新建工程后在 File New New Module 里选 Android Library生成的模块里 build.gradle 会带上com.android.library插件而不是com.android.application。plugins { id com.android.library } android { namespace com.example.mylibrary compileSdk 34 defaultConfig { minSdk 21 targetSdk 34 } }这段配置里的关键是com.android.library插件。它告诉 Gradle 不要把模块编译成可安装的 APK而是把所有产物连同资源打包成 AAR。namespace必须和最终依赖方使用的包名一致否则引用方 compileSdk 编译时报包名冲突或找不到 R 类。minSdk不能比引用方项目的 minSdk 更高否则 Gradle 会在合并时直接报错。2.2 构建命令和 aar 产物出现的路径模块配置确认之后打开 Android Studio 右侧的 Gradle 面板展开该模块的 Tasks build双击 assembleRelease或者直接在终端执行下面的命令./gradlew :mylibrary:assembleRelease如果你没有把模块名写进 settings.gradleGradle 工程同步时会失败所以先确认settings.gradle里有include :mylibrary。命令执行完毕后产物出现在mylibrary/build/outputs/aar/目录下可能存在两个文件mylibrary-release.aar和mylibrary-debug.aar。只执行 assembleRelease 不会出现 debug 包只执行 assembleDebug 则只有 debug 变体。Gradle 会按变体名给产物命名默认的命名规则是模块名-变体名.aar。如果你需要自定义产物文件名可以这样配置android { libraryVariants.all { variant - variant.outputs.all { output - outputFileName mylib-${variant.name}-${variant.versionName}.aar } } }libraryVariants遍历所有构建变体outputFileName直接改变最终输出文件的名字和路径。这个配置在发版的时候很有用里面可以拼版本号、渠道名或者构建时间避免多人协作时把同名 aar 混淆。2.3 打一个包含 so 库和资源的 aarjniLibs 与 assets 的处理aar 的容量优势体现在它能打包 .so 文件。很多开发者把 .so 直接放在 Java 代码里用 System.loadLibrary 加载然后提交 aar 时发现引用方总是报 UnsatisfiedLinkError。原因是 .so 文件在新建库模块后默认不会自动打包进 aar除非放进src/main/jniLibs/abi/目录下。src/main/jniLibs/ ├── arm64-v8a/ │ └── libnative-lib.so ├── armeabi-v7a/ │ └── libnative-lib.so └── x86/ └── libnative-lib.so如果你用的是 cmake 或 ndk-build通常需要显式指定 abiFilters 来过滤不需要的指令集这样 aar 体积更小也不会在安装时出现 so 库缺失。资源、assets 和 manifest 不需要额外配置Gradle 默认会把它们合并进去。值得注意的是aar 里引用的 res 文件名会和主工程产生命名冲突所以库内资源名最好加上前缀比如模块名缩写这是打包方最容易忽略的一件事。3. 引用 aar 包的三种方式文件导入、本地 Maven 仓库和模块依赖3.1 直接在 libs 目录下放 aar用 implementation files 引用最常见的离线引用方式是把 .aar 文件拷贝到主工程的app/libs/目录下然后在 build.gradle 里写依赖dependencies { implementation fileTree(dir: libs, include: [*.jar]) implementation files(libs/mylibrary-release.aar) }注意fileTree只能匹配 jaraar 必须要用files()显式声明。写完配置后需要执行一次 Gradle SyncAndroid Studio 会把 aar 里的资源、清单文件解析到 IDE 中代码里才能识别到库的类。很多人卡在这一步是因为忘记 addflatDir仓库其实只要用implementation files(libs/xxx.aar)不需要在repositories里加 flatDir。只有使用implementation(name: xxx, ext: aar)这种旧写法时才需要repositories { flatDir { dirs libs } } dependencies { implementation(name: mylibrary-release, ext: aar) }这种name:ext的写法在 Gradle 7 之后已经不太推荐但你接手老项目很可能会看到所以建议知道它存在即可。在新工程里优先使用files()方案路径直白Gradle 同步时的失败日志也更清晰。3.2 使用 Maven 仓库引用本地仓与私服的 GAV 坐标只有文件还不够团队协作时每个人都要手动拷贝 aar 太容易出错。标准做法是先发布到 Maven 仓库无论是私有仓库还是本地目录然后用 GAV 坐标依赖。在 library 模块里配置 maven-publish 插件plugins { id com.android.library id maven-publish } afterEvaluate { publishing { publications { release(MavenPublication) { artifact bundleReleaseAar groupId com.example artifactId mylibrary version 1.0.0 } } repositories { maven { url uri(${rootProject.projectDir}/repo) } } } }bundleReleaseAar是 Android 插件提供的一个 taskmaven-publish 会依赖它把 aar 和 pom 文件一起产出到指定目录。引用方需要加入同一个仓库源repositories { maven { url file:///path/to/your/repo } } dependencies { implementation com.example:mylibrary:1.0.0 }本地 Maven 仓库的路径写的是绝对路径而跨机器协作时每个开发机的路径通常不一样。如果是私服url 则是 http 地址这时需要额外处理仓库用户名密码通常写在~/.gradle/gradle.properties里而不是提交到代码仓库。对比文件引用GAV 方式最大的优势在于传递依赖能生成 pom 文件引用方的 Gradle 能自动把 aar 依赖的第三方库也拉下来不用手写多次 implementation。3.3 模块依赖开发期调试和输出 aar 之间的切换套路开发时最顺手的方式其实是 module 依赖直接在主工程的 settings.gradle 里 include然后写implementation project(:mylibrary)。好处是改动库代码立刻能生效不用反复打包 aar。但交付时有交付的依赖方式很多人会在这两套配置之间反复注释掉换。更好的做法是用 Gradle 的 project 属性做切换比如在 gradle.properties 里放一个开关// gradle.properties USE_LOCAL_MODULEtruedependencies { if (USE_LOCAL_MODULE.toBoolean()) { implementation project(:mylibrary) } else { implementation com.example:mylibrary:1.0.0 } }这样切开发态和发布态的时候只改一行配置。这种模式在多个 APP 同时引入统一库组件时最好用避免在 Git 提交记录里反复出现依赖配置的改动。不过要注意切换后必须 Gradle Sync否则 IDE 索引不会刷新。4. aar 包依赖关系的高级管理传递依赖、变体和发布流程4.1 为什么引用方总是报缺类因为 aar 的依赖默认不传递aar 本身不考虑你的依赖别人拿到你的 aar 时如果你的库内部引用了 okhttp、gson 这类第三方库引用方必须自己在 build.gradle 里再写一遍依赖。Gradle 不会因为你在 library 模块里写了 implementation 就自动把这些库带过去。这被称作依赖不传递问题因为implementation对外隐藏了依赖关系。要解决引用方缺类问题打包发布的时候关键在于 pom 文件里是否正确列出了依赖。maven-publish插件默认生成的 pom 只会包含api声明的依赖implementation依赖不会出现在 pom 里。如果你想控制哪些被传递出去应该这样写dependencies { api com.squareup.okhttp3:okhttp:4.12.0 implementation com.google.code.gson:gson:2.10.1 }上面的配置里okhttp 会出现在 aar 的 pom 文件中引用方会通过 Maven 传递依赖自动引入gson 则不会传递引用方如果直接使用 gson 类型会在编译时报错。这里有一个很容易掉进去的坑本地files()方式引用 aar 时即使末端的 aar 里有 gson 的传递依赖配置Gradle 也不会读它因为文件依赖没有关联的 pom 文件。所以没有私服的团队最好把 aar 发布到本地 Maven 或私服而不是只靠 libs 目录分发。4.2 不同 buildTypes 打出不同 aardebug 与 release 混淆状态不一致模块的 buildTypes 配置影响 aar 内容。很多人会在 library 模块里这样写buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } debug { minifyEnabled false } }然后在引用方只 debug 测试不出问题发布时打出 release aar 给到别人引用方一调用就崩。原因在于 release 开了混淆类名、方法名被重命名而工程里没有配置 keep 规则。典型的错误是引用方拿到的 release aar 里类全成了 a.b.c反射、Gson 转换、注解处理全部失效。确认这个问题的快捷方式是直接打开 aar 看内容。aar 内部的 classes.jar 用 jar 工具解压后view 一层文件夹名就知道是否混淆过。如果内部类已经变成单字母说明 keep 规则缺失。建议在 library 模块的 proguard-rules.pro 中把所有公开 API 的类全部保留-keep public class com.example.mylibrary.** { *; } -keepclassmembers public class com.example.mylibrary.** { *; }需要说明的是即便你只在最后一次才打 release aar也应该在本地先跑一次单元测试或引用工程冒烟测试否则混淆带来的问题通常只在线上爆发。4.3 aar 内部的 R 类和资源冲突合并机制与命名空间要求aar 里的 R 类生成规则与主工程不同。library 模块的 R 类在构建时属于库的包名引用方构建时会触发资源合并。如果库的 res 目录下有一个和主工程同名的strings.xml资源项Gradle 会按优先级保留主工程的值有些情况下不会报错但行为会变得诡异库内部访问资源时拿到的值和库开发时预期的不一样。更麻烦的情况是多个 aar 里有同名资源构建时出现 Resource shrinker 相关的错误。常见报错是Attribute applicationicon value(drawable/icon) from ...。这种情况下只能逐个 aar 定位重复资源然后改名字或覆盖资源。兼容写法是给库的 res 资源加一个统一前缀常见做法用库名缩写加下划线比如mylib_btn_confirm.xml。这个习惯虽然不强制但你会发现过了几个版本后资源冲突排查成本远高于当时改名成本。Gradle 7 及以上版本强制要求android.nonFinalResIds和命名空间的不同库模块的 R 类不再和主工程合并到一个包下所以类冲突概率变低但资源文件本身的冲突仍然不可忽略。打包分发之前建议在 library 模块里执行一次gradlew :mylibrary:lintRelease资源引用问题和重复资源问题大多能提前暴露出来。4.4 aar 版本管理SNAPSHOT 与 release 的使用时机发布到 Maven 仓库时版本号后面加不加-SNAPSHOT会影响引用方的缓存策略。正式版1.0.0一旦发布Gradle 会默认缓存 24 小时同一版本号如果私服上的文件更新了引用方依然会拉旧包。团队协作中如果每个人都复用1.0.0某个人偷偷上传新文件其他人的构建结果将不可复现。日常联调建议用1.0.0-SNAPSHOT版本Gradle 在每次构建时会检查私服上的时间戳只要后端更新了就能拉下来。发稳定版时去掉-SNAPSHOT并锁死版本号。这个约定不需要额外插件maven-publish 会自动识别以-SNAPSHOT结尾的版本并生成 maven-metadata.xml。引用方如果想提升检查频率可以在 gradle.properties 中设置org.gradle.internal.http.socketTimeout120000缓存策略可以在仓库配置时微调但一般不需要改用好 SNAPSHOT 就足够了。5. 引用方集成 aar 的排错清单编译报错、资源冲突和版本锁定5.1 类冲突和 Duplicate class 的定位方式引用第三方 aar 时最常见的编译错误是 Duplicate class意思是两个 jar/aar 里有同一个全限定类名。用命令行先看完整错误还能定位到具体模块但错误日志比较长时可以直接在 Gradle 面板执行一次 dependencies 输出所有依赖树./gradlew :app:dependencies --configuration releaseRuntimeClasspath dep.txt在生成的 dep.txt 里搜索报错中的类名比如com.example.common.util.StringUtils你会发现它同时出现在多个依赖项下面。定位到是哪个库重复的之后有几种处理方式。保留你想要的那个库版本另一个用 exclude 排除掉implementation(com.example:libA:1.2.0) { exclude group: com.google.code.gson, module: gson }exclude的作用是剪掉 libA 的传递依赖里引入的 gson然后自己再显式依赖一个固定版本。这样能把冲突收敛到一个版本。如果冲突来自两个独立的 aar依赖排除解决不了那就只能选其中一个库另一方改源码或换实现。5.2 minSdk 与 Java 版本不兼容时的常见报错aar 在 library 模块里定义了自己的 minSdk引用方工程如果 minSdk 比它低编译不会主动报错但运行时可能在低版本设备上出现 NoSuchMethodError。常见场景是库用了 java.time 包主工程 minSdk 是 19java.time 在 API 26 才可用结果 Android Studio 本身在编译期不带任何警告运行时在低版本设备上瞬间崩溃。排查方式是直接看 merge 后的 manifest 或查看库的 pom 文件。更直接的方法是把项目的 minSdk 调高到和 aar 一致这通常不可控所以务必要在库的 README 或发布说明中写明最低 API 要求。Java 版本问题则表现为编译报 Unsupported class file major version一般是库编译用了 Java 17而主工程仍然用 Java 8。老规矩保持主工程 compileOptions 里的 sourceCompatibility 不小于库的编译版本或者干脆统一到 17。5.3 aar 内部使用 Kotlin 协程引用方却拉不到协程库aar 的 pom 中没有把 Kotlin 标准库标记为强制依赖而 Kotlin 协程库属于 optional 时引用方构建时会遇到kotlinx.coroutines包不存在的错误。Gradle 依赖树里只看你的声明不会知道是某个库运行时需要的。解决办法是在引用方的依赖中添加implementation org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1但更隐蔽的情况是协程版本不一致a aar 用 1.6 编译主工程引了 1.8运行时偶发找不到方法。建议引用方做一次完整的依赖树审查如果再叠加资源冲突和 manifest 冲突不这么做很难定位到根因。5.4 引用 aar 后 AndroidManifest 合并冲突的设置与取舍aar 中会带一份自己的 AndroidManifest.xml里面可能声明了权限、Activity、provider 等。引用方构建时会将其与主工程清单合并如果两端都注册了同一个组件或同一条权限冲突会在合并阶段报错。典型场景是多个 aar 同时定义了同一个 FileProvider authority运行时立刻 crashUnable to get provider ...。解决冲突的常用做法是使用 tools:node 属性在主工程 manifest 对应节点上加tools:nodereplace强制完全替换库的声明provider android:nameandroidx.core.content.FileProvider android:authorities${applicationId}.fileprovider tools:nodereplace /还可以用tools:remove删除库中的某项声明用tools:replace替换特定的属性。合并冲突报错信息里会明确点名冲突节点照着提示加工具属性即可。不要出现一排 provider 直接复制粘贴的行为authority 全局唯一重复注册直接运行时报错。5.5 反复改 aar 却没有生效Gradle 缓存与构建产物对不上一个很常见但又不容易发觉的坑是引用方已经替换了 libs 目录下的 aar 文件但重新编译运行时用的还是旧代码。因为 Gradle 对依赖项有缓存尤其当你用implementation files(libs/xxx.aar)时Gradle 会以文件的 lastModified 时间来判断是否重新解析。如果你是用 git 拉取或者压缩包解压文件修改时间为当前时间问题不大但你直接把新 aar 覆盖旧文件且文件的时间戳完全一样Gradle 会认为文件没变。遇到这类问题时执行一次 Gradle 强制刷新./gradlew :app:clean ./gradlew :app:assembleDebug --refresh-dependencies--refresh-dependencies会让 Gradle 重新检查所有动态依赖但本地 files 方式的 aar 不完全受它控制。最稳妥的办法是给 aar 文件改名或者删掉 build 目录重新构建。用 maven 方式依赖没有这个问题因为 Gradle 每次构建前都会去仓库检查更新。6. 拿到一个陌生 aar 之后的验证技巧反解产物、看依赖、改资源后重新打包把 aar 部署到引用方工程之前我会先做一次离线反向检查防止把坏包发给多个团队。aar 的本质是 zip 压缩包所以直接用 unzip 命令解压即可不需要 Android Studio 辅助。unzip mylibrary-release.aar -d aar_preview cd aar_preview解压后的目录中通常有classes.jar、AndroidManifest.xml、R.txt、res/、assets/等。如果里面有jni/目录可以ls jni/查看 so 库覆盖了哪些 ABI。缺少某个 ABI 会导致对应机型安装时无法加载 so 库特别容易漏掉 arm64-v8a。确认内容无误后想查看 classes.jar 里的类结构把它解出来用 javap 看接口签名。这个步骤适合快速确认打包时是否开启混淆以及包是否完整但对 Kotlin 的协程类型会有空安全相关的影响需要额外小心。更近一步的操作是修改别人的 aar 里的资源或者清单这种需求常出现在要临时改第三方库的 authority 名称、或者想替换某个字符串时。具体步骤是解压 aar修改文件后重新压缩但注意 classes.jar 里的 dex 无法直接改只能改 res 和 assets。重新压缩时必须保持目录结构命令如下cd aar_preview zip -r ../mylibrary-modified.aar . -x .DS_Store-r递归压缩当前目录全部内容-x排除 macOS 的临时文件。重打包后的 aar 可以再次被引用但签名会被破坏不过只做本地联调问题不大。还有一点反解 aar 时不要试图直接改 dex即使改了也无法通过较新 Android 版本的验证因为 Gradle 引用 aar 后依然会进行 dex 合并对一个已签名或经过伪混淆的 dex 再做热替换运行期出问题基本只能在真机崩溃日志里看到。最后再推荐一个常用验证流程在一个全新的空工程里引用该 aar只写一段调用库 API 的代码编译并跑通一次。空工程排除主工程里的其他依赖干扰能更快暴露 aar 自身的资源缺失和传递依赖问题。利用 kotlin 脚本写一个冒烟测试接口把所有对外暴露的方法都走一遍比在现有的大工程里测试更可控。我第一次给跨团队分发 aar 时就因为直接在旧项目里集成导致问题被老依赖掩盖了整整一天。本文还有配套的精品资源点击获取
返回列表