先说结论AAB 手动打包这事平时不常用但一旦遇到必须用命令行出包的场景你会发现网上能讲清楚的文章真没几篇。我不是说那些一键打包的教程没用而是很多人没意识到手动打包的核心价值在于“可控”——你清楚每一步发生了什么也就能在出问题时最快定位到根因。这篇文章就把我用命令行手动打包 AAB 的完整流程、签名校验、常见报错一次性讲透步骤都是实测过的可以直接照着抄。1. 先搞清楚AAB 和 APK 到底差在哪很多刚接触 Android 打包的读者会问一个问题“我直接打 APK 不就行了吗为什么非要搞 AAB”这个问题问得很关键不理解这个后面看打包命令都是懵的。1.1 为什么 Google Play 强制推 AAB从 2021 年 8 月开始Google Play 就要求新应用必须使用 AAB 格式上架了。所谓 AAB全称是 Android App Bundle它不是一个可以直接安装到手机上的安装包而是一种“发布格式”。你把它上传到 Google Play 后台后Google 会根据用户的设备配置屏幕密度、CPU 架构、语言等动态生成对应的 APK这叫“分发型 APK”split APKs。打个比方你开了一家自助餐厅AAB 是你的中央厨房备好了所有菜品的原材料Google Play 是这个餐厅的配餐台它根据每个客人的口味偏好只把客人爱吃的菜装盘递出去。客人不需要背着一整包的原材料走。这样做的好处是用户下载的安装包体积会明显变小。比如你的应用同时支持 armeabi-v7a、arm64-v8a、x86 三种 CPU 架构还带多套语言资源和多套屏幕密度资源全部塞进一个 APK 里可能 80MB但 AAB 分发后用户实际下载的可能只有 40MB 左右。省流量、省存储空间安装转化率也会高一些这对开发者来说都是实打实的好处。1.2 什么时候才需要“手动”打包虽然 Android Studio 的 Build Generate Signed App Bundle 菜单点几下就能出包但手动打包依然有它的应用场景持续集成服务器CI上没法弹图形界面只能用命令行跑 Gradle 任务。你手头只有一台远程 Linux 服务器需要通过 SSH 操作完成出包。团队里脚本化构建需求多需要把打包、签名、重命名、上传这些步骤串成一条流水线。Android Studio 本身出了问题Gradle 缓存也乱了你需要在命令行里做一次彻底的 clean 和 rebuild。所以“手动打包”不是要替代 Android Studio 图形化操作而是让你在必须脱离 IDE 时依然能打出稳定可用的正式包。这篇文章所有命令都是在 macOS 终端和 Ubuntu 服务器上实测过的Windows 用户把./gradlew换成gradlew.bat即可。2. 动手前的准备工作环境与版本对齐手动打包最忌讳的就是环境不统一。很多诡异报错比如Gradle sync failed、Unsupported class file major version追根溯源都是本地 JDK 版本和 Gradle 版本不匹配造成的。所以正式开始之前先把环境对齐这件事做好。2.1 JDK、Gradle、Android SDK 版本怎么定我先给你一张我用下来比较稳定的搭配表如果你的项目 AGP 版本不是特别老直接参考这套问题不大组件推荐版本说明JDK17Android Studio 最新版内置 JBR 17命令行打包同样适用Android Gradle PluginAGP8.1.0对应 Gradle 8.0两者有严格的版本映射关系Gradle8.2 左右在gradle-wrapper.properties里指定Android SDK Build-Tools34.0.0新项目基本都要求这个级别签名工具apksignerSDK 自带比 jarsigner 支持的签名方案更全注意一点JDK 版本不是越新越好。JDK 21 我试过AGP 8.1 以下的项目跑起来会直接报错因为 AGP 内部依赖的一些库还没跟上。反过来JDK 8 编译新项目又会因为Unsupported major version失败。所以如果项目已经升到 AGP 8.xJDK 17 是当前最稳妥的选择。2.2 检查 gradle-wrapper.properties 和 build.gradle手动打包之前我习惯先看两个文件。第一个是gradle/wrapper/gradle-wrapper.properties确认distributionUrl指向的 Gradle 版本。distributionBaseGRADLE_USER_HOME distributionPathwrapper/dists distributionUrlhttps\://services.gradle.org/distributions/gradle-8.2-bin.zip networkTimeout10000 validateDistributionUrltrue zipStoreBaseGRADLE_USER_HOME zipStorePathwrapper/dists第二个是项目根目录或app模块下的build.gradle确认 AGP 版本。通常在根目录的build.gradle里能看到plugins { id com.android.application version 8.1.0 apply false }如果你是在 Android Studio 里点击“Sync Now”总是失败的地方很可能是 Gradle 版本和 AGP 版本不匹配。这里给一个官方版本对照表的简化版足够你排查大多数问题AGP 版本最低 Gradle 版本建议 JDK7.47.5118.08.0178.18.0178.28.2172.3 准备签名文件与 keystore 信息AAB 打包需要一个签名文件一般是.jks或.keystore后缀。如果你还没有这个文件用keytool命令生成一个这是 JDK 自带的工具无需额外安装keytool -genkeypair -v -keystore release.jks -keyalg RSA -keysize 2048 -validity 10000 -alias release执行过程中会让你设置密钥库密码、确认个人信息最后生成release.jks。这个文件务必妥善保管丢了就相当于你失去了对这个应用的更新权限。我见过有人把 jks 文件放在 Git 仓库里这种方式坚决不推荐后面第三节我会专门讲怎么处理签名信息的安全性。打包前你还需要确认这几项信息storeFilejks 文件路径、storePassword密钥库密码、keyAlias别名、keyPassword别名密码。如果没有这些后面配置signingConfig就会卡壳。3. 签名配置与手工加固签名是 AAB 打包的重中之重。一个没签名的 AAB 是无法被 Google Play 接受的而且 Android 系统对签名方案的校验也变得越来越严格。3.1 三种签名方式对比手动打包时签名方式可以分成三种在 Gradle 里配置signingConfigs、打包后单独用apksigner签名、以及用旧版jarsigner签名。我把它们的区别列出来签名方式适用场景支持 APK Signature Scheme v2/v3推荐指数signingConfigs bundleRelease常规正式包支持首选打包后apksigner sign无源码签名需求、CI 流水线支持推荐jarsigner老项目、仅支持 v1 签名不支持不推荐这里要特别强调 v1、v2、v3 签名方案的区别。v1 是基于 JAR 签名的对 APK 内的每个文件做校验老系统兼容性好但存在安全隐患而且校验速度慢。v2 是 Android 7.0 引入的对整个 APK 文件做二进制校验安全性和校验速度都更好。v3 在 Android 9.0 上引入了密钥轮转功能允许应用更新时更换签名密钥而不丢失设备上的数据。如果你的应用minSdkVersion大于等于 24完全可以只用 v2 及以上签名方案APK 体积还能略微减小。如果你的minSdkVersion低于 24那必须同时支持 v1 和 v2否则老系统装不上。3.2 在 build.gradle 里配好 signingConfig我平时最常用的做法是在app/build.gradle里定义一个signingConfigs然后把bundleRelease任务挂上签名。示例配置如下android { signingConfigs { release { storeFile file(../../keystore/release.jks) storePassword yourStorePassword keyAlias release keyPassword yourKeyPassword } } buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro signingConfig signingConfigs.release } } }这段配置里两个容易被忽视的点我先提一下。第一minifyEnabled true会开启代码混淆shrinkResources true会移除未使用的资源。这对正式包很重要但也意味着如果你的 ProGuard 规则不完整打包后可能会出现运行时类找不到的崩溃。后面第 6 节我会单独讲这个问题。第二storePassword和keyPassword直接写在build.gradle里有泄露风险。如果项目仓库是私有且只有你自己维护图省事直接写问题不大。但只要是团队项目我建议把密码改成从环境变量读取storePassword System.getenv(STORE_PASSWORD) keyPassword System.getenv(KEY_PASSWORD)然后在打包前用export命令设置环境变量密码就不会进入版本控制历史了。3.3 jks 文件泄露的风险控制关于签名文件的保存我分享几条实际项目里总结出来的经验jks 文件不要提交到 Git、SVN、GitLab 等任何版本控制仓库即使仓库是私有的也不行仓库权限经常比你想的宽。jks 文件建议单独放在keystore/目录并加入.gitignore与源码隔离。正式的发布密钥至少准备两份备份分别存放在不同介质上比如一台离线电脑和一个加密 U 盘。如果怀疑 jks 泄露不要心存侥幸试图“续用”应尽快用新密钥发布新版本并考虑密钥轮转不过这会带来一定的用户数据影响所以前期保护更重要。4. 核心流程一行命令打出 AAB环境准备好了签名配置也完成了接下来就是真正的打包环节。这一节我会从最基础的命令开始逐步深入到多模块项目、flavor 打包等实际场景。4.1 常用任务名与参数说明Gradle 的打包任务是有规律的你不需要死记每一个任务名掌握规律后就很容易举一反三。对单模块项目生成 AAB 的核心任务是./gradlew bundleRelease这里的bundle表示要生成 Android App BundleRelease表示使用 release 构建类型。如果是 debug 包就是./gradlew bundleDebug如果你项目里配置了 product flavors比如dev和prod那么任务名会变成bundleDevRelease和bundleProdRelease。规律就是bundle 首字母大写的 flavor 名 首字母大写的 buildType 名。打包过程中 Gradle 会先执行一系列前置任务包括资源编译、Java 编译、混淆、DEX 生成、资源压缩等。第一次打包通常会比较慢因为需要下载依赖和编译所有模块。4.2 完整的打包命令与产物核对我习惯先执行一次 clean 再打包避免增量编译带来的隐藏问题./gradlew clean ./gradlew bundleRelease如果是 CI 环境可以把两条命令合并./gradlew clean bundleRelease打包完成后AAB 文件的默认输出路径是app/build/outputs/bundle/release/app-release.aab如果你配置了applicationId为com.example.myapp文件名还是app-release.aab不会自动加上包名需要重命名的话可以手动复制一份。我做完打包后的第一件事就是用ls -lh查看文件大小大致判断产物是否正常ls -lh app/build/outputs/bundle/release/app-release.aab正常情况一个中大型应用的 AAB 文件在 20MB 到 80MB 之间。如果你发现文件特别小比如只有几百 KB那要警惕是不是代码被混淆掉了很多或者某些资源没有被正常打包进去。这时候需要去app/build/outputs/mapping/release/目录查看混淆日志确认关键类和方法是否还在。4.3 多模块项目的 flavor 打包规模大一点的项目通常会有多个模块比如:app、:core、:network、:feature_home同时还会配置多个 flavor。这种情况下手动打包的命令会稍有不同。假设 project 里有app和core两个模块flavor 维度有dev和prod你只需要在根目录执行./gradlew :app:bundleProdRelease任务名前加了模块路径:app:Gradle 会帮你自动处理所有依赖模块。比如:app依赖:core那么:core也会以prod和release对应的变体参与编译。这里要特别提醒一个多模块打包常见的坑如果你的某个子模块没有配置proguardFiles而主模块开启了混淆子模块里的代码可能会在混淆阶段被处理导致某些通过反射调用的类找不到报错。所以多模块项目里建议把公共的混淆规则统一放到主模块的proguard-rules.pro中子模块只保留自己特有的规则。5. 没有 Android Studio 也能完成签名验证打完包还没结束。很多人在本机用 Android Studio 打包、上传、上架一路顺风顺水但一到手动打包就卡在了“这 AAB 到底能不能装、签名到底对不对”的验证环节。其实完全可以在命令行里完成这套检查并不需要打开 IDE。5.1 用 bundletool 从 AAB 生成 APKSAAB 不能直接安装到设备这是它的格式决定的。Google 官方提供了一个命令行工具叫bundletool它负责把 AAB 转换成设备可识别的 APK 集合。这个工具你可以在 Google 的 Maven 仓库下载到 jar 包然后用 Java 运行。把 AAB 转换成可安装 APK 集合的命令如下java -jar bundletool-all-1.15.6.jar build-apks \ --bundleapp-release.aab \ --outputapp-release.apks \ --ksrelease.jks \ --ks-passpass:yourStorePassword \ --ks-key-aliasrelease \ --key-passpass:yourKeyPassword这里--ks指定签名文件--ks-pass传入密钥库密码--ks-key-alias指定别名--key-pass传入别名密码。参数含义和 Gradle 配置里的storeFile、storePassword、keyAlias、keyPassword一一对应。生成的文件后缀是.apks它并不是一个普通 zip而是一个 APK 集合的容器里面可能包含多个 split APK。注意这个步骤同时也在对 AAB 进行签名验证如果签名配置有误这里就会直接报错。5.2 单独提取 universal APK.apks文件生成后如果你想安装到本地设备做冒烟测试有两种方式。第一种是直接用 bundletool 安装连接好的设备java -jar bundletool-all-1.15.6.jar install-apks --apksapp-release.apks第二种是先从.apks中提取一个通用 APKuniversal APK这个 APK 包含所有资源相当于传统意义上的完整 APK然后传给同事或者手动安装java -jar bundletool-all-1.15.6.jar extract-apks \ --apksapp-release.apks \ --output-dir./extracted \ --device-specdevice-spec.json这里--device-spec可以指定设备配置如果你不指定bundletool 会尝试连接一台在线设备并读取配置。如果只想提取所有设备通用的那个 APK可以试试unzip -o app-release.apks -d apks_output解压后你会看到一个universal.apk这个就是包含全部资源的完整 APK。直接把它传到手机上就能安装。5.3 验证签名与安装到设备用apksigner可以查看 AAB 或 APK 的签名详情这个工具在 Android SDK 的build-tools目录下$ANDROID_HOME/build-tools/34.0.0/apksigner verify --print-certs app-release.aab如果输出里有Signer #1 certificate DN: CN...这样的信息并且Verified using v2 scheme: true说明签名没问题。如果显示DOES NOT VERIFY或Verified using v1 scheme: true而 v2 是 false你就要根据第一节说的 minSdkVersion 情况判断是否合规。6. 我踩过的坑常见错误与排查实录这一节是我最想写的部分。手动打包不像 Android Studio 图形化操作那么“友好”遇到问题的话报错信息往往很裸但其实就是那几类问题排查思路是固定的。6.1 Execution failed for task :app:bundleRelease这是最常见的报错没有之一。报错信息后面通常还会跟着一句Execution failed for task :app:packageReleaseBundle或类似的字眼。出现这个问题我建议按以下顺序排查看日志中是否出现AAPT2 error。如果是基本是资源文件有问题。常见的场景是某个图片资源用了中文命名、XML 布局里有非法字符、或者 values 目录下的字符串资源有格式错误。逐个检查报错里提示的文件路径通常能发现线索。看是否出现Duplicate resources。这通常是因为多个模块依赖了同一个库的不同版本或者res目录下存在同名资源。在build.gradle里显式排除冲突依赖即可。看是否出现Failed to transform ...。这类问题大多与依赖下载失败或缓存损坏有关。先试试清除 Gradle 缓存再重试./gradlew clean ./gradlew bundleRelease --refresh-dependencies如果还不行删掉用户目录下的.gradle/caches目录让 Gradle 全量重新下载依赖问题大概率解决。6.2 Signature scheme V2 缺失导致上架失败这种情况比较隐蔽。你在本地打包后上传到 Google Play Console系统提示 APK Signature Scheme v2 缺失或者签名无效但你在 Android Studio 里打包却一切正常。出现这个差异的原因通常是你本地手动打包时用了旧版jarsigner而jarsigner只生成 v1 签名。解决办法是改用apksigner对 AAB 进行签名或者在 Gradle 配置中确认signingConfig用的是正确的密钥。验证方法就是我上节写的apksigner verify --print-certs看完输出就知道问题出在哪了。6.3 混淆规则漏配导致的崩溃我在第 3 节提到过minifyEnabled true开启后如果 ProGuard 规则不全打包出来的应用会在运行时崩溃。最常见的现象是安装没问题、启动闪退Logcat 里出现ClassNotFoundException或者NoSuchMethodException。排查思路并不复杂。先看混淆日志app/build/outputs/mapping/release/mapping.txt找到报错类在混淆前的全限定名然后确认是否被错误的重命名了。如果是你自己写的类在proguard-rules.pro里加-keep规则即可-keep class com.yourpackage.yourmodule.** { *; }如果报错的是第三方 SDK 的类优先去对应 SDK 的官方文档找它提供的 ProGuard 配置通常是一段可以直接贴到proguard-rules.pro的规则。不要不加思考地-keep整个依赖那样会失去混淆的意义APK 体积也会变大。6.4 版本号与 versionCode 冲突手动打包时另一个容易踩的坑是 versionCode 没改导致上传到 Google Play 时提示版本号冲突。Android Studio 的 Release 构建菜单会在构建前检查这个问题但是命令行打包没有这个前置检查你很容易带着同一个 versionCode 打出两个包。解决方法是每次手动打包前检查app/build.gradle里的versionCode或者用脚本在 CI 里自动递增def versionCode (System.getenv(CI_PIPELINE_ID) ?: 1) as Integer6.5 打包慢与内存溢出的处理一个大项目首次打包耗时 10 分钟以上很常见。如果你的服务器内存比较小可能会出现Daemon is stopping immediately due to memory pressure或者GC overhead limit exceeded之类的错误。可以在项目根目录的gradle.properties里调大 Gradle JVM 内存org.gradle.jvmargs-Xmx4g -XX:MaxMetaspaceSize1g -Dfile.encodingUTF-8如果需要进一步加快构建速度考虑开启构建缓存和并行执行org.gradle.cachingtrue org.gradle.paralleltrue不过我实测下来并行执行在模块依赖比较复杂时反而可能因为资源竞争导致某几个模块的编译时间增加所以如果你的项目模块数不多保持默认串行反而更稳。手动打包到这个阶段你已经可以脱离 Android Studio 完成从源码到成品的全部工作了剩下的就是把这些命令串成脚本、接入到你的 CI 流程里让出包变成一件不再依赖人工的重复劳动。