AAB, splits y contenedores
Antes conviene leer «Contenedor ZIP», «XML binario (AXML)», «Tabla de recursos»
1. Por qué existen los splits
Durante los primeros diez años de Android una aplicación era un fichero: el APK universal.
Dentro iban las bibliotecas nativas de las cuatro arquitecturas, los iconos de las ocho
densidades y las cadenas de los ochenta idiomas. Un teléfono arm64-v8a en español con pantalla
xxhdpi descargaba, instalaba y conservaba en disco el código x86, los iconos ldpi y las
traducciones al finés.
En mayo de 2018, en el Google I/O, Google Play presentó el Android App Bundle y con él invirtió el modelo.
El desarrollador ya no sube el fichero instalable: sube un contenedor intermedio, el .aab, y
es Play quien genera los APK concretos para cada dispositivo. En agosto de 2021 pasó a ser
obligatorio para las aplicaciones nuevas.
La consecuencia técnica es que una aplicación instalada ya no es un APK, sino un conjunto:
un base.apk más los split que le correspondan a ese dispositivo. PackageManager los monta
como una sola aplicación con un solo AssetManager. La consecuencia práctica para quien analiza
es más incómoda: descargar «la aplicación» de una fuente que no sea Play devuelve un ZIP de ZIPs
con una extensión inventada por el sitio de descargas, y hay que averiguar qué es antes de poder
abrirlo.
Este documento cubre los tres niveles: el AAB que produce el desarrollador, el APK Set que
produce bundletool, y los contenedores de terceros que produce cada sitio de descargas.
2. El Android App Bundle (.aab)
2.1 Estructura interna
Un .aab es un ZIP, pero no es un APK y no se instala. Su estructura, verificada sobre
com.ejemplo.fixture.aab del «muestrario», generado con bundletool build-bundle 1.18.3:
unzip -l ~/muestras-apk/splits/com.ejemplo.fixture.aab
Length Date Time Name
--------- ---------- ----- ----
10 01-01-2010 00:00 BundleConfig.pb
114 01-01-2010 00:00 base/res/drawable-xxhdpi-v4/ic_fixture.png
104 01-01-2010 00:00 base/res/drawable-xhdpi-v4/ic_fixture.png
115 01-01-2010 00:00 base/res/drawable-mdpi-v4/ic_fixture.png
100 01-01-2010 00:00 base/res/drawable-hdpi-v4/ic_fixture.png
6492 01-01-2010 00:00 base/lib/armeabi-v7a/libtermux.so
9008 01-01-2010 00:00 base/lib/arm64-v8a/libtermux.so
9248 01-01-2010 00:00 base/lib/x86_64/libtermux.so
1800 01-01-2010 00:00 base/dex/classes.dex
1714 01-01-2010 00:00 base/manifest/AndroidManifest.xml
68 01-01-2010 00:00 base/native.pb
1083 01-01-2010 00:00 base/resources.pb
El mapa completo, incluyendo lo que este fixture mínimo no trae:
| Entrada | Contenido | Formato |
|---|---|---|
BundleConfig.pb |
Configuración del bundle: versión de bundletool, optimizaciones, compresión, locales |
protobuf android.bundle.BundleConfig |
BUNDLE-METADATA/ |
Metadatos para herramientas y tiendas: mapping.txt de R8, lista de ficheros DEX. No se empaquetan en los APK |
Variable |
base/ |
El módulo base. Obligatorio | Directorio |
<feature>/ |
Un directorio por módulo de funcionalidad, con el nombre del atributo split de su manifiesto |
Directorio |
<asset_pack>/ |
Un directorio por asset pack de Play Asset Delivery | Directorio |
<módulo>/manifest/AndroidManifest.xml |
El manifiesto del módulo, en un directorio aparte, a diferencia del APK | protobuf aapt.pb.XmlNode, no AXML |
<módulo>/dex/ |
Los classes*.dex del módulo, en un directorio aparte |
DEX |
<módulo>/res/ |
Recursos con calificador, idénticos a los de un APK | Ficheros de recurso |
<módulo>/lib/<abi>/ |
Bibliotecas nativas | ELF |
<módulo>/assets/ |
Assets | Cualquiera |
<módulo>/root/ |
Ficheros que se reubican a la raíz del APK generado. Es donde acaban los recursos Java que se leen con Class.getResource() |
Cualquiera |
<módulo>/resources.pb |
La tabla de recursos del módulo | protobuf aapt.pb.ResourceTable |
<módulo>/native.pb |
Descripción del código nativo del módulo | protobuf |
<módulo>/assets.pb |
Descripción de los assets del módulo, cuando hay asset packs | protobuf |
Tres diferencias con la estructura de un APK que se olvidan enseguida:
AndroidManifest.xmlno está en la raíz del módulo, sino en<módulo>/manifest/.- Los
.dexno están en la raíz, sino en<módulo>/dex/. - No hay
resources.arsc, hayresources.pb. No es el mismo formato ni remotamente.
2.2 Por qué protobuf y no los formatos binarios del APK
resources.arsc está diseñado para leerse en un teléfono: es un índice denso, mapeable, con los
valores ya resueltos por configuración. Sus tres virtudes —compacidad, acceso aleatorio y coste
de análisis cero— solo importan en el dispositivo.
El AAB no se ejecuta nunca. Es una entrada de un proceso de compilación que corre en los
servidores de Play y cuyo trabajo es exactamente dividir y recombinar la tabla: quedarse con
los recursos de un idioma, tirar los de las demás densidades, fusionar módulos. Para eso el
formato denso es un estorbo, porque toda edición reindexa el string pool y recalcula los offsets
de todos los chunks (ver «Tabla de recursos», secciones «4» y «7»).
Protocol Buffers da lo contrario: campos con nombre, opcionales, extensibles hacia delante y hacia atrás, sin offsets absolutos y sin punteros. Un servicio puede añadir un campo sin romper a los lectores viejos. El coste es tamaño y velocidad de análisis, que ahí no importan.
Y la conversión alcanza también al manifiesto, cosa que se olvida: el manifiesto del módulo
viaja en protobuf, no en AXML. Es un mensaje aapt.pb.XmlNode del mismo Resources.proto
que define ResourceTable —XmlNode → XmlElement → XmlAttribute, con resource_id y
compiled_item ya resueltos—, lo produce aapt2 link --proto-format y bundletool lo lee con
XmlProtoNode, no con un parser de AXML. Sobre ese árbol proto añade Play los atributos de
split de la «sección 4.2 · Los atributos del manifiesto». La conversión al formato binario va en el sentido contrario, al
materializar cada APK: aapt2 convert --output-format binary para el manifiesto, y
resources.pb → resources.arsc para la tabla.
Se comprueba en un solo paso: unzip -p app.aab base/manifest/AndroidManifest.xml | xxd | head -1
devuelve 0a… —etiqueta de campo protobuf—, no 0300 0800, que es lo que sí aparece en el
AndroidManifest.xml de cualquiera de los splits/*.apk generados.
2.3 Los .proto relevantes
Las definiciones están publicadas en el repositorio de bundletool, bajo src/main/proto/, y
en AOSP para las de aapt2. Los cuatro que hacen falta para leer un bundle o un APK Set:
| Fichero | Paquete proto | Java | Mensaje raíz | Describe |
|---|---|---|---|---|
config.proto |
android.bundle |
com.android.bundle |
BundleConfig |
BundleConfig.pb del .aab |
commands.proto |
android.bundle |
com.android.bundle |
BuildApksResult |
toc.pb del .apks |
targeting.proto |
android.bundle |
com.android.bundle |
ApkTargeting, VariantTargeting |
A qué dispositivos aplica cada APK |
files.proto |
android.bundle |
com.android.bundle |
— | native.pb, assets.pb |
Resources.proto |
aapt.pb |
com.android.aapt |
ResourceTable |
resources.pb de cada módulo |
BundleConfig tiene nueve campos de primer nivel: bundletool, optimizations, compression,
master_resources, apex_config, unsigned_embedded_apk_config, asset_modules_config, type
—REGULAR, APEX o ASSET_ONLY— y locales. El del fixture del muestrario ocupa 10 bytes,
porque solo lleva la versión de bundletool:
java -jar ~/herramientas/bundletool.jar dump config \
--bundle ~/muestras-apk/splits/com.ejemplo.fixture.aab
{
"bundletool": {
"version": "1.18.3"
}
}
Resources.proto de aapt2 define la jerarquía equivalente a la de resources.arsc:
ResourceTable → Package → Type → Entry → ConfigValue, con PackageId, TypeId y
EntryId como mensajes propios. Es una traducción campo a campo de las estructuras del
documento «Tabla de recursos», no un formato distinto.
3. El APK Set (.apks)
3.1 Qué es
Es lo que produce bundletool build-apks a partir de un .aab: un ZIP con todos los APK
que Play podría llegar a generar, más una tabla de contenidos que dice cuál va a cada
dispositivo. No se instala directamente; se le extrae el subconjunto que toca.
De com.ejemplo.fixture.apks del muestrario, generado localmente:
toc.pb
splits/base-master.apk splits/base-master_2.apk splits/base-master_3.apk
splits/base-arm64_v8a.apk splits/base-armeabi_v7a.apk splits/base-x86_64.apk
splits/base-de.apk splits/base-es.apk splits/base-fr.apk
splits/base-ldpi.apk splits/base-mdpi.apk splits/base-hdpi.apk
splits/base-tvdpi.apk splits/base-xhdpi.apk splits/base-xxhdpi.apk
splits/base-xxxhdpi.apk
43 ficheros para una aplicación de una sola actividad. Los sufijos _2 y _3 son las
variantes: bundletool genera un juego de splits por cada rango de versión de Android que
requiera un empaquetado distinto, y toc.pb dice cuál corresponde a cada nivel de API.
Los directorios que puede traer un APK Set:
| Directorio | Contenido |
|---|---|
splits/ |
Los split APK de instalación por Play, agrupados por variante |
standalones/ |
APK monolíticos completos para dispositivos anteriores a Android 5.0, que no soportan splits |
instant/ |
Los splits de la versión instant, si el bundle la declara |
system/ |
APK para preinstalación en la imagen del sistema |
asset-slices/ |
Las porciones de los asset packs |
| (raíz) | universal.apk cuando se generó con --mode=universal |
El modo universal produce un APK Set degenerado: una tabla de contenidos de 90 bytes y un solo APK con todo dentro. Es la vía oficial para obtener un instalable único a partir de un bundle:
Archive: com.ejemplo.fixture-universal.apks
90 toc.pb
66347 universal.apk
3.2 toc.pb: BuildApksResult
El fichero toc.pb en la raíz es la firma estructural del formato y un mensaje protobuf
android.bundle.BuildApksResult. Sus campos:
| Campo | Nº | Tipo | Contenido |
|---|---|---|---|
variant |
1 | repeated Variant |
Las variantes generadas |
bundletool |
2 | Bundletool |
La versión de bundletool que lo produjo |
asset_slice_set |
3 | repeated AssetSliceSet |
Porciones de asset packs |
package_name |
4 | string |
El nombre del paquete |
local_testing_info |
5 | LocalTestingInfo |
Modo de prueba local |
asset_modules_info |
6 | AssetModulesInfo |
Metadatos de bundles de solo assets |
default_targeting_value |
7 | repeated DefaultTargetingValue |
Valores por defecto de las dimensiones |
permanently_fused_modules |
8 | repeated PermanentlyFusedModule |
Módulos fusionados en base en todas las variantes |
device_group_config |
9 | DeviceGroupConfig |
Grupos de dispositivos y conjuntos de países |
Y la jerarquía: Variant → ApkSet → ApkDescription, donde ApkSet lleva un
ModuleMetadata —nombre del módulo, tipo, modo de entrega, dependencias— y cada
ApkDescription lleva su ApkTargeting, su path dentro del ZIP y uno de los siete
metadatos posibles: split_apk_metadata (con split_id y is_master_split),
standalone_apk_metadata, instant_apk_metadata, system_apk_metadata,
asset_slice_metadata, apex_apk_metadata o archived_apk_metadata.
Decodificando el toc.pb del fixture con un lector genérico de wire format, sin biblioteca de
protobuf, se ve exactamente esa estructura:
toc.pb: 3250 bytes
campo 1 (length-delimited) x3 ← tres Variant
campo 2 (length-delimited) x1 ← Bundletool: b'\x12\x061.18.3'
campo 4 (length-delimited) x1 ← package_name: b'com.ejemplo.fixture'
campo 5 (length-delimited) x1 ← local_testing_info, vacío
Variant[0] → ApkSet[0] → 14 ApkDescription
path = splits/base-ldpi.apk
path = splits/base-mdpi.apk
path = splits/base-hdpi.apk
path = splits/base-xhdpi.apk
Tres Variant, que es el origen de los sufijos _2 y _3 del listado de 3.1.
4. Taxonomía de splits
4.1 Los tres tipos
| Tipo | Nombre habitual | Qué lleva |
|---|---|---|
| Base | base.apk, o <paquete>.apk en los contenedores de terceros |
El manifiesto completo, los classes*.dex, resources.arsc y los recursos sin calificador |
| Config split | config.<abi>, config.<densidad>, config.<idioma>, o split_config.* |
Solo lo de esa configuración. No tiene código |
| Feature module | split_<nombre>, o <nombre>.apk |
Un módulo de funcionalidad descargable bajo demanda. Puede tener código propio |
Un split de configuración puede a su vez pertenecer a un módulo de funcionalidad, y entonces
lleva los dos nombres. El zip SAI de Netflix del muestrario lo muestra en estado puro:
base.apk
split_voip.apk ← feature module
split_partnermodule.apk ← feature module
split_config.armeabi_v7a.apk ← config split del base
split_config.es.apk split_config.ja.apk split_config.zh.apk …
split_voip.config.armeabi_v7a.apk ← config split de un feature module
split_partnermodule.config.ko.apk ← ídem
split_partnermodule.config.xxhdpi.apk
4.2 Los atributos del manifiesto
Todo se decide en AndroidManifest.xml. Verificado con aapt2 dump xmltree sobre APK reales
del muestrario.
| Atributo | Resource ID | Dónde aparece | Significado |
|---|---|---|---|
split |
— (sin namespace) | En el <manifest> de todo split, nunca en el base |
El identificador del split: config.es, voip, config.armeabi_v7a |
android:isFeatureSplit |
0x0101055b |
Solo en módulos de funcionalidad | true |
android:isSplitRequired |
0x01010591 |
En el <manifest> del base y de los feature modules |
El sistema rechaza la instalación (INSTALL_FAILED_MISSING_SPLIT) si faltan splits requeridos. En <application> se ignora |
android:requiredSplitTypes |
0x0101064e |
En el base | Lista de tipos de split obligatorios, separados por comas |
android:splitTypes |
0x0101064f |
En cada split | Los tipos que ese split aporta. Cadena vacía si no aporta ninguno |
android:hasCode |
0x0101000c |
En <application> de los config splits |
false |
Todos los identificadores de la tabla proceden de la salida real de aapt2 dump xmltree sobre
APK del muestrario. El de isSplitRequired se midió sobre el base.apk del SAI de Netflix, que es
el único contenedor del muestrario que usa el mecanismo antiguo:
BT=~/Library/Android/sdk/build-tools/37.0.0
$BT/aapt2 dump xmltree --file AndroidManifest.xml base.apk | grep isSplitRequired
A: http://schemas.android.com/apk/res/android:isSplitRequired(0x01010591)=true
Los demás contenedores inspeccionados usan el mecanismo más reciente de
requiredSplitTypes/splitTypes. Conviven los dos, y un fusionador tiene que tratar ambos:
lo detalla la «sección 5.2 de
Fusión de splits · Los dos mecanismos conviven».
El mecanismo moderno, sobre el base.apk de Google Keep extraído del .apks del muestrario:
E: manifest (line=2)
A: android:versionCode(0x0101021b)=220671357
A: android:versionName(0x0101021c)="5.26.321.01.97"
A: android:requiredSplitTypes(0x0101064e)="base__abi,base__density"
A: android:splitTypes(0x0101064f)=""
A: package="com.google.android.keep"
Y su config.armeabi_v7a.apk:
E: manifest (line=0)
A: android:versionCode(0x0101021b)=220671357
A: android:splitTypes(0x0101064f)="base__abi"
A: package="com.google.android.keep"
A: split="config.armeabi_v7a"
E: application (line=0)
A: android:hasCode(0x0101000c)=false
El base declara que necesita alguien que aporte base__abi y base__density; cada config split
declara qué aporta. El instalador comprueba que el conjunto es completo antes de aceptarlo.
Esa es la razón número uno por la que instalar splits sueltos falla: sin el base, con
INSTALL_FAILED_INVALID_APK: Full install must include a base package; con el base y el conjunto
incompleto, con INSTALL_FAILED_MISSING_SPLIT: Missing split for <paquete>. Los dos están
reproducidos en la «sección 2.5 de Diagnóstico de fallos · Lo que responde adb install, reproducido».
Un módulo de funcionalidad añade además el namespace dist. De split_voip.apk de Netflix:
N: dist=http://schemas.android.com/apk/distribution
E: manifest (line=0)
A: android:versionCode(0x0101021b)=50451
A: android:isFeatureSplit(0x0101055b)=true
A: split="voip"
E: dist:module (line=0)
A: dist:onDemand=true
A: dist:title=@0x7f140db8
E: dist:fusing (line=0)
A: dist:include=true
dist:fusing include=true es la marca de que ese módulo debe fusionarse en el APK monolítico
cuando se genera uno para dispositivos anteriores a Android 5.0.
4.3 Lo que todos los splits comparten obligatoriamente
Tres cosas, y las tres son invariantes que un merge tiene que comprobar antes de empezar:
package, el nombre del paquete. Idéntico en todos.android:versionCode. Idéntico en todos. En el ejemplo de Keep,220671357en el base y en cada uno de los 26 config splits.- La firma. Todos los splits tienen que estar firmados con la misma clave, porque
PackageManagerlos trata como una sola aplicación. Ver «Esquemas de firma».
Lo que no comparten: versionName —los config splits lo traen vacío—, minSdkVersion
—cada variante puede declarar el suyo— y, evidentemente, el contenido.
5. Contenedores de terceros
Ninguno de estos formatos está especificado. Lo que sigue está medido sobre el muestrario.
Esta sección y la 6 describen los contenedores desde el lado del formato. Si lo que hay es un fichero concreto y la pregunta es «¿qué es esto y qué hago con ello?», el documento que responde es «Formatos y extensiones», que además cubre la exportación de SAI y la distinción entre un
APKcompleto y unsplitsuelto.
5.1 .xapk — APKCombo y APKPure
ZIP plano con el base y los config splits en la raíz, más un manifest.json de metadatos y un
icon.png. Los de APKCombo añaden un APKComboInstaller.url. De
GoogleAuthenticator_com.google.android.apps.authenticator2.xapk, 36 entradas:
com.google.android.apps.authenticator2.apk ← el base, con el nombre del paquete
config.en.apk config.es.apk config.de.apk … (25 idiomas)
config.ldpi.apk … config.xxxhdpi.apk (7 densidades)
manifest.json
icon.png
APKComboInstaller.url
La clave que identifica el formato es xapk_version dentro de manifest.json:
{
"xapk_version": "2",
"package_name": "com.google.android.apps.authenticator2",
"name": "Authenticator",
"locales_name": { "ar": "Authenticator", "de": "Authenticator", … },
"version_code": "7002007",
"version_name": "7.2",
"min_sdk_version": "28",
"target_sdk_version": "37",
"permissions": [ "android.permission.CAMERA", … ],
"total_size": 6647133,
"icon": "icon.png",
"split_apks": [ … ]
}
split_apks es una lista de objetos con el nombre de fichero y el id de cada split, así que
el contenedor es autodescriptivo sin necesidad de abrir los APK.
5.2 .apkm — APKMirror
ZIP con el base y los splits en la raíz, un info.json, un icon.png y su propia firma JAR
sobre el contenedor. De tv.twitch.android.app.apkm:
META-INF/MANIFEST.MF
META-INF/APKMIRRO.SF
META-INF/APKMIRRO.RSA ← firma del contenedor, no de la aplicación
info.json
icon.png
base.apk
split_config.armeabi_v7a.apk split_config.arm64_v8a.apk split_config.x86.apk …
split_config.ldpi.apk … split_config.xxxhdpi.apk
Esa firma JAR (ver «Esquemas de firma») es de APKMirror sobre el contenedor, no de la aplicación: acredita la procedencia de la descarga y no tiene ninguna relación con la firma de los APK que hay dentro. Confundirlas es un error de análisis frecuente.
info.json tiene 17 claves, entre ellas la que identifica el formato:
apkm_version, apk_title, app_name, release_version, variant, release_title,
versioncode, pname, post_date, capabilities, languages, arches, dpis,
min_api, accent_color, apk_id, release_id
apkm_version vale 5 en los ocho .apkm del muestrario, y pname es el nombre del paquete.
5.3 SAI — Split APKs Installer
El más simple de todos: un ZIP plano con los .apk y nada más. Sin metadatos, sin
manifest.json, sin firma propia. El del muestrario, com.netflix.mediaclient.sai.zip, trae 40
entradas, todas .apk, con el prefijo split_ en los splits y base.apk en el base.
La ausencia de cualquier fichero descriptivo es precisamente lo que lo define, y por eso está el último en el orden de precedencia de la «sección 6 · Detección por estructura, nunca por extensión».
5.4 Los OBB heredados
Antes del AAB, la vía oficial para superar el límite de tamaño de Play eran los expansion
files: hasta dos ficheros .obb de 2 GB cada uno que viajaban fuera del APK y se
descargaban aparte. Su nombre sigue un formato fijo:
[main|patch].<expansion-version>.<package-name>.obb
donde <expansion-version> es el versionCode del APK en su primera subida —por ejemplo
main.314159.com.example.app.obb—, y se instalan en
<almacenamiento-compartido>/Android/obb/<package-name>/.
Están superados: desde agosto de 2021 las aplicaciones nuevas de más de 200 MB usan Play
Feature Delivery o Play Asset Delivery. Siguen apareciendo en descargas de juegos antiguos, a
veces dentro de un .xapk, donde ocupan una entrada Android/obb/… junto a los APK. El
muestrario no contiene ninguno; lo de esta sección procede de la documentación oficial,
no de una medición.
6. Detección por estructura, nunca por extensión
6.1 El principio
La extensión miente. No es una precaución teórica: es el hallazgo medido de la «sección 6.3 · La comprobación sobre el muestrario».
Un fichero .apks puede no ser un APK Set; un .zip puede ser un contenedor SAI; un .apk
puede ser un .aab renombrado. La única fuente fiable es el contenido del Central Directory del ZIP, que se lee sin descomprimir nada.
Cada formato tiene una evidencia estructural que ningún otro comparte:
| Formato | Evidencia | Por qué es exclusiva |
|---|---|---|
| APK Set | toc.pb en la raíz |
Solo bundletool lo escribe |
.apkm |
info.json más META-INF/*.RSA en la raíz |
Ningún otro contenedor firma el propio ZIP |
.xapk |
manifest.json con la clave xapk_version |
La clave es privativa de APKCombo y APKPure |
| SAI / zip plano | Varias entradas *.apk en la raíz o bajo un prefijo, y ningún AndroidManifest.xml en la raíz |
Es lo que queda cuando no hay metadatos |
| APK suelto | AndroidManifest.xml en la raíz; resources.arsc no se puede exigir, porque los splits de ABI no lo llevan |
Un APK real siempre tiene manifiesto |
.aab |
BundleConfig.pb más base/manifest/AndroidManifest.xml |
El manifiesto en subdirectorio es exclusivo del bundle |
6.2 El orden de precedencia
El orden importa porque las evidencias se solapan. Un .apkm tiene entradas *.apk en la
raíz, así que también cumple la regla del zip plano; un APK Set tiene splits/*.apk. Se
comprueba de lo más específico a lo más genérico:
1. ¿toc.pb en la raíz? → APK Set genuino de bundletool
2. ¿info.json + META-INF/*.RSA en la raíz? → .apkm de APKMirror
3. ¿manifest.json con clave xapk_version? → .xapk de APKCombo o APKPure
4. ¿varias entradas *.apk y sin manifiesto? → zip plano de splits (incluido SAI)
5. ¿AndroidManifest.xml en la raíz? → APK suelto (resources.arsc es opcional)
6. ¿BundleConfig.pb + base/manifest/…? → .aab
Un detector que empiece por el paso 4 clasifica los .apkm y los APK Set como zips planos y
pierde sus metadatos. Uno que empiece por el 5 no distingue un APK de un split.
La tabla de la «sección 4 de Formatos y extensiones · Cómo saber en diez segundos qué tienes delante» aplica el mismo
criterio con dos diferencias: adelanta el .aab al segundo lugar, lo que no cambia ningún
resultado, y añade antes del zip plano una regla para los .apks exportados por SAI
(meta.sai_v1.json o meta.sai_v2.json en la raíz), que este orden clasificaría como zip
plano.
Y cuando la estructura contradice a la extensión, no se falla: se continúa y se advierte en el informe. Es exactamente el fallo silencioso que caracteriza al sector.
6.3 La comprobación sobre el muestrario
La cifra de partida era que 14 de los 16 .apks del muestrario no llevan toc.pb. Comprobado
de forma independiente, sobre los 25 contenedores del directorio splits/, mirando el
Central Directory de cada uno:
cd ~/muestras-apk/splits
for f in *; do
case "$f" in *.aab) continue;; esac
toc=$(unzip -l "$f" 2>/dev/null | grep -c " toc\.pb$")
mf=$(unzip -l "$f" 2>/dev/null | grep -c " manifest\.json$")
ij=$(unzip -l "$f" 2>/dev/null | grep -c " info\.json$")
echo "$f|toc.pb=$toc|manifest.json=$mf|info.json=$ij"
done
com.duolingo.apks|toc.pb=0|manifest.json=1|info.json=0
com.google.android.apps.docs.apks|toc.pb=0|manifest.json=1|info.json=0
com.google.android.apps.maps.apks|toc.pb=0|manifest.json=1|info.json=0
com.google.android.apps.photos.apks|toc.pb=0|manifest.json=1|info.json=0
com.google.android.gm.apks|toc.pb=0|manifest.json=1|info.json=0
com.google.android.keep.apks|toc.pb=0|manifest.json=1|info.json=0
com.google.android.youtube.apks|toc.pb=0|manifest.json=1|info.json=0
com.king.candycrushsaga.apks|toc.pb=0|manifest.json=1|info.json=0
com.moonactive.coinmaster.apks|toc.pb=0|manifest.json=1|info.json=0
com.ejemplo.fixture-universal.apks|toc.pb=1|manifest.json=0|info.json=0
com.ejemplo.fixture.apks|toc.pb=1|manifest.json=0|info.json=0
com.outfit7.mytalkingtomfree.apks|toc.pb=0|manifest.json=1|info.json=0
com.spotify.music.apks|toc.pb=0|manifest.json=1|info.json=0
com.supercell.clashroyale.apks|toc.pb=0|manifest.json=1|info.json=0
com.whatsapp.apks|toc.pb=0|manifest.json=1|info.json=0
org.telegram.messenger.apks|toc.pb=0|manifest.json=1|info.json=0
…
com.netflix.mediaclient.sai.zip|toc.pb=0|manifest.json=0|info.json=0
tv.twitch.android.app.apkm|toc.pb=0|manifest.json=0|info.json=1
El resultado es concluyente. De los 16 ficheros con extensión
.apks:
- 2 llevan
toc.pb:com.ejemplo.fixture.apksycom.ejemplo.fixture-universal.apks, los dos generados localmente conbundletool. - 14 no lo llevan y los 14 llevan
manifest.json. Y esemanifest.jsontiene la clavexapk_version:
unzip -p com.google.android.keep.apks manifest.json | python3 -m json.tool | head -4
{
"xapk_version": "2",
"package_name": "com.google.android.keep",
"name": "Keep Notes",
Comprobado también en com.whatsapp.apks, con el mismo "xapk_version": "2".
La conclusión es más fuerte que «no son APK Set»: son .xapk de APKCombo con la extensión
cambiada. El detector de la «sección 6.2 · El orden de precedencia» los clasifica correctamente en el paso 3, y el paso 1
—el que la extensión sugiere— falla como debe.
Los 8 .apkm y el .zip de SAI se clasifican igual de bien: los primeros por info.json más
META-INF/APKMIRRO.RSA, el segundo por descarte en el paso 4.
7. Instalar sin fusionar frente a fusionar
Hay dos maneras de llevar un conjunto de splits a un dispositivo, y hacen cosas distintas.
Instalar sin fusionar. El sistema recibe los APK por separado y los monta como una sola aplicación. Es lo que hace Play y lo que preserva el comportamiento original:
Verificado el 13 de agosto de 2026 contra un emulador Android 14 arm64-v8a creado para la
ocasión, con adb 1.0.41 de las platform-tools 37.0.0 y los splits de Telegram del
muestrario:
adb install-multiple org.telegram.messenger.apk config.es.apk \
config.arm64_v8a.apk config.xxhdpi.apk
Success
bundletool hace lo mismo pero eligiendo él los splits:
java -jar bundletool.jar install-apks --apks=$MUESTRAS/splits/com.ejemplo.fixture.apks
The APKs have been extracted in the directory: /var/folders/lk/…/8598555168175782903
install-apks consulta el toc.pb, interroga al dispositivo conectado y extrae solo los splits
que le corresponden. Es lo correcto siempre que haya un dispositivo. También acepta
--device-id=<serie> cuando hay más de uno.
Instalar el conjunto no es instalar los ficheros que uno le pasa. El sistema los renombra
según el atributo split de cada manifiesto, no según el nombre del fichero de entrada:
adb shell pm path org.telegram.messenger
package:/data/app/~~cB6DHo33I…==/org.telegram.messenger-F_5lfNc…==/base.apk
package:/data/app/~~cB6DHo33I…==/org.telegram.messenger-F_5lfNc…==/split_config.arm64_v8a.apk
package:/data/app/~~cB6DHo33I…==/org.telegram.messenger-F_5lfNc…==/split_config.es.apk
package:/data/app/~~cB6DHo33I…==/org.telegram.messenger-F_5lfNc…==/split_config.xxhdpi.apk
Entraron cuatro config.*.apk y quedaron cuatro split_config.*.apk. Los dos sufijos
aleatorios de la ruta cambian en cada instalación, y el directorio con ~~ es de Android 11 en
adelante.
Los dos fallos que salen a la primera, y que reproducen con su salida literal las secciones «3.1» y
«6.2» de «Formatos y extensiones», son
INSTALL_FAILED_MISSING_SPLIT —falta algún split del conjunto— e
INSTALL_FAILED_NO_MATCHING_ABIS —el config.<abi> no corresponde a la arquitectura del
dispositivo—.
Fusionar. Producir un único APK instalable con todo dentro. Es lo que hace falta cuando no
hay adb, cuando el objetivo es analizar la aplicación como una unidad, o cuando se va a
modificar y volver a firmar. Exige mucho más que concatenar ZIPs:
- Unir las tablas de recursos de todos los splits en una sola, resolviendo las colisiones de identificador (ver «Tabla de recursos» y «Fusión de splits»).
- Sanear el manifiesto: quitar
split,isFeatureSplit,requiredSplitTypesysplitTypes, porque el resultado ya no es un conjunto. - Renumerar los
classes*.dexde los módulos de funcionalidad que tengan código propio. - Seleccionar qué configuraciones se conservan y cuáles se descartan.
- Realinear y volver a firmar, porque la firma original ya no vale.
Lo que no se puede hacer es fusionar un AAB directamente: hay que pasar antes por
bundletool build-apks, porque el .aab no contiene ningún resources.arsc, solo
resources.pb. La vía oficial es --mode=universal, que produce el universal.apk de la
«sección 3.1 · Qué es».
8. Recetas
Máquina de referencia: macOS, bundletool 1.18.3 en
~/herramientas/bundletool.jar, aapt2 2.20-15087165 de las build-tools 37.0.0, OpenJDK 21.
8.1 bundletool validate y dump sobre un .aab
Los subcomandos dump y validate solo aceptan --bundle. Invocarlos con --apks falla:
BTOOL=~/herramientas/bundletool.jar
MUESTRAS=~/muestras-apk
java -jar $BTOOL dump manifest --apks $MUESTRAS/splits/com.ejemplo.fixture.apks
[BT:1.18.3] Error: Missing the required --bundle flag.
com.android.tools.build.bundletool.flags.Flag$RequiredFlagNotSetException: …
Sobre el bundle sí funcionan:
java -jar $BTOOL validate --bundle $MUESTRAS/splits/com.ejemplo.fixture.aab | head -12
App Bundle information
------------
Feature modules:
Feature module: base
File: res/drawable-xxhdpi-v4/ic_fixture.png
File: res/drawable-xhdpi-v4/ic_fixture.png
File: res/drawable-mdpi-v4/ic_fixture.png
File: res/drawable-hdpi-v4/ic_fixture.png
File: lib/armeabi-v7a/libtermux.so
File: lib/arm64-v8a/libtermux.so
File: lib/x86_64/libtermux.so
File: dex/classes.dex
java -jar $BTOOL dump manifest --bundle $MUESTRAS/splits/com.ejemplo.fixture.aab | head -6
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:compileSdkVersion="36" android:compileSdkVersionCodename="16" android:versionCode="1" android:versionName="1.0" package="com.ejemplo.fixture" platformBuildVersionCode="36" platformBuildVersionName="16">
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="36"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application android:extractNativeLibs="false" android:icon="@drawable/ic_fixture" android:label="@string/app_name"/>
dump resources --values lee el resources.pb de los módulos y lo presenta con la misma forma
que aapt2 dump resources presenta un resources.arsc:
java -jar $BTOOL dump resources --bundle $MUESTRAS/splits/com.ejemplo.fixture.aab --values
Package 'com.ejemplo.fixture':
0x7f010000 - drawable/ic_fixture
density: 160 - [FILE] res/drawable-mdpi-v4/ic_fixture.png
density: 240 - [FILE] res/drawable-hdpi-v4/ic_fixture.png
density: 320 - [FILE] res/drawable-xhdpi-v4/ic_fixture.png
density: 480 - [FILE] res/drawable-xxhdpi-v4/ic_fixture.png
0x7f020000 - string/app_name
(default) - [STR] "Ejemplo Fixture"
locale: "de" - [STR] "Ejemplo Beispiel"
locale: "fr" - [STR] "Echantillon Ejemplo"
locale: "es" - [STR] "Muestra Ejemplo"
8.2 Extraer los splits de un dispositivo concreto
Sin dispositivo conectado se puede escribir la especificación a mano:
cat > /tmp/dev.json <<'EOF'
{"supportedAbis":["arm64-v8a"],"supportedLocales":["es"],"screenDensity":420,"sdkVersion":34}
EOF
java -jar $BTOOL extract-apks --apks $MUESTRAS/splits/com.ejemplo.fixture.apks \
--output-dir ./extracted --device-spec /tmp/dev.json
ls extracted
The APKs have been extracted in the directory: ./extracted
base-arm64_v8a_3.apk
base-es_3.apk
base-master_3.apk
base-xxhdpi_3.apk
Cuatro APK de los 43: el maestro, la ABI, el idioma y la densidad, todos de la variante _3,
que es la que corresponde a sdkVersion 34. Es exactamente lo que instalaría Play.
Y el tamaño de descarga por combinación:
java -jar $BTOOL get-size total --apks $MUESTRAS/splits/com.ejemplo.fixture.apks \
--dimensions=ABI,LANGUAGE | head -5
ABI,LANGUAGE,MIN,MAX
arm64-v8a,de,14549,14703
x86_64,de,14933,15080
x86_64,es,14928,15074
x86_64,fr,14937,15083
8.3 Inspeccionar el manifiesto de un split
BT=~/Library/Android/sdk/build-tools/37.0.0
$BT/aapt2 dump badging /tmp/keep/config.es.apk | head -4
package: name='com.google.android.keep' versionCode='220671357' versionName='' split='config.es'
application: label='' icon=''
minSdkVersion:'32'
feature-group: label=''
versionName vacío y split='config.es': la marca inequívoca de un config split.
aapt2 dump badging también lista los locales y densidades que aporta:
locales: 'es' 'es-419' 'es-US'
densities: '160'
Un solo config.es.apk cubre tres locales, porque es-419 y es-US son variantes regionales
que Play agrupa en el mismo split.
Fuentes
- Formato del Android App Bundle — https://developer.android.com/guide/app-bundle/app-bundle-format
Consultado el 12 de agosto de 2026. De aquí sale la tabla de estructura interna de la «sección
2.1 · Estructura interna», incluidos
BUNDLE-METADATA/, el directorioroot/y la separación demanifest/ydex/, con las citas literales. google/bundletool,src/main/proto/commands.proto— https://raw.githubusercontent.com/google/bundletool/master/src/main/proto/commands.proto Consultado el 12 de agosto de 2026. De aquí sale íntegra la tabla de campos deBuildApksResultde la «sección 3.2 · toc.pb: BuildApksResult» y la jerarquíaVariant→ApkSet→ApkDescriptioncon sus siete metadatos alternativos.google/bundletool,src/main/proto/config.proto— https://raw.githubusercontent.com/google/bundletool/master/src/main/proto/config.proto Consultado el 12 de agosto de 2026. De aquí salen los nueve campos deBundleConfigy los valores deBundleTypede la «sección 2.3 · Los .proto relevantes».google/bundletool,src/main/proto/targeting.protoyfiles.proto— https://raw.githubusercontent.com/google/bundletool/master/src/main/proto/targeting.proto Consultado el 12 de agosto de 2026. De aquí sale la fila correspondiente de la tabla de.protode la «sección 2.3 · Los .proto relevantes».frameworks/base/tools/aapt2/Resources.proto(etiquetaandroid-17.0.0_r1) — https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/tools/aapt2/Resources.proto Consultado el 25 de septiembre de 2026, descargado con?format=TEXTy decodificado. De aquí salen el paqueteaapt.pb, eljava_packagecom.android.aapty la jerarquíaResourceTable→Package→Type→Entry→ConfigValuede la «sección 2.3 · Los .proto relevantes».- bundletool — documentación oficial — https://developer.android.com/tools/bundletool
Consultado el 12 de agosto de 2026. De aquí sale la sintaxis de
build-apks,install-apks,extract-apks,get-device-specyget-size totalde las secciones «7» y «8», y la definición del APK Set. - adb — documentación oficial — https://developer.android.com/tools/adb
Consultado el 12 de agosto de 2026. De aquí sale
adb install-multiplede la «sección 7 · Instalar sin fusionar frente a fusionar» y la confirmación de que la documentación no publica su sintaxis completa de parámetros. - APK expansion files (OBB) — https://developer.android.com/google/play/expansion-files Consultado el 12 de agosto de 2026. De aquí salen el formato de nombre, la ruta de instalación, el límite de 2 GB y el estado de obsolescencia de la «sección 5.4 · Los OBB heredados».
services/core/java/com/android/server/pm/PackageInstallerSession.java(etiquetaandroid-17.0.0_r1) — https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/services/core/java/com/android/server/pm/PackageInstallerSession.java Consultado el 25 de septiembre de 2026. De aquí sale queisSplitRequiredhace fallar la instalación conINSTALL_FAILED_MISSING_SPLIT(«sección 4.2 · Los atributos del manifiesto»).- bundletool —
ModuleSplit.java— https://github.com/google/bundletool/blob/master/src/main/java/com/android/tools/build/bundletool/model/ModuleSplit.java Consultado el 25 de septiembre de 2026. De aquí sale que los splits de bibliotecas nativas se crean sin tabla de recursos (forNativeLibraries,setResourceTable = false), y por tanto sinresources.arsc(«sección 6 · Detección por estructura, nunca por extensión»).