三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Android开发必备:bundletool工具详解与实战指南

Android开发必备:bundletool工具详解与实战指南

1. 从APK到AAB:为什么我们需要bundletool

如果你是一名Android开发者,最近几年肯定没少和AAB(Android App Bundle)打交道。从2021年8月开始,Google Play要求所有新应用都必须以AAB格式提交,这标志着APK作为直接发布格式的时代在官方商店正式落幕。但随之而来的问题是:我们本地测试、内部渠道分发、或者需要分析AAB包内容时,该怎么办?总不能每次都上传到Play Console再下载测试包吧?

这时候,bundletool就登场了。它不是Google Play商店的后台工具,而是一个由Google官方提供的、运行在你本地命令行中的Java工具链。它的核心价值,就是充当连接“开发环境”与“AAB分发生态”的桥梁。简单来说,bundletool让你能在自己的电脑上,完成Google Play服务器对AAB包所做的大部分处理工作,比如:将单个AAB文件转换成针对特定设备配置的APK文件集(我们称之为APK Set, 后缀为.apks),或者直接安装到连接的设备上。

我刚开始接触AAB时,最不习惯的就是不能像以前那样,直接adb install一个app-release.apk就完事了。每次构建出.aab文件后,想要给测试同事装包,或者自己跑一下多ABI(应用二进制接口)的兼容性测试,都得绕个弯子。bundletool正是解决这个“绕弯子”痛点的利器。它让你在本地就能掌控AAB的“分裂”过程,看清里面到底有什么,并精准地部署到目标设备上。这对于持续集成(CI)流程、自动化测试以及深度优化包体大小都至关重要。

2. bundletool核心功能拆解:不止是“转换”那么简单

很多人把bundletool简单理解为一个“AAB转APK”的工具,这其实大大低估了它的能力。它的功能模块设计紧密围绕着AAB格式的核心理念:动态交付。下面我们来拆解它的几个核心命令,你会发现它更像一个AAB的“瑞士军刀”。

2.1build-apks:构建APK集合的“总控台”

这是你最常用到的命令。它的作用是根据你的.aab文件和一系列配置参数,生成一个.apks文件。这个.apks文件不是一个单一的APK,而是一个ZIP压缩包,里面包含了针对不同设备配置“量身定制”的一套APK文件。

java -jar bundletool.jar build-apks \ --bundle=/path/to/your-app.aab \ --output=/path/to/your-app.apks \ --ks=/path/to/keystore.jks \ --ks-pass=pass:your-keystore-password \ --ks-key-alias=your-key-alias \ --key-pass=pass:your-key-password

关键参数解析:

  • --bundle: 输入的AAB文件路径。这是原料。
  • --output: 输出的APK集合文件路径。这是成品。
  • --ks,--ks-pass,--ks-key-alias,--key-pass: 签名信息。这是最容易踩坑的地方!用于生成APK集的签名必须与最终上传到Google Play的签名证书一致。如果你用了一个调试签名来生成.apks,然后试图用这个.apks来更新一个由正式签名安装的App,会导致安装失败。在CI环境中,务必妥善管理你的正式签名文件。
  • --mode: 运行模式。这是build-apks的灵魂参数。
    • universal(默认): 生成一个“通用APK”,包含了所有语言、屏幕密度、ABI的代码和资源。这个APK会很大,但兼容所有设备。主要用于本地调试和兼容性检查,因为它失去了AAB动态交付减小包体的优势。
    • default: 生成标准的APK集合,会根据设备配置进行拆分。这是最常用的模式,模拟了Google Play的真实分发行为。
    • system: 生成不包含分割APK(split APKs)的APK集合,用于系统镜像烧录等场景。
    • persistent: 类似system,但包含应用在系统分区中运行所需的特定功能模块。

一个实用的技巧:你可以通过--device-spec参数来为特定的设备配置生成APK集。这在你需要为某个特定型号的设备(比如只支持armeabi-v7a的旧设备)准备测试包时非常有用。你需要先通过bundletool get-device-spec命令获取该设备的规格JSON文件。

2.2install-apks:精准安装到目标设备

生成了.apks文件后,下一步就是安装。install-apks命令会做一件很聪明的事:它连接到你指定的设备(通过--device-id指定,或默认连接当前唯一设备),读取该设备的详细规格(如屏幕密度、ABI、语言),然后从.apks文件这个“大仓库”里,精准挑选出适合这台设备的、最小的一组APK(基础APK + 对应的配置APK),并通过ADB进行安装。

java -jar bundletool.jar install-apks --apks=/path/to/your-app.apks

这个过程完全模拟了用户从Google Play下载安装你的应用时的体验。对于测试来说,这比安装一个庞大的“通用APK”要真实得多,因为你能真正测试到动态交付的资源(比如特定分辨率的图片)是否正确加载。

2.3extract-apks:手动“拆包”查看内容

有时候你并不想安装,只是想看看这个.apks文件里到底包含了哪些APK,或者想手动提取出某个特定配置的APK。extract-apks命令就是干这个的。

java -jar bundletool.jar extract-apks \ --apks=/path/to/your-app.apks \ --output-dir=/path/to/extracted-apks \ --device-spec=/path/to/device-spec.json

执行后,它会在你指定的输出目录里,解压出针对device-spec.json所描述的设备所需的所有APK文件。你可以清晰地看到:base-master.apk(基础代码和资源),base-zh.apk(中文语言资源),base-xxhdpi.apk(xxhdpi密度资源),base-armeabi-v7a.apk(对应ABI的本地库)等等。这对于分析包体构成、验证资源拆分是否正确极其有帮助。

2.4get-device-spec&get-size:获取设备信息与估算大小

  • get-device-spec: 这个命令输出一个JSON文件,详细描述了当前连接设备的特性,包括支持的ABI列表、屏幕密度、语言区域等。这个文件可以作为build-apksextract-apks命令中--device-spec参数的输入,实现针对性的操作。
    java -jar bundletool.jar get-device-spec --output=/path/to/device-spec.json
  • get-size: 这个命令用于估算你的应用在Google Play上针对不同设备配置的下载大小和安装大小。它提供了两种输出:
    • --human-readable: 输出易于阅读的摘要。
    • --json: 输出详细的JSON数据,包含每种配置的详细分析。 这个功能在优化包体大小阶段非常关键,你可以直观地看到启用资源压缩、不必要语言/密度资源移除等措施带来的收益。

3. 实战工作流:将bundletool集成到日常开发与CI中

理解了核心命令后,我们来看看如何把它用起来,打造一个流畅的本地测试和自动化构建流程。

3.1 本地快速测试流程

假设你刚用Android Studio构建了一个app-release.aab文件,想快速在连接的手机上安装测试。

步骤一:生成针对当前设备的APK集与其生成通用的universalAPK,不如生成一个精准匹配你测试手机的APK集,这样更贴近真实场景。

# 1. 获取当前设备的规格 java -jar bundletool.jar get-device-spec --output=device-spec.json # 2. 根据此规格生成APK集 java -jar bundletool.jar build-apks \ --bundle=app-release.aab \ --output=app-release.apks \ --ks=my-release-key.jks \ --ks-pass=pass:123456 \ --ks-key-alias=my-alias \ --key-pass=pass:123456 \ --device-spec=device-spec.json # 这里没有指定--mode,默认就是`default`,即标准拆分模式

步骤二:安装

java -jar bundletool.jar install-apks --apks=app-release.apks

这个过程可以封装成一个简单的Shell脚本或Makefile任务,一键完成。

3.2 CI/CD流水线集成

在持续集成环境中(如Jenkins, GitLab CI, GitHub Actions),bundletool通常用于生成可供测试的APK集,或者进行包体大小监控。

场景:每次合并请求(Merge Request)都生成一个可安装的APK集供测试人员下载。

  1. 构建AAB:CI任务首先编译生成AAB文件。
  2. 生成通用APK集(用于直接下载安装)
    java -jar bundletool.jar build-apks \ --bundle=app.aab \ --output=app-universal.apks \ --mode=universal \ ... # 签名参数
    因为测试人员设备各异,生成一个universalAPK最省事。虽然大,但保证都能安装。
  3. .apks文件转换为可直接安装的.apk.apks是压缩包,不方便直接安装。我们可以解压出里面的通用APK。
    # 解压.apks文件(它就是个zip) unzip app-universal.apks -d extracted_apks # 通常通用APK名为 `toc.pb` 同目录下的某个apk,或直接寻找最大的那个apk文件 # 更规范的做法是使用`bundletool extract-apks`命令指定universal模式提取 java -jar bundletool.jar extract-apks \ --apks=app-universal.apks \ --output-dir=extracted_apks \ --device-spec=universal.json # 需要一个表示“通用”的设备规格文件,可以手动创建或使用bundletool内置逻辑
    实际上,更常见的CI做法是直接构建一个universal.apk用于测试分发,这可以通过Gradle的assemble任务或bundletooluniversal模式实现。
  4. 上传产物:将生成的.apk文件作为CI流水线的构建产物(Artifact)提供下载。

场景:包体大小监控与门禁在CI中集成包体大小检查,防止某次提交意外引入大体积资源导致包尺寸暴涨。

# 生成大小报告JSON java -jar bundletool.jar get-size total \ --apks=app.apks \ --json \ --output=size-report.json # 使用jq等工具解析JSON,提取关键指标,如“针对en-US, xxhdpi, arm64-v8a设备的下载大小” DOWNLOAD_SIZE=$(cat size-report.json | jq '.configs[] | select(.locale=="en-US") | select(.density=="XXHDPI") | select(.abi=="ARM64_V8A") | .downloadSize') # 设置阈值,如果超过阈值则CI失败或发出警告 if [ $DOWNLOAD_SIZE -gt 100000000 ]; then # 假设阈值100MB echo "错误:预估下载大小超过100MB!" exit 1 fi

4. 进阶技巧与深度避坑指南

用了这么久bundletool,我也积累了一些教科书里不会写的经验和坑点。

4.1 签名管理:混乱的来源

问题INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES安装失败,提示证书不一致。

根因与解决:这是bundletool使用中最常见的问题。关键在于理解“签名链”。

  1. 上传到Google Play的AAB,必须用你应用的正式发布签名密钥签名。
  2. bundletool build-apks生成.apks,使用的签名必须与第1步的签名一致,或者与设备上已安装版本的签名一致。
  3. 如果你在本地调试,设备上安装的是Android Studio用调试签名(debug.keystore)打的APK,那么你用正式签名生成的.apks就无法安装(覆盖)它。
  4. 最佳实践
    • 本地调试:专门为调试创建一个debug.aab的构建变体(Build Variant),并使用调试签名。bundletool也使用调试签名来操作它。
    • 测试分发:在CI中,使用与Play Store相同的正式签名来生成所有测试包。确保测试团队始终安装由同一签名签名的包。
    • 使用--overwrite参数:在install-apks时添加--overwrite,有时可以强制覆盖,但并非万能,尤其是签名算法变更时。

4.2 资源匹配与“找不到资源”问题

问题:应用在特定设备上运行时,出现Resources$NotFoundException,但在其他设备或通用APK上正常。

根因与解决:这通常是AAB资源拆分和动态交付的“副作用”。你的代码可能访问了一个在当前设备配置下被拆分开、但代码路径假设其一定存在的资源。

  1. 检查资源限定符:确保你访问的资源(如图片R.drawable.foo)在所有必要的配置目录下都有定义,或者有兜底的默认资源(放在不带限定符的目录,如drawable/而非drawable-xxhdpi/)。
  2. 使用bundletool extract-apks诊断:针对出问题的设备规格生成设备规格文件,然后用extract-apks命令解压出该设备会收到的APK集。检查这些APK中是否包含你出问题的资源。用aapt2或解压APK后直接查看资源目录。
  3. 小心代码中的资源引用:避免在运行时通过字符串硬编码拼接资源名(如getResources().getIdentifier("icon_" + type, "drawable", ...))。这种动态查找在资源被拆分后可能失败。尽量使用静态的R.引用。

4.3 本地库(Native Library)的拆分与兼容性

AAB会根据设备的ABI(如armeabi-v7a, arm64-v8a, x86等)拆分本地库(.so文件)。这带来了包体节省,但也带来了复杂性。

问题:应用在混合ABI环境(如ARM芯片的Chromebook运行x86应用)或通过模拟器安装时崩溃。

排查

  1. 使用bundletool get-device-spec确认设备报告的ABI列表。
  2. 使用extract-apks查看针对该设备,是否包含了正确的本地库APK(如base-arm64-v8a.apk)。
  3. build.gradle中检查ndk.abiFilters设置。如果你过滤掉了某些ABI,那么即使设备支持,也不会生成对应的库文件。确保你的过滤策略符合目标设备范围。
  4. 模拟器特别注意:很多x86/64的模拟器支持ARM二进制翻译(如Intel Houdini)。但通过bundletool安装时,它可能只会选择x86的本地库APK。如果应用强依赖某些ARM-only的库,可能会出问题。测试时,最好使用对应ABI的真实设备或模拟器。

4.4 版本管理与回滚

.apks文件本身不包含版本信息,它依赖于内部APK的版本码。在CI中管理多个版本的测试包时,容易混乱。

建议:在生成.apks文件时,将版本号包含在文件名中,并配套生成一个简单的元数据文件。

VERSION_CODE=$(cat app-version-code.txt) # 从构建脚本中获取版本码 OUTPUT_NAME="myapp-v${VERSION_CODE}.apks" java -jar bundletool.jar build-apks ... --output="${OUTPUT_NAME}" # 同时生成一个包含版本码、构建时间、Git提交哈希的info.txt文件 echo "VersionCode: ${VERSION_CODE}" > ${OUTPUT_NAME}.info.txt echo "BuildTime: $(date)" >> ${OUTPUT_NAME}.info.txt echo "Commit: ${GIT_COMMIT_HASH}" >> ${OUTPUT_NAME}.info.txt

5. 超越基础:用bundletool进行深度分析与优化

bundletool不仅是构建和安装工具,更是分析和优化AAB的利器。

5.1 分析AAB内容结构

使用bundletool dump命令可以以纯文本或JSON格式输出AAB包的内部清单,包括模块、资源、清单文件、DEX文件等详细信息。

java -jar bundletool.jar dump bundle --bundle=app.aab

这个输出非常详细,可以帮助你理解模块化应用的结构,检查是否有多余的资源或代码被包含在了基础模块中。

5.2 验证AAB的完整性

在将AAB上传到Play Console之前,可以用bundletool validate命令进行预检。

java -jar bundletool.jar validate --bundle=app.aab

它会检查一些常见问题,比如清单文件中的版本设置、模块依赖关系等,提前发现潜在的上传失败风险。

5.3 模拟Play商店的动态功能模块交付

如果你的应用使用了动态功能模块(Dynamic Feature Module),bundletool可以模拟“按需安装”的行为。

  1. 首先,在build-apks时,使用--modules参数来指定除了基础模块外,还需要包含哪些动态功能模块。
    java -jar bundletool.jar build-apks \ --bundle=app.aab \ --output=app-with-dfm.apks \ --modules=base,my_dynamic_feature \ # 包含基础模块和`my_dynamic_feature`模块 ... # 其他参数
  2. 安装后,你可以测试动态模块的下载、安装和代码/资源的访问流程,而无需真正通过Play商店。

5.4 包体大小优化闭环

结合bundletool get-size和Android Studio的APK分析器,可以形成一个优化闭环:

  1. 构建基准AAB:在优化前,构建一个AAB作为基准。
  2. 获取基准大小报告:用bundletool get-size --json生成详细报告。
  3. 实施优化:例如,启用R8/ProGuard的更强优化、移除未使用的资源(shrinkResources true)、使用WebP图片、配置语言/密度资源过滤。
  4. 构建优化后AAB
  5. 获取优化后大小报告并对比:重点关注目标设备配置(如en-US, xxhdpi, arm64-v8a)的下载大小变化。bundletool的报告可以精确到每个模块、每种配置的贡献。
  6. 使用APK分析器深挖:如果某个模块或资源类型大小异常,用Android Studio的APK分析器打开bundletool extract-apks提取出的特定APK,进行可视化分析,定位具体是哪些文件占用了空间。

这个过程能让你数据驱动地进行包体优化,每一次改动带来的收益或副作用都清晰可见。

从我自己的经验来看,熟练掌握bundletool是现代Android开发,尤其是涉及Google Play发布和大型应用开发的必备技能。它把黑盒般的AAB分发过程透明化、本地化,给了开发者极大的掌控力和调试能力。初期可能会觉得命令行参数繁琐,但一旦将其整合到你的构建脚本和CI流程中,它带来的效率提升和问题排查能力是无可替代的。花点时间把它摸透,绝对是一笔划算的技术投资。

← 返回列表