AAB 转 APK(bundletool)与 APK 资源提取
本页目录
结论先行:AAB 不能直接安装,但 bundletool 可以把它转成一个能装的 APK:java -jar bundletool.jar build-apks --bundle=app.aab --output=app.apks --mode=universal,再 unzip app.apks universal.apk。APK 本身就是 ZIP,unzip app.apk 'assets/*' -d out 就能取出 assets。只想看 AAB 里的 assets 的话,连 bundletool 都不用:它们就在包里的 base/assets/ 下面。
下面的输出全部来自我为本文构建的一个 hello-world 包(com.example.helloassets:两个 asset 文件,英、法、德三种语言的字符串,以及 arm64-v8a 和 x86_64 两种 ABI 的原生库)。bundletool 是从 GitHub Releases 下载的 1.18.3,运行在 Temurin JDK 17.0.18 上,安装步骤用的是 Android 14 模拟器。
AAB 里面有什么
AAB 也是 ZIP,但按模块组织,Manifest 和资源用的是 protobuf,而不是 APK 里的二进制 XML:
$ unzip -l hello-assets.aab
Length Date Time Name
--------- ---------- ----- ----
10 01-01-2010 00:00 BundleConfig.pb
5008 01-01-2010 00:00 base/lib/arm64-v8a/libhello.so
4704 01-01-2010 00:00 base/lib/x86_64/libhello.so
2616 01-01-2010 00:00 base/dex/classes.dex
53 01-01-2010 00:00 base/assets/config.json
64 01-01-2010 00:00 base/assets/www/index.html
1433 01-01-2010 00:00 base/manifest/AndroidManifest.xml
28 01-01-2010 00:00 base/assets.pb
43 01-01-2010 00:00 base/native.pb
390 01-01-2010 00:00 base/resources.pb
--------- -------
14349 10 files
adb 根本不会把它发到设备上:
$ adb install hello-assets.aab
adb: filename doesn't end .apk or .apex: hello-assets.aab
第 1 步:准备 bundletool
从 Releases 页面 下载 bundletool-all-<版本>.jar(2026-11-02 时最新是 1.18.3),用 Java 运行(我用的是 JDK 17):
java -jar bundletool-all-1.18.3.jar version
1.18.3
下文把这个 JAR 简称为 bundletool.jar。
第 2 步:把 AAB 转成通用 APK
java -jar bundletool.jar build-apks --bundle=hello-assets.aab \
--output=hello-universal.apks --mode=universal
INFO: The APKs will be signed with the debug keystore found at '/Users/longmon/.android/debug.keystore'.
输出的 .apks 是一个“APK 集”,本身也是 ZIP。universal 模式下里面只有一个目录文件和一个 APK:
$ unzip -l hello-universal.apks
Length Date Time Name
--------- ---------- ----- ----
96 01-01-1981 01:01 toc.pb
12827 01-01-1981 01:01 universal.apk
--------- -------
12923 2 files
$ unzip -o hello-universal.apks universal.apk -d out
extracting: out/universal.apk
$ adb install out/universal.apk
Performing Streamed Install
Success
通用 APK 把所有东西放进了一个文件:两种 ABI、三种语言和全部 assets。
$ unzip -l out/universal.apk
Length Date Time Name
--------- ---------- ----- ----
2228 01-01-1981 01:01 AndroidManifest.xml
53 01-01-1981 01:01 assets/config.json
64 01-01-1981 01:01 assets/www/index.html
2616 01-01-1981 01:01 classes.dex
5008 01-01-1981 01:01 lib/arm64-v8a/libhello.so
4704 01-01-1981 01:01 lib/x86_64/libhello.so
556 01-01-1981 01:01 res/xml/splits0.xml
1036 01-01-1981 01:01 resources.arsc
--------- -------
16265 8 files
几个要注意的地方:
- 签名。 不传
--ks时,bundletool 会用~/.android/debug.keystore签名,INFO 那一行说的就是这个。apksigner verify --print-certs out/universal.apk显示的是CN=Android Debug。测试和分析没问题;要把通用 APK 发给用户,就用发布密钥签名:加上--ks=release.jks --ks-key-alias=release,密码通过--ks-pass和--key-pass传入。 - 输出文件已存在。 同一条命令再跑一次,报错
[BT:1.18.3] Error: File 'hello.apks' already exists.,后面跟着一段 Java 堆栈。加--overwrite或先删掉旧文件。 - 功能模块。 只有声明了
<dist:fusing dist:include="true"/>的模块才会合进通用 APK。通用 APK 里少了某个功能,先查这个标记,别急着下结论说应用没有这个功能。 - 字节已经不同。 你自己生成的通用 APK 和 Play 下发的 APK(用应用签名密钥签)不是同一组字节,两者的哈希没法比。
第 3 步:生成设备实际会拿到的拆分包
不加 --mode 时,bundletool 会生成 Play 那样的完整拆分包集合。我这个包一共 16 个文件:
$ java -jar bundletool.jar build-apks --bundle=hello-assets.aab --output=hello.apks
$ unzip -l hello.apks
1057 01-01-1981 01:01 toc.pb
28830 01-01-1981 01:01 splits/base-arm64_v8a.apk
28830 01-01-1981 01:01 splits/base-arm64_v8a_2.apk
28830 01-01-1981 01:01 splits/base-arm64_v8a_3.apk
8339 01-01-1981 01:01 splits/base-de.apk
...
8592 01-01-1981 01:01 splits/base-master.apk
12688 01-01-1981 01:01 splits/base-master_2.apk
12688 01-01-1981 01:01 splits/base-master_3.apk
28827 01-01-1981 01:01 splits/base-x86_64.apk
...
也就是一个 master APK,加上每种 ABI、每种语言各一个拆分包,每个都有三个变体(无后缀、_2、_3)。不用自己挑:用 JSON 描述设备,交给 extract-apks 选。
cat device-spec.json
# {"supportedAbis":["arm64-v8a"],"supportedLocales":["fr-FR"],"screenDensity":420,"sdkVersion":34}
java -jar bundletool.jar extract-apks --apks=hello.apks \
--output-dir=fr-arm64 --device-spec=device-spec.json
The APKs have been extracted in the directory: fr-arm64
$ ls fr-arm64
base-arm64_v8a_3.apk base-fr_3.apk base-master_3.apk
对 SDK 34 的设备,它选的是 _3 这一组:master、arm64 拆分包和法语拆分包。
java -jar bundletool.jar get-device-spec --output=emulator-spec.json 会为当前连接的设备生成这份 JSON。我的模拟器报告的是 "supportedAbis": ["arm64-v8a"]、"supportedLocales": ["zh-CN"]、密度 420、SDK 34。build-apks --connected-device 只生成这台设备需要的文件,这里是 base-master.apk 和 base-arm64_v8a.apk。
安装拆分包集合用 install-apks(或者用 adb install-multiple 把所有文件一起装)。单独装其中一个拆分包会失败,见 安装包似乎无效。
$ java -jar bundletool.jar install-apks --apks=hello.apks
$ adb shell pm path com.example.helloassets
package:/data/app/~~9-EMT_sEmxxD3fGvBiMVvw==/com.example.helloassets-2074h57ADobth13GxZbeZg==/base.apk
package:/data/app/~~9-EMT_sEmxxD3fGvBiMVvw==/com.example.helloassets-2074h57ADobth13GxZbeZg==/split_config.arm64_v8a.apk
没有装任何语言拆分包,因为模拟器只有 zh-CN 这一种语言,而这个包里没有中文字符串。
get-size total 按配置估算下载大小(单位是字节):
$ java -jar bundletool.jar get-size total --apks=hello.apks --dimensions=ABI,LANGUAGE
ABI,LANGUAGE,MIN,MAX
arm64-v8a,de,10685,10717
x86_64,de,10600,10629
x86_64,fr,10595,10626
arm64-v8a,fr,10680,10714
第 4 步:提取 APK 里的 assets
assets 是原样存放的普通文件,unzip 加个通配就行:
$ unzip -o out/universal.apk 'assets/*' -d universal-assets
inflating: universal-assets/assets/config.json
inflating: universal-assets/assets/www/index.html
$ cat universal-assets/assets/config.json
{"apiBase":"https://example.com/api","theme":"dark"}
直接从 AAB 里取,同样的文件不用任何转换就能拿到:
$ unzip -o hello-assets.aab 'base/assets/*' -d aab-assets
inflating: aab-assets/base/assets/config.json
inflating: aab-assets/base/assets/www/index.html
res/raw/、lib/<abi>/*.so、classes*.dex 也一样。unzip 给不了的是可读的 XML:Manifest 和 resources.arsc 都是编译过的。
$ unzip -p out/universal.apk AndroidManifest.xml | file -
/dev/stdin: Android binary XML
改用 SDK 工具读:
$ aapt2 dump badging out/universal.apk
package: name='com.example.helloassets' versionCode='3' versionName='1.2' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
minSdkVersion:'24'
targetSdkVersion:'35'
application-label:'Hello Assets'
...
launchable-activity: name='com.example.helloassets.MainActivity' label='' icon=''
$ java -jar bundletool.jar dump manifest --bundle=hello-assets.aab --xpath=/manifest/@android:versionCode
3
bundletool dump manifest --bundle=... 不加 --xpath 会以 XML 打印完整 Manifest。要完整解码的 res/ 目录,就对 APK 的副本执行 apktool d app.apk。不想装任何东西、只想看 Manifest、签名证书、原生库和全部条目,可以把 APK 或 AAB 拖进本站的 APK/AAB 分析与上架体检,它在浏览器里运行。
macOS 的坑:只有大小写不同的文件名。 有些 release 包里有 res/2f.xml 和 res/2F.xml 这样的资源路径(F-Droid 2.0.2 客户端就有)。macOS 默认卷不区分大小写。我用一个同时包含这两个名字的测试 ZIP 试了一下:unzip -o 两个都显示 extracting,磁盘上却只剩 res/2F.xml;不加 -o 则停在 replace ...? 提示。需要完整文件时,请在 Linux 或区分大小写的卷上解压。
从 assets 能看出什么
文件名只是线索,文件头才是更可靠的证据(xxd -l 16 文件、file 文件)。用来确认自己的构建或开源应用是怎么打包的:
- React Native。 Gradle 插件默认把 JavaScript bundle 打成
assets/index.android.bundle(见ReactExtension.kt里的bundleAssetName)。lib/<abi>/里有libhermes.so说明引擎很可能是 Hermes。Hermes 字节码的魔数是0x1F1903C103BC1FC6(Hermes 源码 BytecodeFileFormat.h 里的MAGIC),按小端存放,xxd显示为c6 1f bc 03 c1 03 19 1f。纯 JavaScript 的 bundle 可以按 从 bundle 还原 JavaScript 源码 处理。 - Flutter。
lib/<abi>/libapp.so加libflutter.so,见 Flutter 应用逆向。 - Unity IL2CPP。
libil2cpp.so加global-metadata.dat。 - 其他。 图片、字体、音频、JSON 配置和 WebView 用的 HTML,都以普通文件的形式放在
assets/和res/raw/下。
只解包你自己写的应用、开源应用,或者你获得授权检查的应用。提取出来的文件仍然受原许可证约束。
可选:用 REA 做带哈希的清单
第三方开源命令行工具 REA 可以盘点并提取 APK 或 AAB,给每个条目算 SHA-256。在 6.7.0 里,rea inspect-artifact hello-assets.aab --json 把这个包识别为 zip 容器,两个 libhello.so 识别为 ELF dynamic-library。它没有 AAB 格式,Android 相关工具都要求独立的 APK,所以要先转换。之前的一次测试里,inspect-android-package 对 .aab 直接拒绝(“is zip; provide one standalone APK”),decode-android-resources 则不报错、但返回的字段全是空的。rea extract-artifact 会用清单里的哈希校验每个写出的文件;在不区分大小写的卷上遇到大小写冲突的文件名时,它会拒绝提取而不是覆盖,正好避开了上面 macOS 的问题。