2026/9/7 8:29:22

AAB手动打包实战:命令行构建、签名与验证全链路解析

AAB手动打包实战:命令行构建、签名与验证全链路解析 作为一个常年和Android构建打交道的人我得说AABAndroid App Bundle手动打包这事表面上看起来只是比APK多勾一个选项可真到了没有Android Studio图形界面、只在命令行环境里摸爬滚打的时候很多人才发现自己对AAB的构建链路其实一知半解。这篇文章不打算把操作步骤简单罗列一遍就完事而是会把环境准备、命令行编译、签名校验、安装验证整个链路拆开讲清楚。适合三类人需要在CI或服务器上出AAB的工程师、被要求交付AAB母包给渠道方的移动端开发、以及想彻底搞懂Android构建产物细节的进阶学习者。1. 为什么放着IDE不用偏要手动打包AAB1.1 手动打包的典型场景说个我自己经历过的情况。有次接了一个外包交付项目客户要求提供AAB格式的母包但我们这边负责打包的同事用的是Windows电脑Android Studio里的Gradle任务一跑就报内存不足试了几次都没成。后来我远程连上去开了一个命令行终端直接敲gradlew bundleRelease十几分钟就出了包。倒不是说Android Studio不好用而是在某些特定环境下IDE的图形按钮反而是最不可控的一环。手动打包AAB最常见的需求通常集中在这么几个场景CI/CD服务器构建Jenkins、GitLab Runner这类环境通常没有图形界面也不可能装完整的Android Studio最稳妥的方式就是命令行构建。跨团队交付母包有些发行商、预装渠道或合作方不要APK只要AAB而且对构建产物的结构、签名方式有明确要求这时候需要手动控制每一步。复现构建问题Gradle在Android Studio里报错时日志经常被IDE吞掉一部分直接在终端跑命令能拿到完整错误堆栈。定制化构建流程比如需要在打完AAB之后自动拷贝到指定目录、自动改版本号、自动做多语言资源裁剪这些都可以写在脚本里比人肉操作IDE稳定得多。1.2 手动打包不等于没有Gradle需要先界定一个概念很多人一听手动打包以为是要完全不依赖Gradle像远古时代那样手写aapt、javac、d8一条条命令。其实90%以上的生产场景所谓手动打包指的是脱离Android Studio的图形界面通过Gradle命令行工具完成构建。真正到aapt2、d8、bundletool这个粒度的手工构建通常只出现在复杂问题排查或自定义构建工具的研发中普通项目没有必要也不用这么做。所以我在后文里会分成两个层面来写第一层是Gradle命令行打AAB这是绝大多数人能直接复用的方案第二层是拆解AAB的诞生过程看看Gradle背后到底替你干了哪些活这一步虽然不用你每回都做但理解之后能帮你少踩很多坑。2. 环境清单工具链版本是最大的隐性坑手动打包的第一步不是敲命令而是确认环境。我见过太多人卡在sdk component was not installed、Gradle sync failed这种问题上一整天回头一看全是版本对不上。2.1 JDK、Gradle、AGP的三角关系Android的构建工具链对Java版本极其敏感。JDK版本太高或太低Gradle会在配置阶段直接崩溃错误信息往往还写得晦涩难懂。这里有一份对应关系是我自己试出来的现在基本当作默认配置用Android Gradle Plugin 版本最低JDK版本Gradle版本要求AGP 4.xJDK 8Gradle 6.xAGP 7.xJDK 11Gradle 7.xAGP 8.xJDK 17Gradle 8.xAGP 8.2JDK 17Gradle 8.2检查当前JDK版本终端执行java -version如果你电脑上装了好几个JDK务必确保JAVA_HOME指向正确的那一个。在macOS或Linux上可以用/usr/libexec/java_home -V查看已安装的JDK列表Windows则直接在环境变量里改。还有一个非常容易忽略的点Android Studio自带的那套JRE和你在命令行里用的JDK可能不是同一个。在Android Studio里构建没问题一到命令行就报Unsupported class file major version多半就是命令行的JDK版本比AGP要求的要高或者低到不被支持。2.2 Android SDK与Build Tools版本匹配手动打包时需要确保ANDROID_HOME环境变量已经配置指向SDK目录。Linux/macOS上我一般这样配置export ANDROID_HOME$HOME/Android/Sdk export PATH$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/build-tools/33.0.2Build Tools版本选择有个小技巧不要盲目追新就看项目里build.gradle的buildToolsVersion要求。如果项目没有显式声明Gradle会用AGP默认绑定的版本。构建时报The following SDK component was not installed: android sdk build-tools XXX就是SDK Manager里没装对应版本一条命令补齐sdkmanager build-tools;33.0.2 platforms;android-33需要注意的是新版的AGP也会自动补装缺失的SDK组件但前提是你的ANDROID_HOME目录有写入权限并且命令行环境能连到SDK仓库。有时候构建日志里明明写着android sdk is up to date.但后续还是报找不到构建工具多半是权限或路径问题。2.3 准备好命令行工具集合打AAB需要三样核心工具Gradle项目自带gradlew包装器这是最推荐的方式能保证团队使用一致的Gradle版本。SDK Build Tools内部包含aapt2、d8、apksigner等核心工具。bundletool用于构建、验证AAB以及从AAB生成APK集。我之前遇到过一个问题客户提供的构建脚本里用了一个特定版本的bundletool而我本机装的是最新版结果解析同一个AAB时行为不一致生成的APK集体积都不一样。此后我学乖了在任何自动化流程里都把bundletool的版本号固定绝不自动升级。bundletool是一个jar包需要Java环境运行下载后可以放到统一目录例如~/tools/bundletool.jar方便脚本引用。3. 从零构建AAB的完整命令行流程3.1 用Gradle命令行输出AAB这是最接近生产环境的手动打包方式。假设你的项目根目录有gradlew文件没有的话用gradle wrapper生成一个先执行环境检查./gradlew -version然后构建一个不签名的调试AAB./gradlew :app:bundleDebug产物路径在app/build/outputs/bundle/debug/app-debug.aab调试包的签名用的是debug keystore所以直接是可以用apksigner验证的。如果要打Release包需要先确认app/build.gradle里的signingConfig正确配置了。执行./gradlew :app:bundleRelease产物路径app/build/outputs/bundle/release/app-release.aab这里有一点需要注意bundleRelease任务只负责把代码、资源、原生库等文件打包成AAB格式是否签名取决于你项目里的signingConfig配置。如果没配生成的就是未签名AAB后续需要用jarsigner补签。如果你不想把签名信息硬编码在build.gradle里可以用命令行参数方式临时指定签名配置但Gradle本身不直接支持在命令行传keystore密码常见做法是读取环境变量signingConfigs { release { storeFile file(System.getenv(KEYSTORE_FILE) ?: release.keystore) storePassword System.getenv(KEYSTORE_PASSWORD) keyAlias System.getenv(KEY_ALIAS) keyPassword System.getenv(KEY_PASSWORD) } }这样既能保证密码不泄漏到仓库里也能让打包命令保持统一KEYSTORE_FILE/data/keys/release.keystore \ KEYSTORE_PASSWORDxxx \ KEY_ALIASmyapp \ KEY_PASSWORDxxx \ ./gradlew :app:bundleRelease3.2 验证AAB包是否完整打完包不能直接就交出去至少得确认AAB的内部结构符合预期。AAB本质上是一个带特殊目录结构的Zip文件你可以用unzip直接看unzip -l app-release.aab | head -50一个正常的AAB会包含这些关键元素base/manifest/AndroidManifest.xml编译后的manifest文件base/dex/classes.dexDEX字节码base/res/资源文件base/assets/assets目录base/lib/各ABI架构的原生库BundleConfig.pb描述AAB元信息的protobuf文件META-INF/签名和清单信息这里特别强调一点AAB包里不应该有resources.arsc这类APK专属文件。如果你在AAB里看到类似APK的结构那说明打包流程可能走错了Gradle直接生成了APK但改了扩展名这种情况上传到应用市场会被拒绝。用bundletool验证一下AAB是否有问题java -jar ~/tools/bundletool.jar validate --bundle app-release.aab如果输出BUILD SUCCESSFUL或者Bundle app-release.aab is valid.说明结构没问题。3.3 理解Gradle背后做了什么AAB的诞生过程如果你只是需要打个包上面步骤已经足够。但一个合格的构建负责人应该知道AAB是怎么被拼装出来的。我把它拆成四步每一步对应一个底层工具第一步编译资源。AAB里的资源不是普通APK那种二进制资源表而是proto格式以便上传到应用市场后按需分发。Gradle调用aapt2 link --proto-format把res/目录和AndroidManifest.xml编译成resources.pb。这也是为什么你直接拿aapt2打出来的APK结构和AAB结构不一致的原因。第二步编译字节码。Java和Kotlin代码先被编译成.class文件再由d8转换成classes.dex。如果你的项目启用了混淆R8或ProGuard会在这个阶段介入移除无用代码和重命名类。第三步装配模块。AAB可以有多个模块base、feature等每个模块独立生成后需要按约定结构组合到一起。bundletool的build-bundle命令就是负责最后装配的。如果你要手动验证某个模块是否独立比如动态功能模块可以执行java -jar ~/tools/bundletool.jar build-bundle --modulesbase.zip,feature1.zip --outputtest.aab第四步签名。AAB使用的是JAR签名方案v1签名而不是APK的v2/v3签名。这一步Gradle同样会替你做但如果你想完全脱离Gradle手动签名就需要自己执行jarsigner。理解这四步之后遇到奇怪问题你就能快速定位到底是从哪一步开始坏的而不是干瞪眼。我在实际工作中遇到过一回资源文件里引用了一个只有Release资源目录才存在的drawableDebug构建完全正常Release却一直报资源链接失败。我根据报错直接定位到是aapt2 link阶段的问题而不是DEX或者签名的问题排查时间从一整天缩短到半小时。4. 签名AAB手动签名和APK完全不是一个套路4.1 为什么AAB要签名但方式又不一样AAB在Android构建体系里属于待分发产物应用市场拿到AAB后会再生成、重签APK。所以在AAB阶段使用的签名主要用途是验证来源合法性而不是直接用于设备安装验证。Google官方推荐对AAB使用JAR签名v1签名方案而不是APK Signature Scheme v2/v3。这里有一个很容易踩的坑有人用apksigner去给AAB签名发现命令跑完没有报错但生成的AAB拿去上传或安装时各种校验失败。原因就是apksigner只会写入v2/v3签名块而AAB需要的v1签名信息在META-INF里两者的签名结构根本不兼容。4.2 用jarsigner给AAB签名手动签名其实不复杂关键是用对工具。假设你有一个keystore文件release.keystore别名是myapp执行jarsigner -keystore release.keystore \ -signedjar app-release-signed.aab \ app-release-unsigned.aab \ myapp根据提示输入keystore密码和密钥密码即可。如果不希望交互式输入密码可以用-storepass和-keypass参数jarsigner -keystore release.keystore \ -storepass YOUR_STORE_PASS \ -keypass YOUR_KEY_PASS \ -signedjar app-release-signed.aab \ app-release-unsigned.aab \ myapp等命令执行完可以用以下命令验证签名jarsigner -verify -verbose -certs app-release-signed.aab看到jar verified.就是完整签名成功。如果要给AAB重新签名比如客户给了一个带他们私钥签名的AAB但我们拿到的源包是一个debug签名的这时必须先移除旧签名再签新的zip -d app.aab META-INF/*.SF META-INF/*.RSA META-INF/*.DSA META-INF/MANIFEST.MF然后重新执行jarsigner。4.3 签名方案和密钥选择建议在新项目里Google要求AAB使用上传密钥upload key签名后续通过应用市场进行应用签名。如果你维护一个已经上线的应用不要随便更换上传密钥否则会面临密钥重置申请流程非常麻烦。密钥长度的建议是使用RSA 2048位以上或者直接用EC密钥。那些还在用1024位RSA的老项目我建议尽早规划迁移。另外需要注意的是keystore文件一定要保管好丢了key基本上就等于这个渠道包废了没有任何办法能逆推出私钥内容。我在本地一般会建一个keystore.properties文件存放签名路径和别名但密码不会写在这个文件里而是通过环境变量注入这样打完的包即使别人拿到也没有什么安全风险。5. 从AAB到APK用bundletool在本地完成分发验证AAB本身不能直接安装到设备上你还需要通过bundletool把它转换成一个APK合集再从中取出对应设备配置的APK来安装。这一步在做灰度验证或渠道自测时非常有用。5.1 生成一套通用APK集如果你只想快速装到自己的测试手机上用通用模式生成一个universal APK就够了java -jar ~/tools/bundletool.jar build-apks \ --bundleapp-release-signed.aab \ --outputapp.apks \ --modeuniversal注意这里app.apks其实是一个zip格式的文件集合不是单一APK可以直接解压获取里面最大的那个universal APKunzip -o app.apks -d apks_output执行后你会看到apks_output/universal.apk这就是一个包含所有资源、代码、原生库的完整APK等价于传统打包方式打出来的单个APK。如果希望生成的APK集按照设备配置拆分比如只需要arm64-v8a架构和中文资源可以去掉--modeuniversal改成java -jar ~/tools/bundletool.jar build-apks \ --bundleapp-release-signed.aab \ --outputapp.apks生成后通过get-device-spec获取当前设备配置java -jar ~/tools/bundletool.jar get-device-spec \ --adbadb路径 \ --outputdevice-spec.json这个json文件记录了设备ABI、屏幕密度、SDK版本等信息。然后按设备配置提取APKjava -jar ~/tools/bundletool.jar extract-apks \ --apksapp.apks \ --device-specdevice-spec.json \ --output-dirextracted5.2 直接安装APK集到设备如果只是想快速安装到已连接的设备上可以跳过extract一步直接用install命令java -jar ~/tools/bundletool.jar install-apks \ --apksapp.apks \ --adbadb路径这个命令会自动读取连接设备的信息从APK集中挑选最匹配的APK配置并安装。我在平时测试动态功能模块时会经常用到因为它可以同时把base模块和feature模块都装上省去手动逐个安装的麻烦。这里的坑在于如果AAB还没有签名生成的APK也是未签名状态install时Android会拒绝安装。你会在adb日志里看到INSTALL_PARSE_FAILED_NO_CERTIFICATES。解决办法是先对AAB做签名再由bundletool生成APK集。如果AAB已经用jarsigner签过名bundletool生成的APK会自动把签名信息继承过来install时不需要再额外指定签名。6. 手动打包路上最容易踩的坑这个部分我总结一下自己这几年手动打AAB时遇到的高频问题每一条都是真实代价换来的。6.1 资源编译阶段崩溃aapt2 link阶段如果崩溃报错信息里通常有资源路径。最常见的情况是某个资源文件的格式损坏比如PNG文件被人为改成了jpg后缀或者vector drawable里有非法语法。我踩过一次最隐晦的坑Release包里的某个res/values/strings.xml含有未转义的单引号Debug构建因为没启用严格资源检查所以通过了Release开启shrinkResources后直接崩溃。排查方法很简单在项目里执行./gradlew :app:processReleaseResources --stacktrace把--stacktrace加上Gradle会打出资源编译的完整链路哪里崩了一目了然。6.2 构建工具版本和AGP不兼容如果你在命令行构建时报Minimum supported Gradle version is 7.5或The Android Gradle plugin requires Java 17这之类的话基本都是版本组合不对。我自己的处理习惯是不要试图把Gradle升到最新版而是看项目用的AGP版本支持的最高Gradle版本是多少。可以从AGP的官方发布说明里查到也可以直接在项目gradle/wrapper/gradle-wrapper.properties里看当前项目应用的Gradle版本只要那个版本不是太老就保持它不变。6.3 第三方SDK在AAB模式下多出问题很多依赖了SO库的SDK在APK时代一切正常换成AAB后却报库找不到。原因其实不难理解AAB模式下应用市场上架之后会按设备ABI拆分APK如果你的jniLibs目录里没有放全所有目标架构的SO库某些机型安装后被拆掉的APK里就没有那个ABI了。排查方法unzip -l app.aab | grep lib/看你的AAB里base/lib/下有哪些ABI目录。如果你只接了armeabi-v7a那到了arm64设备上系统还能用兼容模式跑但如果你连armeabi-v7a都没有那大概率会崩在UnsatisfiedLinkError。尽量把arm64-v8a、armeabi-v7a、x86_64至少这三种打全。6.4 版本名被工具改写有次我们打了一个AAB上传到市场后运营反馈版本号不对。查了半天发现是构建脚本里有人在打包后调用了bundletool build-apks而bundletool在生成APK集时会读取AAB里的版本信息但因为AAB的BundleConfig.pb里的版本号没同步更新导致最终安装包版本信息异常。后来我们的规范是AAB构建和后续转换工具链的版本号必须来自同一个来源不要在中间手工改任何配置文件。6.5 安装时的文件路径权限问题有些测试机会限制安装来自外部存储的APK。如果你通过bundletool install-apks安装时报错INSTALL_FAILED_INVALID_URI优先检查一下APK集文件是否放在了可读目录比如设备的/data/local/tmp下。使用adb直接推送adb push extracted/universal.apk /data/local/tmp/ adb shell pm install /data/local/tmp/universal.apk这个方法绕过了文件管理器权限限制在大部分设备上都能正常安装。结个尾我的建议是先把一条链路跑通如果你以前从没手动打过AAB我的建议是不要一开始就上生产签名先用bundleDebug跑通流程确认命令行环境没问题后再尝试Release和签名。别急着学那些花哨的bundletool高级用法把gradlew bundleRelease到jarsigner verify这条链路跑通你已经解决了工作中80%的打包问题。对我来说手动打包最大的价值不在于摆脱Android Studio而在于让构建过程变得可控制、可复现、可排查。当你哪一天接到一个为什么这个AAB在别人那边能装上在我这边装不上的bug单时你就会明白这些基础积累比任何花哨工具都管用。