Anatomía de un APK

panorama · Revisado el 12 de agosto de 2026

1. Qué es un APK

Un APK (Android Package) es un fichero ZIP con una convención encima. No es un formato nuevo, no es un ZIP «modificado» y no lleva cifrado: cualquier unzip lo abre, y lo que sale son ficheros normales colocados en rutas concretas. Toda la especificidad de Android está en qué entradas tiene que haber y en qué formato está el contenido de cada una.

Este documento introduce el vocabulario del corpus. Cada término técnico se explica aquí en una frase; la definición completa de todos ellos está en el glosario, que se puede leer en paralelo.

Cuatro condiciones lo convierten en instalable. Ninguna es opcional:

Condición Dónde vive Qué pasa si falta
Un AndroidManifest.xml en la raíz, en formato binario AXML raíz del ZIP El instalador no sabe ni cómo se llama el paquete. INSTALL_PARSE_FAILED_NO_CERTIFICATES y familia
Código ejecutable en classes.dex (y classes2.dex, classes3.dex…), en formato DEX raíz del ZIP La aplicación instala pero no arranca, salvo que no tenga código
Una tabla de recursos resources.arsc, si se usa algún recurso raíz del ZIP Los identificadores del DEX no resuelven a nada
Una firma válida con un esquema que la versión de Android acepte META-INF/ y/o el APK Signing Block El sistema rechaza el paquete antes de mirar nada más

Las dos palabras que hay que retener de esa tabla son binario y convención.

Binario, porque el manifiesto y los recursos no son texto. Descomprimir un APK y hacer cat AndroidManifest.xml devuelve basura: los primeros bytes son 03 00 08 00, la cabecera de un chunk RES_XML_TYPE, no <?xml. Lo mismo con cualquier .xml bajo res/. La conversión a texto legible es una reconstrucción, y es el nivel 2 de la sección 4.

Convención, porque nada en el formato ZIP obliga a que exista classes.dex. Lo obliga el cargador de Android. Esa distinción importa mucho al analizar: un ZIP puede tener dos entradas con el mismo nombre, o entradas cuya cabecera local contradiga al índice del final, y el formato lo permite aunque Android no lo tolere. La historia de las dos vulnerabilidades más conocidas de Android —Master Key y Janus— es exactamente la historia de dos implementaciones que leyeron el mismo ZIP y no coincidieron; está contada en Contenedor ZIP, sección 12.

Lo que un APK no es, y conviene fijarlo pronto:

  • No es un AAB. El Android App Bundle es un formato de publicación, no de instalación, y por dentro usa Protocol Buffers en vez de los formatos binarios de Android. Ver la sección 2.8 y AAB, splits y contenedores.
  • No es necesariamente un fichero. Desde el App Bundle, una aplicación instalada suele ser un base.apk más varios split. Lo que se descarga de un sitio de terceros con extensión .xapk, .apks o .apkm es un ZIP que contiene varios APK.
  • No contiene el .odex, el .vdex ni el .oat. Esos los genera dex2oat en el dispositivo al instalar. Ver Artefactos auxiliares, sección 5.

2. El ciclo de build

2.1 El diagrama

Tres cadenas independientes que convergen en un ZIP, y dos pasos finales sobre el ZIP ya formado. El orden de esos dos últimos pasos —alinear y después firmar— es la fuente número uno de APK rotos.

 RECURSOS                          CÓDIGO JVM                   CÓDIGO NATIVO
 ────────                          ──────────                   ─────────────
 res/                              .kt  .java                   .c  .cpp
 AndroidManifest.xml                 │                            │
   │                                 ▼                            ▼
   │                          kotlinc / javac                CMake / ndk-build
   ▼                                 │                            │
 aapt2 compile                       ▼                            ▼
   │                              .class                    lib/<abi>/*.so
   ▼                                 │                            │
 *.flat                              ▼                            │
   │                             D8  o  R8                        │
   ▼                                 │  └── mapping.txt           │
 aapt2 link  ──── R.java ────────────┤                            │
   │  -I android.jar                 ▼                            │
   ▼                          classes*.dex                        │
 AndroidManifest.xml (AXML)          │                            │
 resources.arsc                      │                            │
 res/**  ya compilados               │                            │
   │                                 │                            │
   └─────────────────┬───────────────┴────────────────────────────┘
                     ▼
              empaquetado ZIP                assets/  META-INF/  baseline.prof
                     │
                     ▼
              zipalign  -P 16  4
                     │
                     ▼
              apksigner sign            ← v1, v2, v3, v3.1 (+ .idsig para v4)
                     │
                     ▼
                  APK final

Quien lo lea de derecha a izquierda tiene el mapa de la ingeniería inversa: cada flecha es una transformación, y la sección 4 clasifica cuáles se pueden deshacer y cuáles no.

aapt2 (Android Asset Packaging Tool) es la herramienta oficial que convierte res/ y AndroidManifest.xml en los formatos binarios del dispositivo. Trabaja en dos fases separadas, y esa separación es toda la diferencia con el aapt original:

«AAPT2 supports faster compilation of resources by enabling incremental compilation. To accomplish incremental compilation, resource processing is separated into two steps: Compile: compiles resource files into binary formats. Link: merges all compiled files and packages them to a single package.»

aapt2 compile toma un fichero de recurso por invocación y produce un intermedio con extensión .flat. Lo de res/values/ sale como *.arsc.flat; todo lo demás, como XML binario *.flat; y los .png se pasan además por el crunch salvo que se le diga --no-crunch.

aapt2 link funde todos los .flat, resuelve las referencias contra android.jar (la opción -I) y emite tres cosas: la tabla resources.arsc, el AndroidManifest.xml ya en AXML, y —fuera del APK— el fichero R.java con las constantes que el compilador de Java va a necesitar. La documentación es explícita sobre lo que no produce: «the generated APK does not contain DEX bytecode and is unsigned. You can't deploy this APK to a device». Al final de esta rama hay un APK a medio hacer.

2.3 El código: javac/kotlinc, y luego D8 o R8

El compilador de Kotlin o de Java produce bytecode de la JVM en ficheros .class, uno por clase, big-endian y con una tabla de constantes propia por fichero. Android no ejecuta eso.

D8 convierte ese bytecode a DEX: un solo fichero con todas las clases y las tablas de cadenas, tipos, prototipos, campos y métodos compartidas. El porqué de ese cambio de reparto está en Formato DEX, sección 1.

R8 es D8 más el trabajo que antes hacía ProGuard, en una sola pasada. La documentación oficial lo divide en tres operaciones: code shrinking (tree shaking) —partiendo de los puntos de entrada declarados en el manifiesto, construye el grafo de código alcanzable y borra el resto—, logical optimizationsinlining de métodos y fusión de clases— y obfuscation (minification), que acorta los nombres de clases, campos y métodos: «com.example.MyActivity could become a.b.a».

De ahí sale mapping.txt, el fichero con la correspondencia entre los nombres originales y los ofuscados. Sin él, la ofuscación es irreversible en la práctica; con él, es reversible de forma determinista — es lo que hace retrace. Ver R8 y ProGuard.

Dos cambios recientes que conviene tener presentes porque la documentación antigua circula mucho: desde AGP 8.0 el modo full mode de R8 está activo por defecto, y desde AGP 9.3 la configuración se declara en un bloque optimization { enable = true } en vez de con isMinifyEnabled / isShrinkResources, con las reglas keep en ficheros .keep del source set src/<variant>/keepRules. El DSL antiguo sigue soportado.

2.4 El código nativo

Lo que se compila con el NDK no pasa por ninguna de las dos ramas anteriores: sale como bibliotecas compartidas ELF y se copia a lib/<abi>/ dentro del ZIP, un directorio por arquitectura. Es la única parte del APK que no es un formato propio de Android. Ver Código nativo y ELF.

2.5 Empaquetado, alineación y firma

Los tres productos se meten en un ZIP junto con assets/, los perfiles de arranque y los metadatos de build, y sobre ese ZIP ya cerrado actúan dos herramientas más.

zipalign no reordena nada ni recomprime: ajusta el relleno para que los datos de las entradas sin comprimir empiecen en un offset alineado, y así el sistema pueda mapearlas con mmap(2) en vez de copiarlas a RAM. Dos alineaciones distintas y ambas necesarias: 4 bytes para toda entrada stored, y 16 KiB (-P 16) para los .so.

apksigner firma. Y aquí está la regla que hay que memorizar, en palabras de la documentación oficial:

«If you use apksigner, zipalign must be used before the APK file has been signed. If you sign your APK using apksigner and make further changes to the APK, its signature is invalidated.»

El motivo es estructural: desde el esquema v2 la firma cubre todos los bytes del fichero, no las entradas del ZIP. Mover un byte después de firmar la invalida. Ver Alineación y zipalign y Esquemas v1, v2 y v3.

2.6 El ciclo entero, ejecutado

Todo lo anterior se puede hacer sin Gradle y sin Android Studio, con las build-tools que ya están en el SDK. Lo siguiente está ejecutado el 12 de agosto de 2026 con build-tools 37.0.0 (aapt2 2.20-15087165, D8 9.2.4-dev, apksigner 0.9), javac 21.0.10 y platforms/android-35, sobre un proyecto mínimo de una Activity y un string.

BT=~/Library/Android/sdk/build-tools/37.0.0
AJ=~/Library/Android/sdk/platforms/android-35/android.jar

# 1-2 · recursos: compilar y enlazar (produce AXML + resources.arsc + R.java)
$BT/aapt2 compile --dir src/res -o out/res.zip
$BT/aapt2 link -o out/base.apk -I $AJ --manifest src/AndroidManifest.xml \
    --java out/gen --min-sdk-version 24 --target-sdk-version 35 out/res.zip

# 3-4 · código: javac produce bytecode de la JVM, D8 lo convierte a DEX
javac --release 17 -cp $AJ -d out/classes src/java/MainActivity.java \
    out/gen/com/ejemplo/demo/R.java
$BT/d8 --release --min-api 24 --lib $AJ --output out/ out/classes/com/ejemplo/demo/*.class

# 5 · empaquetar: el DEX entra en el APK que dejó aapt2
cp out/base.apk out/app-unaligned.apk
(cd out && zip -q -X app-unaligned.apk classes.dex)

# 6 · alinear · 7 · firmar
$BT/zipalign -P 16 -f -v 4 out/app-unaligned.apk out/app-aligned.apk
keytool -genkeypair -keystore out/demo.jks -storepass pruebas123 -keypass pruebas123 \
    -alias demo -keyalg RSA -keysize 2048 -validity 30 -dname "CN=Ejemplo Demo, C=ES"
cp out/app-aligned.apk out/app-release.apk
$BT/apksigner sign --ks out/demo.jks --ks-pass pass:pruebas123 \
    --key-pass pass:pruebas123 --ks-key-alias demo out/app-release.apk

# 8 · verificar
$BT/apksigner verify -v --v4-signature-file out/app-release.apk.idsig out/app-release.apk

De aapt2 compile sale un solo intermedio, values_strings.arsc.flat; de aapt2 link, un APK con dos entradas (AndroidManifest.xml de 1.224 bytes y resources.arsc de 564) más el R.java, donde ya se ve la estructura 0xPPTTEEEE del resource ID — paquete 7f, tipo 01, entrada 0000:

public final class R {
  public static final class string { public static final int app_name=0x7f010000; }
}

dexdump -f sobre el classes.dex recién producido responde DEX version '037'. No es arbitrario: la versión la elige D8 a partir de --min-api 24, y la correspondencia completa está medida en Formato DEX, sección 4. Y la verificación final:

Verifies
Verified using v1 scheme (JAR signing): false
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme (APK Signature Scheme v3.1): false
Verified using v3.2 scheme (APK Signature Scheme v3.2): false
Verified using v4 scheme (APK Signature Scheme v4): true

Tres observaciones sobre esa salida, las tres del tipo que se aprende tarde:

  1. apksigner generó un .idsig sin pedírselo. La firma v4 vive en un fichero acompañante, fuera del APK, y apksigner verify no la mira salvo que se le pase --v4-signature-file.
  2. v1: false no significa que no haya firma v1. El APK resultante contiene META-INF/MANIFEST.MF, META-INF/DEMO.SF y META-INF/DEMO.RSA. Lo que dice apksigner es que, con minSdkVersion 24, la verificación no necesita v1: repitiendo la orden con --min-sdk-version 21 responde v1: true.
  3. El APK alineado pesa 2.293 bytes y el firmado 8.587. La firma no es un adorno.

El resultado tiene seis entradas —AndroidManifest.xml, resources.arsc, classes.dex y los tres de META-INF/— y es exactamente la anatomía mínima de la sección 1.

2.7 Los nombres de las tareas de Gradle

En un proyecto real nada de lo anterior se invoca a mano: lo orquesta el Android Gradle Plugin, cuya versión estable el 12 de agosto de 2026 es 9.3.1. Los nombres de tarea documentados son tres, y los tres llevan el nombre de la variante en camel case:

Tarea Produce
assemble<Variante>assembleDebug, assembleRelease Un APK en <módulo>/build/outputs/apk/
install<Variante>installDebug Lo mismo, y lo instala en el dispositivo conectado
bundle<Variante>bundleRelease, :base:bundleDebug Un AAB

Cada una de ellas dispara por debajo una cadena de tareas internas —compilación de Kotlin, fusión de recursos, invocación de R8, empaquetado— cuyos nombres no forman parte del contrato público y han cambiado varias veces entre versiones de AGP. Este documento no los cita a propósito: ./gradlew tasks los lista para la versión que se tenga delante, que es la única respuesta que no envejece. La build de depuración se firma sola con una clave de depuración y sale ya alineada; la de release exige clave propia.

2.8 Dónde encaja el AAB

Desde agosto de 2018 existe el Android App Bundle, y desde agosto de 2021 es obligatorio para las aplicaciones nuevas de Google Play. Cambia quién genera el fichero instalable:

«An Android App Bundle is a publishing format that includes all your app's compiled code and resources, and defers APK generation and signing to Google Play.»

El desarrollador sube un .aab. Play lo procesa y genera del lado del servidor el conjunto de APK que cada dispositivo concreto necesita: un base.apk más los split de su ABI, su densidad y su idioma. El resto ni se descarga.

   desarrollador                 Google Play                    dispositivo
 ┌────────────────┐          ┌──────────────────┐          ┌────────────────┐
 │  bundleRelease │          │  generación de   │          │   base.apk     │
 │       │        │          │  splits + firma  │          │ + config.arm64 │
 │       ▼        │  .aab    │  con la clave    │  splits  │ + config.xxhdpi│
 │   app.aab      │ ───────► │  de Play App     │ ───────► │ + config.es    │
 │  (protobuf)    │          │  Signing         │          │                │
 └────────────────┘          └──────────────────┘          └────────────────┘

Dos consecuencias que atraviesan todo el corpus:

  • La firma que ve el usuario no es la del desarrollador. Con Play App Signing, Play re-firma cada APK generado. La clave que el desarrollador guarda es la de upload.
  • Una aplicación instalada ya no es un fichero. Analizarla exige o bien recoger todos sus splits del dispositivo, o bien fusionarlos en un APK único, que es una operación no trivial: hay que unir tablas de recursos, sanear el manifiesto y renumerar los DEX. Ver Fusión de splits.

La misma generación se puede reproducir en local con bundletool, que es la herramienta que usan Android Studio, AGP y el propio Play. Sobre el .aab de prueba del corpus:

java -jar ~/herramientas/bundletool.jar \
    build-apks --bundle=$CORPUS/splits/com.ejemplo.fixture.aab --output=fixture.apks
unzip -l fixture.apks | head -8
     3166  toc.pb
    25551  splits/base-arm64_v8a.apk
    25551  splits/base-arm64_v8a_2.apk
    23037  splits/base-armeabi_v7a.apk
     1283  splits/base-de.apk
     1283  splits/base-es.apk
     1287  splits/base-fr.apk
     1484  splits/base-hdpi.apk

toc.pb es la tabla de contenidos en Protocol Buffers que describe a qué dispositivo va cada split. Su presencia distingue un APK Set genuino de un ZIP de splits con la extensión puesta a mano — y la extensión miente con frecuencia: 14 de los 16 ficheros .apks del corpus de verificación no llevan toc.pb. El detalle completo, en AAB, splits y contenedores, sección 6.

3. Inventario comentado de un APK real

Todo lo de esta sección está ejecutado el 12 de agosto de 2026 sobre org.fdroid.fdroid_1023052.apk del corpus (F-Droid 1.23.2, minSdk 23, targetSdk 30).

3.1 Lo primero: es un ZIP

CORPUS=~/corpus-apk
unzip -l $CORPUS/org.fdroid.fdroid_1023052.apk | tail -3
  4103288  01-01-1981 01:01   resources.arsc
---------                     -------
 23660746                     1119 files

Mil ciento diecinueve entradas. Y esa fecha, 01-01-1981 01:01, no es la de compilación: es la constante que el Android Gradle Plugin escribe en todas las entradas para que la salida sea reproducible. Es idéntica en los 58 APK standalone del corpus.

3.2 Las categorías

Agrupando las 1.119 entradas por su prefijo de ruta, con el tamaño sin comprimir y el método de compresión de cada grupo:

categoria                                  nº bytes sin compr.   metodo
1  AndroidManifest.xml                      1          48.248   1 deflate
2  classes*.dex                             2      17.839.628   2 deflate
3  resources.arsc                           1       4.103.288   1 stored
4  res/                                  1067       1.218.209   852 deflate · 215 stored
5  lib/<abi>/                               4          37.392   4 stored
6  assets/dexopt/ (baseline profile)        2           9.915   2 stored
7  assets/                                  6           9.710   3 deflate · 3 stored
8  META-INF/ firma v1                       3         204.631   3 deflate
9  META-INF/ resto                         19          23.308   19 deflate
10 raiz y otros                            14         166.417   14 deflate

3.3 Categoría por categoría

1 · AndroidManifest.xml — Una sola entrada, en la raíz, en formato AXML. Sus primeros bytes lo confirman:

unzip -p $CORPUS/org.fdroid.fdroid_1023052.apk AndroidManifest.xml | xxd -l 16
00000000: 0300 0800 78bc 0000 0100 1c00 4049 0000  ....x.......@I..

03 00 es RES_XML_TYPE, 08 00 el tamaño de la cabecera del chunk y 78 bc 00 00 el tamaño total del fichero. Es la misma estructura de chunks que usa resources.arsc. Ver XML binario (AXML), sección 2.

2 · classes*.dex — Aquí, dos ficheros. El límite de 65.536 referencias a método por fichero DEX obliga a repartir las clases en classes.dex, classes2.dex, classes3.dex… sin huecos en la numeración. El orden es semántico: si una clase se define en dos ficheros, gana la del primero en orden de carga. Ver Formato DEX, secciones 1 y 15, y el juego de instrucciones en Bytecode Dalvik.

3 · resources.arsc — La tabla que convierte cada resource ID de 32 bits en el valor que corresponde a la configuración del dispositivo. Está stored, sin comprimir: desde targetSdkVersion 30 es obligatorio, porque el sistema la mapea en memoria. Ver Tabla de recursos.

4 · res/ — Los 1.067 ficheros de recurso ya compilados. Los .xml de aquí no son texto: son AXML, igual que el manifiesto. El reparto de compresión no es caprichoso: los 852 .xml van deflate y los 215 stored son 214 .png y un .ogg, formatos que ya vienen comprimidos y a los que deflate no les quitaría nada. El nombre del fichero se conserva, y ese es justo el detalle que la ofuscación de recursos ataca: en una aplicación ofuscada se ven rutas como res/AB/x1.xml.

5 · lib/<abi>/ — Aquí solo cuatro .so, uno por arquitectura, todos del mismo libandroidx.graphics.path.so. Cuatro directorios significa cuatro ABIs empaquetadas, no necesariamente cuatro soportadas de verdad. Ver Código nativo y ELF, sección 2.

6 · assets/dexopt/baseline.prof y baseline.profm: la lista de métodos que conviene compilar por adelantado para que la aplicación arranque rápido la primera vez. Van siempre en pareja, y están en 52 de los 58 APK standalone del corpus. Ver Artefactos auxiliares, sección 3.

7 · assets/ — Ficheros que se entregan tal cual, sin identificador de recurso. Se abren por ruta con AssetManager. Es un sistema de ficheros opaco dentro del APK, sin índice y sin restricciones de nombre, y por eso es donde acaban los modelos de aprendizaje automático, las bases de datos precargadas y —el caso que importa— el payload cifrado de los packer.

8 · META-INF/ firma v1MANIFEST.MF con un digest por entrada, CIARANG.SF con los digests de las secciones del anterior, y CIARANG.RSA con el bloque PKCS#7. El nombre base lo elige quien firma y no significa nada. Ver Esquemas v1, v2 y v3.

9 · META-INF/ resto — Metadatos que vienen de las dependencias y de AGP: services/ del mecanismo ServiceLoader, *.version de cada biblioteca AndroidX, app-metadata.properties con la versión del plugin y version-control-info.textproto con el hash de commit exacto del que salió el APK.

10 · Raíz y otros — Y aquí está lo interesante, que es lo que no encaja en ninguna convención:

DebugProbesKt.bin                 kotlin/kotlin.kotlin_builtins
kotlin-tooling-metadata.json      kotlin/collections/collections.kotlin_builtins
okhttp3/internal/publicsuffix/publicsuffixes.gz
org/bouncycastle/x509/CertPathReviewerMessages.properties
org/bouncycastle/x509/CertPathReviewerMessages_de.properties
version.properties

Leído sin abrir un solo DEX: la aplicación es Kotlin (los kotlin_builtins), usa corrutinas (DebugProbesKt.bin), usa OkHttp (y con él, casi seguro, Retrofit) y usa BouncyCastle para criptografía. Ese inventario es la huella del stack, y es gratis.

3.4 Lo que cambia en un APK más moderno

El mismo recuento sobre com.x8bit.bitwarden_2026.5.0.apk (targetSdk 36) da tres diferencias que no son cosméticas:

F-Droid 1.23.2 (targetSdk 30) Bitwarden (targetSdk 36)
classes*.dex deflate stored
lib/<abi>/ 4 entradas, stored 23 entradas, stored y alineadas a 16 KiB
Firma v1 Presente (3 entradas en META-INF/) Ausente: solo v2

La firma v1 desaparece cuando minSdkVersion es lo bastante alto: apksigner verify -v sobre Bitwarden responde v1: false / v2: true, sin más. Y la alineación a 16 KiB responde a un requisito con fecha: desde el 1 de febrero de 2027, Google Play no admite actualizaciones de aplicaciones con targetSdk 35 o superior que no soporten páginas de memoria de 16 KB en dispositivos de 64 bits.

4. Los cuatro niveles de «decompilar»

«Decompilar un APK» se usa para cuatro operaciones distintas que se diferencian en una cosa: cuánta información se está inventando el que las hace. Confundirlas es el error conceptual más caro del dominio, porque lleva a confiar en una salida que puede no corresponderse con lo que la aplicación hace.

4.1 Nivel 1 — Desempaquetar: unzip -d salida/ app.apk

Abrir el ZIP y sacar los ficheros. Sin pérdida y trivial: los bytes que salen son exactamente los que estaban. Lo único que se pierde es la información del propio contenedor —el orden de las entradas, el método de compresión, el relleno de alineación y el APK Signing Block, que no es una entrada del ZIP y por tanto unzip ni lo ve—. Quien pretenda volver a montar el APK necesita todo eso; quien solo quiera mirar dentro, no.

4.2 Nivel 2 — Decodificar: aapt2 dump xmltree / aapt2 dump resources

Convertir los formatos binarios de Android a algo legible: AXML a XML de texto, resources.arsc a un árbol res/values/. Reversible en el sentido que importa: la información está toda ahí y se puede volver a compilar. Lo que no se recupera es la forma del fuente —indentación, comentarios, si un booleano se escribió true o TRUE—, porque el compilador nunca la guardó. Dos manifiestos distintos pueden producir el mismo AXML, y un AXML admite varias reconstrucciones igualmente válidas.

4.3 Nivel 3 — Desensamblar: baksmali disassemble (o dexdump -d)

Convertir el DEX a smali, la sintaxis de texto del bytecode Dalvik. Fiel y uno a uno: cada instrucción del fichero es una línea del texto y viceversa, smali tiene inverso exacto —el ensamblador del mismo nombre— y por eso es el nivel al que se edita una aplicación de la que no se tiene el fuente. No se inventa nada. Lo que se pierde ya se había perdido al compilar: los nombres de las variables locales y los números de línea solo están si el debug_info_item sobrevivió, y ese es el primer campo que un ofuscador pone a cero.

⚠️ no ejecutado localmente: baksmali no está instalado en esta máquina; su sintaxis procede de la documentación del proyecto y se detalla en Desensambladores smali. Las salidas de dexdump de este documento sí están ejecutadas.

4.4 Nivel 4 — Decompilar: jadx -d salida/ app.apk

Convertir el DEX en algo que se parezca a Java. Esto no es una traducción: es una reconstrucción. El bytecode Dalvik está basado en registros y el compilador ya destruyó la estructura del fuente: los bucles son saltos, las constantes pequeñas se fundieron dentro de las instrucciones, las lambdas se convirtieron en clases sintéticas, los genéricos solo sobreviven si quedó la anotación dalvik.annotation.Signature. El decompilador tiene que inferir un programa Java cuyo bytecode sea equivalente al que ve, con tres consecuencias que hay que asumir:

  1. Puede fallar, y falla por método, no por fichero. Lo normal es obtener un .java con la mayoría de los métodos reconstruidos y unos cuantos sustituidos por un comentario de error y el smali original.
  2. Lo que sale no compila necesariamente. Y no siempre por un fallo de la herramienta: los nombres admisibles en un DEX son mucho más permisivos que los de Java, así que un ofuscador puede emitir identificadores que no existen como código Java válido.
  3. Los fallos son silenciosos si no se miran. Nadie avisa de qué porcentaje del código que se está leyendo no es lo que la aplicación hace.

4.5 Tabla resumen

Nivel Qué hace Herramienta típica Reversible Qué se pierde
1 · Desempaquetar ZIP → ficheros unzip, apktool d -s, APKEditor Sí, con cuidado Orden, compresión, alineación, APK Signing Block
2 · Decodificar AXML → XML, .arscres/values/ aapt2 dump, apktool, ARSCLib Formato del fuente: indentación, comentarios, mayúsculas
3 · Desensamblar DEXsmali baksmali, dexdump -d Sí, exactamente Nada nuevo: los nombres de locales y las líneas ya se habían perdido al compilar
4 · Decompilar DEX → Java jadx, dex2jar + Vineflower/CFR, JEB No La certeza. El resultado es una hipótesis, y puede fallar por método

4.6 El mismo método, en los cuatro niveles

Sobre el APK de ocho pasos de la sección 2.6, del que se conserva el fuente original:

// nivel 0 · el fuente, que en un APK ajeno no se tiene
android.util.Log.i("EJEMPLO", getString(R.string.app_name));
// nivel 3 · dexdump -d sobre classes.dex
0000: invoke-super {v0, v1}, Landroid/app/Activity;.onCreate:(Landroid/os/Bundle;)V
0003: const/high16 v1, #int 2130771968 // #7f01
0005: invoke-virtual {v0, v1}, L…/MainActivity;.getString:(I)Ljava/lang/String;
0008: move-result-object v0
0009: const-string v1, "EJEMPLO" // string@0010
000b: invoke-static {v1, v0}, Landroid/util/Log;.i:(…)I
000e: return-void

R.string.app_name ya no existe. Lo que queda es la constante 2130771968 = 0x7f010000 incrustada en la instrucción. El nombre del recurso no está en el DEX: está en la tabla de recursos, y es el nivel 2 el que lo devuelve.

// nivel 2 · aapt2 dump resources sobre el mismo APK
Package name=com.ejemplo.demo id=7f
  type string id=01 entryCount=1
    resource 0x7f010000 string/app_name
      () "Ejemplo Demo"

Esa es la lección de la sección entera: los niveles no son etapas de una escalera, son fuentes de información distintas, y el análisis serio los cruza. El nivel 4 solo puede escribir getString(R.string.app_name) porque alguien le ha dado la tabla del nivel 2.

El resto de la salida de dexdump sobre ese mismo método dice qué más se recuperó y qué no:

      positions   :  0x0000 line=4   ·   0x0005 line=5
      locals      :  reg=0 this L…/MainActivity;  ·  reg=1 (null) Landroid/os/Bundle;

Los números de línea sobrevivieron —d8 --release los conserva para que las trazas de pila sean legibles—, pero el nombre del parámetro es (null): se perdió. Un decompilador lo llamará bundle, arg0 o p1 según su heurística, y ninguna de las tres es el nombre que escribió el desarrollador. Y eso, en un APK sin ofuscar. Con R8 en modo completo se pierden además los nombres de clases, campos y métodos, y la única vía de vuelta es el mapping.txt, que no viaja dentro del APK.

5. Qué sobrevive siempre a la ofuscación

Aunque el código sea completamente ilegible —y en el corpus hay aplicaciones con el 100 % de los identificadores ofuscados—, hay cosas que no se pueden ocultar sin romper la aplicación. Es el suelo del análisis, y conviene tenerlo claro antes de invertir horas en decompilar:

Qué sobrevive Por qué no se puede ocultar
El nombre del paquete, la versión, minSdk y targetSdk El instalador los lee antes de ejecutar nada
Los permisos Los concede el sistema a partir del manifiesto. Un permiso no declarado no se obtiene
Los componentes exportados (activity, service, receiver, provider) Otras aplicaciones y el propio sistema tienen que poder resolverlos por nombre
La firma y la cadena de certificados Es lo que el sistema verifica. Es además el identificador estable del autor entre versiones
Los recursos y su estructura Se resuelven por identificador numérico en tiempo de ejecución. Los nombres se pueden ofuscar; la tabla, el conjunto de idiomas, de densidades y los valores, no
Las bibliotecas nativas y sus ABIs El cargador las busca en lib/<abi>/ por convención de ruta
Las llamadas a la API de Android Landroid/... no se puede renombrar: lo define el framework, no la aplicación
Las cadenas que llegan al sistema URLs, nombres de fichero y nombres de tabla se pueden cifrar en reposo, pero tienen que estar en claro en el momento de usarse

De ahí sale la regla práctica: el manifiesto, la firma, el inventario del ZIP y las llamadas al framework dan un perfil completo del comportamiento observable de la aplicación sin decompilar una sola línea. Un packer —un protector que sustituye el DEX original por un stub que descifra y carga el código real en ejecución— sigue teniendo que declarar sus permisos y sus componentes, y sigue teniendo que llamar a DexClassLoader desde algún sitio.

Lo que sí desaparece: los nombres de clases, campos y métodos propios; los nombres de las variables locales y de los parámetros; los números de línea y el nombre del fichero fuente; los genéricos; los nombres de las entradas de recurso; y la estructura del código, si hay aplanamiento de flujo de control. Ver R8 y ProGuard y Anti-análisis y hardening.

6. Mapa de lectura

Esta tabla organiza la lectura por pregunta concreta, con la sección de este documento que la enmarca.

Lo que quieres hacer Empieza aquí Y sigue por
Saber qué hace la aplicación sin tocar el código (§3, §5) Pipeline de análisis XML binario (AXML)
Averiguar qué es ese .xapk / .apks / .apkm (§2.8) AAB, splits y contenedores, §6 Fusión de splits
Leer el código — decide antes el nivel (§4) Decompiladores a Java (nivel 4) Desensambladores smali (nivel 3)
Modificar la aplicación y que se instale (§2.5) Pipeline de modificación Firma y empaquetado
Escribir un parser Contenedor ZIP Código fuente de referencia
Enfrentarte a código ofuscado (§5) R8 y ProGuard Deofuscación
Entender por qué ha fallado algo Diagnóstico de fallos
Elegir herramienta, y saber si sigue viva Mapa del ecosistema Comparativa y selección
Buscar una palabra Glosario

Fuentes

  1. Build your app from the command line — Android Developers — https://developer.android.com/build/building-cmdline Consultado el 12 de agosto de 2026. De aquí salen los nombres de tarea assembleDebug, assembleRelease y bundle<Variante> (sección 2.7), el flujo de aapt2 --proto-format hacia bundletool build-bundle (2.8) y la generación de claves con keytool (2.6).
  2. AAPT2 — Android Developers — https://developer.android.com/tools/aapt2 Consultado el 12 de agosto de 2026. De aquí salen la cita sobre las dos fases compile y link, la tabla de tipos de fichero .flat, la nota de que el APK que produce link no contiene DEX ni firma, y las opciones --no-crunch, -I y --java (sección 2.2).
  3. d8 — Android Developers — https://developer.android.com/tools/d8 Consultado el 12 de agosto de 2026. De aquí salen la definición de D8 y las opciones --release, --min-api y --lib (secciones 2.3 y 2.6).
  4. Enable app optimization with R8 — Android Developers — https://developer.android.com/topic/performance/app-optimization/enable-app-optimization Consultado el 12 de agosto de 2026; la URL developer.android.com/build/shrink-code redirige aquí. De aquí salen las tres operaciones de R8, el ejemplo com.example.MyActivitya.b.a, el full mode por defecto desde AGP 8.0 y el bloque optimization {} de AGP 9.3 (sección 2.3).
  5. Android Gradle plugin 9.3.0 release notes (julio de 2026) — https://developer.android.com/build/releases/gradle-plugin Consultado el 12 de agosto de 2026. De aquí sale que la rama estable de AGP el día de la consulta es la 9.3, con Gradle 9.5.0, SDK Build Tools 36.0.0 y JDK 17 (sección 2.7).
  6. zipalign — Android Developers — https://developer.android.com/tools/zipalign Consultado el 12 de agosto de 2026. De aquí salen la cita sobre mmap(2), la regla de -P 16 para los .so y el orden respecto a apksigner (sección 2.5).
  7. apksigner — Android Developers — https://developer.android.com/tools/apksigner Consultado el 12 de agosto de 2026. De aquí salen la sintaxis de sign y verify y la advertencia de que modificar el APK tras firmarlo invalida la firma (secciones 2.5 y 2.6).
  8. About Android App Bundles — Android Developers — https://developer.android.com/guide/app-bundle Consultado el 12 de agosto de 2026. De aquí sale la definición citada del AAB como formato de publicación que difiere la generación y la firma de los APK a Google Play (sección 2.8).
  9. Corpus de verificación — 5,9 GB, 86 contenedores y 717 APK: 58 sueltos de F-Droid y 659 dentro de contenedores de splits. Medido el 12 de agosto de 2026. De aquí salen íntegras las secciones 3.1 a 3.4, la salida de bundletool build-apks de la sección 2.8 y el dato de los 14 de 16 .apks sin toc.pb.
  10. Ejecución local del ciclo de build y de los cuatro nivelesbuild-tools 37.0.0 (aapt2 2.20-15087165, D8 9.2.4-dev, apksigner 0.9, zipalign, dexdump), javac 21.0.10, platforms/android-35 y bundletool 1.18.3, el 12 de agosto de 2026. De aquí salen todas las salidas de las secciones 2.6, 2.8, 4.2 y 4.6.
  11. Documentos de formatos de este corpus10-formatos/ Consultados el 12 de agosto de 2026. La sección 3 es un resumen de los ocho, y cada categoría del inventario enlaza al que la desarrolla; los datos de compresión, alineación, baseline profiles, ABIs, límite de 65.536 referencias, packers y firma de las secciones 1, 3.3, 3.4 y 5 proceden de ellos.