ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

手动打包AAB完全指南:从环境配置到签名发布

手动打包AAB完全指南:从环境配置到签名发布 1. 为什么还要手动打包 AAB背景与适用场景1.1 AAB 到底是什么以及它和 APK 的本质区别先把我踩过的坑放在前面说清楚AABAndroid App Bundle不是一种可以直接装到手机上的安装包而是一种“提交给应用商店的发布格式”。它内部包含的是你的应用全部编译后的代码、资源和元数据但并不直接生成最终的 APK。Google Play 拿到 AAB 之后会根据用户的设备配置屏幕密度、CPU 架构、语言等动态生成对应的 APK这叫“分片 APK”Split APK。以前我们习惯打一个 fat APK里面塞了所有架构的 so 库、所有密度的图片资源装上之后体积感人。AAB 的思路是“按需交付”用户下载时只拿到自己设备需要的那部分。比如你的应用支持 arm64-v8a 和 armeabi-v7a用 APK 打包时两个 so 库都塞进去用 AAB 之后arm64 设备只下载 arm64 的 so资源同理。据 Google 官方口径AAB 方案平均能减少 20% 左右的应用下载体积这个数字在实践里我测到过 15% 到 35% 的波动取决于你的资源冗余程度。但是问题来了日常开发调试、内部分发、或者上架一些不支持 AAB 的应用市场时你还是需要 APK。所以 AAB 和 APK 不是替代关系而是“发布给 Google Play 用 AAB其他场景用 APK”的并行关系。手动打包 AAB 的场景主要有这几种项目构建脚本里没有集成打包 AAB 的任务需要临时出包给测试或上架。公司有持续集成CI环境但打包机上的 Android Studio 配置不完整需要纯命令行完成。你想绕过 Gradle 的一些自动化逻辑手动控制打包流程排查构建问题。从第三方渠道拿到的 AAB 需要做二次签名或者拆包验证。1.2 手动打包的适用范围什么时候该用什么时候别折腾说实话绝大多数情况下你不需要手动打包 AAB。Android Studio 里 Build Generate App Bundle or APK或者 Gralde 面板里的 bundleRelease 任务已经足够好用了。手动打包适合的是那些“工具链不全”但“手工活熟练”的场景或者是你在学习 AAB 内部结构时想彻底搞清楚每个步骤做了什么。我的建议是如果项目能用 Gradle 正常出包优先用 Gradle如果 Gradle 出了幺蛾子、或者你需要在没有完整 Android SDK 的机器上出包、又或者你想把 AAB 打包流程拆成可独立执行的脚本步骤那手动打包就非常有价值了。这也是我这篇文档的出发点把 AAB 打包的每一个环节拆开用命令行工具手动跑一遍让你既知道流水线上的每一步在干什么也能在自动构建挂掉的时候自己上手修。需要说明的是我下面所有操作都是在 macOS 环境下完成的Linux 和 Windows用 Git Bash 或 WSL的差异不大主要注意路径分隔符和可执行文件后缀Windows 下是 .bat 或 .exe。2. 打包前需要准备的工具链2.1 核心工具清单与版本建议手动打包 AAB本质上就是用一系列 Android SDK 和 Java 工具把编译产物按 AAB 目录规范组装起来。这里我给出一份我平时用的工具清单以及版本上的建议工具用途版本建议JDK运行所有 Java 工具的基础环境JDK 17 最佳AAB 格式要求最低 Java 8Android SDK Build-Tools提供 aapt2、apksigner、zipalign 等工具建议 33.0.0 及以上Android SDK Platform提供 android.jar用于编译资源建议 targetSdkVersion 对应版本bundletoolAAB 打包、解析、拆分 APK 的核心工具建议 1.15.0 及以上keytool / jarsignerJDK 自带用于生成密钥和签名JDK 17 自带的即可aapt2编译和链接资源Build-Tools 内附带这里需要特别提一下 bundletool它是 Google 官方提供的命令行工具专门用于操作 AAB 文件。它的功能覆盖了“从 AAB 到 APKS”APK Set 文件的完整链路。我之前遇到过一个坑早期版本的 bundletool 对 Java 版本要求很严格用 Java 8 可能跑不起来后来我统一升到 JDK 17 才算消停。2.2 环境变量配置与最小化 SDK 目录方案不废话直接说怎么配置环境变量。假设你的 Android SDK 装在~/Library/Android/sdkmacOS 默认路径那么需要把以下路径加入PATHexport ANDROID_HOME~/Library/Android/sdk export PATH$PATH:$ANDROID_HOME/platform-tools export PATH$PATH:$ANDROID_HOME/build-tools/33.0.2如果你手头没有完整 SDK又不想为了打一个包下载几百 MB 的 Android Studio可以只下载命令行工具commandline-tools。但是要注意build-tools 和 platform 这两个目录里的核心工具aapt2、apksigner、zipalign、android.jar是必需的缺了任何一个环节都过不去。我之前试过只装 build-tools 不装 platform编译资源的时候疯狂报错找不到 android.jar后来把 platform 补上才解决。检查工具是否就绪运行下面的命令确认版本号aapt2 version apksigner version zipalign -h java -version全部能正常输出版本信息说明基础环境过了。如果某个命令报“command not found”优先检查路径是否写对、是否已经执行了source ~/.zshrc或source ~/.bash_profile让配置生效。2.3 bundletool 的获取与安装bundletool 是一个单文件的 Java jar官方仓库在 GitHub 上发布下载地址和用法都比较稳定。你可以直接把 jar 放在一个固定目录比如~/tools/bundletool.jar然后给它配一个 shell 别名方便调用alias bundletooljava -jar ~/tools/bundletool.jar我习惯用这种方式而不是把它打进 PATH因为 bundletool 本质还是java -jar用别名更直观。如果你在 CI 环境里用直接全路径调用也完全没问题。3. AAB 打包全流程从零到一手动出包3.1 准备工作获取编译后的 classes.dex 和资源文件这一步很关键先要理清一个概念手动打包 AAB并不是从 Java/Kotlin 源码开始编译——那活太大了有 javac、kotlinc、混淆、desugar 一堆环节手动搞一遍纯属自虐。我的做法是用 Gradle 把项目编译成中间产物然后停在这些产物上再用命令行工具把它们手动组装、签名成 AAB。所以第一步先在项目目录里用 Gradle 执行assembleRelease或bundleRelease把编译产物尤其是 dex 和资源准备好。如果你用的是assembleRelease产物里会有一个没有签名的 APK 文件里面有classes.dex和resources.arsc这些就是我们要的东西。如果你用了bundleRelease产物里直接有一个 .aab 文件但那已经算“半自动”了不是纯手动。我建议用assembleRelease的这个产物来当原料。具体来说编译后你需要在app/build/intermediates/dex/release/找到classes.dex或者多个 dex 文件在app/build/intermediates/merged_res/release/找到合并后的资源在app/build/intermediates/merged_manifests/release/找到合并后的 AndroidManifest.xml。不同 AGP 版本路径可能有差异如果你找不到直接在app/build/intermediates下搜*.dex和AndroidManifest.xml一般都能定位。3.2 构建 AAB 目录结构这一步是手动打包的核心AAB 本质上是一个 ZIP 包但后缀是 .aab内部必须按 Google 规定的目录结构组织。我先给你看一个标准的 AAB 内部结构长啥样myapp.aab ├── base/ # 基础模块必须有 │ ├── manifest/ │ │ └── AndroidManifest.xml │ ├── dex/ # dex 文件 │ │ ├── classes.dex │ │ └── classes2.dex │ ├── res/ # 编译后的二进制资源 │ ├── root/ # 应用根部文件如 assets 等 │ └── assets/ # 资源文件 ├── BUNDLE-METADATA/ # 元数据可选 └── META-INF/ # 签名信息注意这里面的AndroidManifest.xml不是文本文件而是 aapt2 编译后的二进制 XML。资源文件res/目录里的也都是编译后的二进制资源。所以你不能直接把源码里的 XML 复制进去必须先经过 aapt2 编译链接。这点非常容易踩坑我第一次手动打包时直接把纯文本 XML 放进去结果 bundletool 解析直接报错。3.3 使用 aapt2 编译并链接资源aapt2 的用法分两步compile 和 link。第 1 步编译单个资源文件aapt2 compile --dir res -o compiled_res.zip这条命令会把整个res目录里所有资源编译到一个 zip 文件里。--dir参数指定资源目录路径-o指定输出文件。如果你只想编译某一个文件也可以直接指向具体文件aapt2 compile -o compiled/ app/src/main/res/drawable/icon.png第 2 步链接资源并生成 android.jar 之外的包aapt2 link -o base-res.apk \ -I $ANDROID_HOME/platforms/android-34/android.jar \ --manifest AndroidManifest.xml \ --min-sdk-version 24 \ --target-sdk-version 34 \ compiled_res.zip这一步要做几件事把编译后的资源链接成base-res.apk也是一个中间产物同时生成或合并资源表。其中-I指向 android.jar用于解析系统资源引用--manifest指向你的 AndroidManifest.xml--min-sdk-version和--target-sdk-version必须和项目的 build.gradle 保持一致否则后面 bundletool 生成 APKS 时会报版本不匹配的错。链接完成后base-res.apk里会包含resources.arsc和编译后的资源文件这些就是我们 AAB 里base/res/目录的来源。3.4 组装目录并压缩为 AAB这一步需要非常细心。把上一步链接产物、dex 文件、AndroidManifest.xml 按 AAB 规范放到对应目录里然后用 zip 命令打包。我通常先建一个临时目录比如aab_tmp/然后按以下步骤组织mkdir -p aab_tmp/base/manifest aab_tmp/base/dex aab_tmp/base/res # 1. 把编译后的 Manifest 放入 cp app/build/intermediates/merged_manifests/release/AndroidManifest.xml aab_tmp/base/manifest/ # 2. 把 dex 放入 cp app/build/intermediates/dex/release/classes*.dex aab_tmp/base/dex/ # 3. 把链接生成的资源放入 unzip -o base-res.apk -d aab_tmp/base/res/注意第 3 步直接把 base-res.apk 当 zip 解压到base/res/目录下就行。此时aab_tmp/base/下面应该有 manifest、dex、res 三个子目录和对应的文件。有时候还要把assets目录放进去如果你项目里有assets记得在aab_tmp/base/assets/下放好。然后压缩成 AABcd aab_tmp zip -r ../myapp.aab . -x .* -x __MACOSX关键一点压缩的时候要在aab_tmp目录内部执行确保 zip 包的根目录是base/、META-INF/这些而不是外层多包一层aab_tmp/。我见过有人直接在工程目录下执行zip -r myapp.aab aab_tmp/*结果 AAB 里所有路径都多了个aab_tmp/前缀bundletool 直接拒绝解析。3.5 签名 AAB 文件AAB 和 APK 一样需要签名而且签名用的工具也差不多。我习惯用apksigner它是 Android 官方推荐的签名工具支持 v1、v2、v3 签名方案对 AAB 同样适用。先生成密钥如果已有密钥库就跳过这一步keytool -genkeypair -v -keystore my-release.keystore \ -alias my-alias -keyalg RSA -keysize 2048 -validity 3650然后用 apksigner 签名apksigner sign --ks my-release.keystore --ks-key-alias my-alias \ --ks-pass pass:你的密码 --key-pass pass:你的密钥密码 \ --out myapp-signed.aab myapp.aab如果密钥库里的密钥密码和 keystore 密码一样可以省略--key-pass。签名完后用apksigner verify验证一下apksigner verify --verbose myapp-signed.aab这里有个容易踩的坑如果你之前用jarsigner给 AAB 签过名后来再用apksigner重新签可能会报“包含已有签名”的错误需要先删除 META-INF 里的旧签名文件再签。3.6 使用 bundletool 生成 APKS 并安装测试签名之后AAB 文件理论上已经可以提交到 Google Play 了。但为了验证 AAB 内部结构是否正确、能不能正常拆分成设备可安装的 APK我建议先用 bundletool 生成 APKS 并用本地模拟器或真机测一下。生成 APKSbundletool build-apks --bundlemyapp-signed.aab \ --outputmyapp.apks \ --ksmy-release.keystore --ks-key-aliasmy-alias \ --ks-passpass:你的密码注意这里--ks参数是可选的如果不传bundletool 会生成 debug 签名 APKS。但既然我们前面已经签过 AAB 了这里推荐传入同一套签名确保从拆分到安装的签名一致。APKS 其实也是一个 zip 包里面包含了各种设备配置对应的 APK 文件。如果想直接安装到连接的设备上用bundletool install-apks --apksmyapp.apks如果你在真机上安装后打开发现正常的那整个手动打包流程就验证通过了。可以用 Android Studio 里的 APK Analyzer 打开这个 AABAndroid Studio 是支持直接打开 AAB 的检查基础结构是否合理资源是否完整。4. AAB 打包常见问题与排查技巧实录4.1 问题速查表手动打包不像 Gradle 一键出包那么“无脑”遇到问题经常要自己定位。我把平时遇到的典型问题列个表按“症状→原因→处理方式”的形式给你参考症状常见原因处理方式bundletool 报 “No base module found”aab 内部缺少 base 目录或目录名不对解开 aab 检查根目录是否有 base/安装到设备报 “INSTALL_PARSE_FAILED_NO_CERTIFICATES”AAB 签名没做或签名被后处理覆盖用 apksigner verify 检查签名状态aapt2 link 报 “resource directory does not exist”编译和链接的资源路径不一致确保 compiled 目录下所有资源都正确编译打出的 AAB 在 Play Console 上传被拒提示签名太弱使用了 RSA 1024 或 SHA1 签名用 RSA 2048 SHA256 重新签名APKS 生成时报 “device-spec” 参数不匹配bundletool 版本或 --min-sdk 参数不对检查 --min-sdk-version 和 targetSdkVersion 的一致性zip 解压后多了一层目录bundletool 解析失败打包时在项目根目录执行 zip导致外层多了一层进入 aab 临时目录内部再执行 zip用 jarsigner 签过名之后再 apksigner 签名失败两者签名机制冲突解压删除 META-INF 下的旧签名文件再使用 apksigner4.2 资源编译不通过的心路历程我在手动打包过程中遇到最头大的问题就是 aapt2 link 阶段偶尔报 “resource … not found”。一开始想不通因为 Gradle 自动构建完全没问题。后来排查发现问题出在“编译资源”和“链接资源”不是从同一个目录出来的。aapt2 compile --dir res编译了 res 下所有资源如果其中某个 res 子目录下有隐藏文件或文件格式异常编译时可能没报错但链接时就找不到对应的 R 类条目。解决办法是把编译后的 zip 解开检查每个资源是否都生成了 .flat 文件aapt2 编译后的二进制格式缺失的文件单独重新 compile。还有一次我用的 Build-Tools 是 30.0.3链接时一直报unknown option: --target-sdk-version当时愣了一下才反应过来aapt2 的--target-sdk-version参数是 31 版本才加入的。后来把 Build-Tools 升到 33.0.0问题消失。如果你也碰到类似参数不识别的问题优先确认工具链版本。4.3 安装失败INSTALL_FAILED_MISSING_SPLIT 这类错误怎么定位有时候 bundletool 生成 APKS 并安装后打开应用会闪退logcat 里能看到INSTALL_FAILED_MISSING_SPLIT类似的关键字。这个错误的意思是设备上安装的 APK 集合里缺少某个必要的 split 模块。比如 AAB 里定义了多个模块但生成 APKS 时因为某模块条件不满足被跳过了而应用代码又强制引用了这个模块的资源。这种问题多半是基础模块配置不对或者资源分片条件写得太激进。检查方式有两个方向一是用bundletool get-device-spec确认设备配置再看 APKS 里的 split APK 列表二是回头检查 AAB 的 base 模块是否完整manifest、dex、res 都不能缺。如果 base 模块里缺少关键资源拆出来的 APK 就是“残缺”的装上去自然闪退。4.4 调试技巧在一个环境里同时保留 Gradle 产物和手动产物我在实践里养成了一个好习惯手动打包时绝不直接修改 Gradle 编译产物所在的目录而是把它们复制到独立的临时目录再做后续操作。这样一旦手动流程出问题可以随时回到“干净”的 Gradle 产物状态重新来不用重新编译半分钟再花一分钟排查。另一个调试技巧是手动打包过程中每个关键步骤都产生一个中间文件。比如 aapt2 的base-res.apk、解压后的base/res/目录、签名前的myapp-unsigned.aab这些文件都放在一个带时间戳的目录下方便回溯。我经历过“前面第 3 步资源路径错了后面第 5 步签名却成功但是 APKS 安装不上”的情况最后就是靠时间戳目录往回查很快定位到了问题在第 3 步因为 base-res.apk 里缺少了一张 drawable。5. 实操记录一次从零开始的手动打包 AAB 全过程5.1 我的环境与项目参数为了让记录有参考价值我先交代一下当时的环境macOS 12.6JDK 17.0.2Android SDK Build-Tools 33.0.2bundletool 1.15.0targetSdk 34minSdk 24。项目是一个普通的多模块 Android 工程一个 app 模块 一个 library 模块里面有图片资源、一些 so 库、assets 目录。整体不算复杂但足够覆盖手动打包流程中的大部分环节。5.2 分步骤记录每个命令和输出Step 1编译项目获取中间产物在项目根目录执行./gradlew :app:assembleRelease这一步的目的是拿到编译好的 dex、资源、manifest。静默构建成功后我在app/build/intermediates/下面验证了一下产物app/build/intermediates/dex/release/下有classes.dex、classes2.dex和classes3.dex因为用了 multidex。app/build/intermediates/merged_manifests/release/下有AndroidManifest.xml。app/build/intermediates/merged_res/release/下有合并后的资源目录。Step 2创建临时目录并拷贝产物mkdir -p ~/manual_aab/base/manifest mkdir -p ~/manual_aab/base/dex mkdir -p ~/manual_aab/base/res mkdir -p ~/manual_aab/base/root cp app/build/intermediates/merged_manifests/release/AndroidManifest.xml ~/manual_aab/base/manifest/ cp app/build/intermediates/dex/release/classes*.dex ~/manual_aab/base/dex/Step 3用 aapt2 编译并链接资源aapt2 compile --dir app/build/intermediates/merged_res/release -o ~/manual_aab/compiled_res.zip aapt2 link -o ~/manual_aab/base-res.apk \ -I $ANDROID_HOME/platforms/android-34/android.jar \ --manifest ~/manual_aab/base/manifest/AndroidManifest.xml \ --min-sdk-version 24 --target-sdk-version 34 \ ~/manual_aab/compiled_res.zip这里有个细节需要注意链接时用的AndroidManifest.xml是合并后的“二进制 XML”而不是源码里那个纯文本 XML。如果你用了纯文本 XMLaapt2 会报错提示binary XML file line #N: invalid element之类的。Step 4把链接产物解压到 base/resunzip -o ~/manual_aab/base-res.apk -d ~/manual_aab/base/res/这一步过后检查了下~/manual_aab/base/res/能看到res/下的子目录drawable、layout、mipmap 等和根目录的resources.arsc。如果这里发现缺少某个资源目录说明第 3 步编译的 res 不完整需要回头检查。Step 5拷贝 assets如果有我的项目里有assets/目录所以把 Gradle 中间产物里的 assets 整个拷过来cp -R app/build/intermediates/assets/release/* ~/manual_aab/base/assets/如果你的项目里没有 assets这步跳过。Step 6生成 AAB未签名cd ~/manual_aab zip -r ~/myapp-unsigned.aab . -x .* -x __MACOSX注意cd进目录再压缩这一步我们前面强调过。用unzip -l确认一下包内路径应该能看到base/manifest/AndroidManifest.xml、base/dex/classes.dex、base/res/resources.arsc等文件路径。Step 7签名keytool -genkeypair -v -keystore ~/my-release.keystore -alias myalias \ -keyalg RSA -keysize 2048 -validity 3650 apksigner sign --ks ~/my-release.keystore --ks-key-alias myalias \ --ks-pass pass:123456 --out ~/myapp-signed.aab ~/myapp-unsigned.aab apksigner verify --verbose ~/myapp-signed.aabStep 8用 bundletool 生成 APKS 并安装验证java -jar ~/tools/bundletool.jar build-apks \ --bundle~/myapp-signed.aab --output~/myapp.apks java -jar ~/tools/bundletool.jar install-apks --apks~/myapp.apks安装完成后打开应用功能正常整个流程走通。5.3 过程中的“意外”与处理方式这次实操最意外的点出现在第 4 步和第 5 步之间解压base-res.apk到base/res/之后我发现resources.arsc确实在解压后的根目录但是 mipmap 目录里少了一张启动图标。排查下来问题不在 aapt2而在 Gradle 的merged_res路径下这个图标是以“overlay”方式存在另一个目录中的aapt2 编译时没有把它编译进去。解决方式是在编译前先把 merge 后的res目录完整地复制到临时目录再对这个副本做 aapt2 compile避免对原始中间产物的误判。这类问题很少遇到但一旦遇到很容易让人以为是 aapt2 的锅。经验是资源缺失时先不要怀疑工具先把最终的resources.arsc里有没有对应条目检查一下用 aapt2 dump 可以看如果没有再回到资源编译环节找原因。6. 签名与安全性手动打包最容易忽略的一环6.1 签名算法与密钥管理建议很多开发者觉得手动打包和 Gradle 自动打包的核心差别只是“命令多敲了几条”但签名环节的坑比想象中多。AAB 签名如果做不对后续上架 Google Play 会被直接打回连检查都过不了。我给的签名建议是统一用apksignerRSA 2048 密钥长度签名算法选 SHA256withRSA。jarsigner属于老古董了虽然还能用但它只支持 v1 签名方案AAB 在 Google Play 上架时会要求 v2 或更高所以直接放弃 jarsigner 最省心。密钥库文件一定要用独立密码保护并且和代码仓库分离存放。我的习惯是把.keystore文件放在构建机的外部存储目录CI 里通过环境变量注入密码而不是提交到 Git。这个习惯在手动打包流程里同样适用——不要为了图方便把密码写进脚本文本里。6.2 签名验证的几层检查签名做完之后我用三层检查确保万无一失第一层是工具层验证跑apksigner verify --verbose看输出里有无 error。第二层是结构层验证用unzip -p myapp-signed.aab META-INF/MANIFEST.MF查看签名摘要文件确认里面确实记录了每个条目尤其是 dex 和 resources.arsc 的 SHA256 摘要。第三层是运行时验证把 APKS 安装到真机上看应用能否正常安装、打开、读写资源。这三层全过了才敢说这个 AAB 能交付出去了。6.3 签名异常时的恢复技巧有一类很讨厌的情况是AAB 已经签好名了但后续为了调试又改了内部某个文件比如替换了一个资源导致签名失效。这时候很多人的第一反应是重新生成 AAB 再签一遍但其实可以更快地修复# 解开 AAB mkdir -p fix_tmp cd fix_tmp unzip -q ../myapp-signed.aab # 修改你需要改的文件比如替换一个资源 # 重新压缩注意仍然要在 fix_tmp 内部执行 zip -r ../myapp-fixed.aab . # 重新签名 apksigner sign --ks my-release.keystore --ks-key-alias myalias \ --ks-pass pass:123456 --out ../myapp-fixed-signed.aab ../myapp-fixed.aab这里有个细节重新压缩后 AAB 里可能残留旧的 META-INF 签名文件apksigner 会拒绝覆盖已经存在的签名。所以压缩前先删掉旧的 META-INFrm -rf META-INF但我不建议频繁走这种“拆开-修改-再打包”的流程因为手改 AAB 内部结构容易弄坏格式尤其是resources.arsc这种二进制文件手动改了最后肯定出问题。能不改内部结构就别改。7. AAB 体积优化与实践建议7.1 拆分资源、动态交付背后的体积收益说实话AAB 手动打包最大的价值不在于“你能手动出包”而在于你通过手动出包理解了 AAB 背后的“按需交付”逻辑从而能在项目里去优化体积。我举一个我们项目的例子之前 release APK 是 48 MB改成 AAB 后在 Google Play 的分片下载大约是 33 MB。原因是项目里有大量 drawable 资源之前全塞进 APK而 AAB 环境下高密度设备不会下载低密度版本。如果你想知道当前项目的 AAB 能省多少可以用 bundletool 提供的build-apks --modeuniversal生成一个包含全部分片的 APKS对比单 APK 的体积就一目了然了。7.2 压缩建议哪些资源可以动哪些不能碰在手动处理 AAB 内部资源时我总结了一些经验res/目录下的图片和布局可以用 aapt2 优化过的二进制格式体积会有一定下降不要手动改。assets/目录里的文件是原样打包的可以用编译脚本比如 webpack 或图片压缩提前缩小AAB 不会帮你压缩。resources.arsc是二进制资源表别尝试手动编辑它里面是紧凑编码改一个字节都可能让整个表解析失败。so 库建议用 ABI 拆分的方式放到 AAB 的base/lib下这样不同架构的设备只会下载对应 so。这个策略在手动打包时也能做只要把对应 so 文件放到base/lib/arm64-v8a/、base/lib/armeabi-v7a/目录里即可。7.3 会不会影响现有 CI 流程如何衔接手动打包听起来像是“手工活”但完全可以脚本化后接入 CI。我是这样干的把第 3 节里的命令全部写进一个build_aab.sh脚本输入参数是项目路径、签名文件路径、密码输出是签名好的 AAB。这样本地手动执行、CI 远程执行都一样整个流程可重复、可追溯。脚本里需要注意一个点因为手动打包依赖 Gradle 的中间产物所以脚本必须保证 Gradle 任务在脚本最前面执行并且在脚本里加一句“产物完整性检查”比如判断 dex 目录是否非空避免 Gradle 执行失败后脚本继续往下跑最终打出一个坏包。8. 最后的小技巧与避坑总结8.1 三个非常容易踩的坑每个都值得单独说第一个坑是 AAB 内部目录结构大小写问题。严格来说AAB 的模块名必须是小写字母base就是base不能写成Base或BASE。我见过有人改了目录名结果 bundletool 报错找模块花了好久才定位。如果你要加动态功能模块模块名必须和 build.gradle 里的dynamicFeatures配置一致且模块目录名小写。第二个坑是resources.arsc的位置。它在 AAB 里的路径必须是base/res/resources.arsc而不是base/resources.arsc。如果放错位置AAB 能打出来但拆分成 APKS 时必然报错提示资源表缺失。我第一次手动打包就栽在这里后来看了官方格式说明才明白资源表就是跟着base/res/走的不是跟根目录。第三个坑是重复签名导致的嵌套签名。如果你先手动用 apksigner 签了一次后来又在某个环节调用了apksigner或jarsigner再次签名AAB 的 META-INF 里可能会出现两个签名单元。这种“二次签名”的文件在本地安装可能没事但上传到 Google Play 会被判定为签名异常。解决思路每次签名前干脆删掉旧的 META-INF保证只有一轮签名。8.2 手动打包的价值和后续演进思路如果说 Gradle 的bundleRelease是一台全自动咖啡机那手动打包就是手冲咖啡步骤多、讲究多但每一环节你都知道在干什么出了问题能精准定位。我个人的看法是除非你要深入排查 AAB 结构问题、或者构建环境受限否则日常开发别手撸 AAB有那时间多写几个单元测试更划算。但作为一项“应急技能”和“理解工具”手动打包值得每个 Android 构建负责人掌握。后续如果你有兴趣可以顺着这几个方向继续研究用 bundletool 的get-device-spec和build-apks配合做不同设备的体积预诊断。把脚本和 CI 流程整合实现“提交代码 → 自动出 AAB → 自动拆 APKS → 自动发到内部测试群”的完整链路。研究 AAB 内的BUNDLE-METADATA目录里面可以放一些构建元信息比如 Git commit、构建时间帮助出包后回溯。我现在手里的项目已经把“手动打包 AAB”这个操作固化成了独立脚本平时几乎不用碰但每次构建系统的 Gradle 版本升级、或者打出的包有问题我都会把脚本翻出来跑一遍对照每一步的输出去排查是编译问题、资源问题还是签名问题。这套流程帮我在多次构建事故中快速恢复了出包能力这也是我写这篇文档的初衷。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进