Decompiladores a Java
Antes conviene leer «Anatomía de un APK»
1. Qué es y por qué existe
Un APK no lleva dentro código fuente. Lleva classes*.dex: bytecode Dalvik, un juego de
instrucciones basado en registros que describe qué hace el programa pero no cómo se escribió.
Decompilar a Java es el intento de recorrer ese camino al revés — de las instrucciones a un
texto que un humano pueda leer como si fuera el original.
La palabra clave es intento. La compilación de Java o Kotlin a DEX es una función que
pierde información a propósito: descarta nombres, aplana construcciones del lenguaje,
reordena instrucciones y borra tipos. Un decompilador no invierte esa función porque no es
invertible; produce un programa distinto que se comporta igual. Cuando el decompilador
acierta, la diferencia es cosmética y el lector no la nota. Cuando falla, el resultado va
desde un método que no compila hasta —el caso peligroso— un método que compila, parece
razonable y no dice lo que la aplicación hace.
Este documento cubre las herramientas que hacen ese trabajo y, sobre todo, dónde fallan.
Para el nivel de abajo —el bytecode que se está decompilando— está
«Bytecode Dalvik». Para el nivel intermedio, que es
fiel en lugar de reconstruido, está
«Desensambladores smali».
2. Qué se pierde, y por qué no vuelve
Antes de comparar herramientas conviene fijar el techo. Ninguna de las de este documento puede recuperar lo que sigue, porque no está en el fichero.
2.1 Los genéricos
La JVM implementa los genéricos por borrado de tipos (type erasure): en tiempo de
ejecución List<String> y List<Integer> son la misma clase. Parte de la información
sobrevive en el atributo Signature, que D8 propaga al DEX como la anotación
dalvik.annotation.Signature, y los decompiladores buenos la leen. Pero es un atributo
opcional: si R8 lo descarta —lo hace cuando nadie lo consulta por reflexión y no se ha
pedido -keepattributes Signature—, la firma genérica desaparece sin dejar rastro y el
decompilador solo puede escribir el tipo crudo.
Consecuencia práctica: un Map<String, List<Usuario>> puede salir como Map. El programa
sigue siendo correcto; la intención del autor, no.
2.2 El azúcar sintáctico
Todo lo que el compilador de Java o de Kotlin traduce a construcciones más simples se pierde como forma, aunque se conserve como comportamiento:
| En el fuente | En el DEX |
Qué recupera el decompilador |
|---|---|---|
for (X x : lista) |
Bucle con Iterator explícito |
El for extendido, casi siempre |
switch sobre String |
Dos switch: uno sobre hashCode(), otro sobre el índice |
A veces; si no, dos switch anidados sin sentido aparente |
| Lambda / referencia a método | Clase sintética, o invoke-custom con invoke-polymorphic |
Depende del decompilador; es de lo más frágil |
try-with-resources |
try/finally anidados con addSuppressed |
Rara vez; suele salir el finally expandido |
| Autoboxing | Integer.valueOf(...) |
Se ve la llamada, no el azúcar |
Concatenación con + |
StringBuilder, o invokedynamic con StringConcatFactory |
Normalmente sí |
| Clases internas | Clases separadas con this$0 y accesores sintéticos |
Se reconstruyen, con cicatrices |
enum |
Clase con campos estáticos y array $VALUES |
Normalmente sí |
Nada de esto es un fallo del decompilador. Es que la información no viaja.
2.3 Los nombres de las variables locales
Los nombres de parámetros y de variables locales viven en debug_info_item, dentro del
code_item de cada método. Es una estructura opcional. Una compilación de release con
R8 la elimina casi siempre, y cuando no la elimina R8, la elimina el flag --no-debug-info
de quien haya reempaquetado la aplicación por el camino.
Sin debug_info, el decompilador tiene que inventar nombres. jadx los deriva del tipo y del
número de registro (str, i, list2, obj), lo cual es legible pero no es el nombre
original y no debe citarse como tal. Los nombres de campos y de métodos son otra cosa: esos
sí están en las tablas del DEX, salvo que R8 los haya ofuscado — y entonces el problema
ya no es la decompilación sino la ofuscación, que trata
«R8 y ProGuard».
Puede comprobarse sobre el ejemplo mínimo de la «sección 5 de
Desensambladores smali · La misma clase en smali, comentada»: compilado con javac -g, el
debug_info conserva los nombres; compilado sin -g, los registros aparecen sin nombre.
2.4 El orden real del código
R8 no se limita a renombrar. Hace inlining de métodos pequeños, outlining de secuencias
repetidas a un método sintético compartido, elimina ramas muertas, fusiona clases y mueve
código entre métodos. Lo que sale del decompilador es el programa después de esas
transformaciones.
Esto tiene un efecto que sorprende al que llega: un método que en el fuente tenía diez líneas puede aparecer con doscientas porque le han inyectado el cuerpo de cinco llamadas; y un método que en el fuente existía puede no aparecer en absoluto. Buscar «el método que hace X» en la salida de un decompilador y no encontrarlo no prueba que la aplicación no haga X.
2.5 El salto de modelo
El bytecode de la JVM está basado en pila; el de Dalvik, en registros. Los decompiladores de
Java —CFR, Vineflower, Procyon, Fernflower— están escritos para el primero. Para usarlos sobre
un APK hace falta una traducción previa de registros a pila (dex2jar), y esa traducción es
otra fuente de pérdida y de error, encadenada a la primera. jadx evita el rodeo porque lee
DEX de forma nativa; es su ventaja estructural.
3. jadx
3.1 Ficha
| Dato | Valor |
|---|---|
| Repositorio | https://github.com/skylot/jadx |
| Licencia | Apache-2.0 |
| Lenguaje | Java |
| Versión estable | 1.5.6, publicada el 10 de julio de 2026 |
| Último push al repositorio | 5 de agosto de 2026 |
| Creado | 18 de marzo de 2013 |
| Tracción | 50.030 ★ · 5.719 forks |
| Issues abiertas | 441 |
| Archivado | No |
Consultado el 12 de agosto de 2026 vía la API de GitHub. Activo y con cadencia sostenida: las últimas releases son 1.5.4 (13 de febrero de 2026), 1.5.5 (25 de febrero de 2026) y 1.5.6 (10 de julio de 2026). Trece años de historia, licencia permisiva y el mayor número de estrellas de todo el instrumental de este bloque. Es la referencia del sector y no está cerca de dejar de serlo.
Las 441 issues abiertas no son señal de abandono sino de superficie: un decompilador falla por definición en algún porcentaje de los métodos, y cada fallo es un caso concreto que alguien reporta.
3.2 La arquitectura, en cuatro etapas
jadx lee DEX directamente y no pasa por .class. El recorrido conceptual es:
┌──────────────┐ plugin de entrada
│ classes.dex │ dex-input (también .apk), smali-input, java-convert…
└──────┬───────┘
▼
┌──────────────────────────────────────────────┐
│ 1. Carga: tablas del DEX a un modelo de │
│ clases, campos, métodos e instrucciones │
└──────┬───────────────────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ 2. IR: grafo de bloques básicos, forma SSA e │
│ inferencia de tipos sobre los registros │
└──────┬───────────────────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ 3. Reconstrucción de control de flujo: del │
│ grafo de saltos a regiones anidadas — │
│ if, bucles, switch, try/catch │
└──────┬───────────────────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ 4. Generación de código Java │
└──────────────────────────────────────────────┘
La etapa 3 es donde se juega todo. El DEX no tiene bucles ni condicionales: tiene saltos.
Recuperar una estructura anidada a partir de un grafo arbitrario no siempre es posible con
las construcciones del lenguaje Java, y ese es el origen directo de los modos de fallo de la
«sección 3.5 · Los modos de fallo, y qué significa cada uno».
Las cuatro etapas son las del propio código. En jadx 1.5.6, Jadx.getRegionsModePasses()
(jadx-core/src/main/java/jadx/core/Jadx.java, líneas 123-216) ordena las pasadas en tres
representaciones intermedias que el fichero marca con comentarios —// instructions IR,
// blocks IR y // regions IR— y termina en PrepareForCodeGen: la etapa 1 acaba en
ProcessInstructionsVisitor; la 2 son BlockSplitter, BlockProcessor, BlockFinisher,
SSATransform y TypeInferenceVisitor; la 3 empieza en RegionMakerVisitor y sigue con
IfRegionVisitor, LoopRegionVisitor y compañía; y la 4 es el paquete jadx.core.codegen
(ClassGen, MethodGen, RegionGen, InsnGen). Los modos -m simple y -m fallback
usan listas más cortas, sin la etapa 3.
3.3 jadx-gui
El proyecto distribuye dos binarios: jadx (línea de órdenes) y jadx-gui (interfaz
gráfica). La interfaz no es un envoltorio de la CLI: decompila bajo demanda, clase a clase, lo
que la hace utilizable sobre aplicaciones donde la decompilación completa tardaría minutos.
Aporta además búsqueda global por texto, clase, método o campo; navegación por referencias
(«quién llama a esto»); renombrado interactivo con persistencia; y la vista del smali del
método al lado del Java, que es la forma práctica de comprobar si el Java es fiel.
Esa vista lado a lado es la respuesta operativa a todo lo de la «sección 2 · Qué se pierde, y por qué no vuelve»: cuando el Java no
cuadra, se mira el smali, que no miente.
3.4 Plugins
Desde la rama 1.5 jadx tiene un sistema de plugins con gestión desde la CLI. Los de entrada determinan qué formatos acepta; estos son los que trae la 1.5.6, según su propio código:
| Plugin | Qué añade |
|---|---|
dex-input |
.dex y .apk, con verificación opcional de checksum |
java-convert |
.class, .jar y .aar, convirtiéndolos a DEX con dx o d8 |
java-input |
.class y .jar, cargados directamente, sin convertir |
smali-input |
Ficheros .smali, con nivel de API configurable |
raung-input |
Ficheros .raung, el ensamblador de bytecode de Java de Raung |
aab-input · apks-input · apkm-input · xapk-input |
Los contenedores: .aab, .apks, .apkm y .xapk se abren directamente, sin fusionar nada antes |
Otros tres no añaden formatos, sino pasadas sobre el código:
| Plugin | Qué hace |
|---|---|
kotlin-metadata |
Lee la anotación kotlin.Metadata para mejorar la salida de Kotlin |
kotlin-smap |
Usa SourceDebugExtension para renombrar alias de clases |
rename-mappings |
Aplica ficheros de mapping externos |
La gestión se hace con el subcomando plugins: -l/--list lista los instalados,
-a/--available los del marketplace, -i/--install <locationId> instala y --uninstall <pluginId> desinstala.
El kotlin-metadata importa más de lo que parece. Una parte grande del Android moderno es
Kotlin, y sin esa anotación el decompilador escribe el Java equivalente —con null checks
inyectados, clases Companion, funciones de extensión convertidas en estáticas— que es
correcto y bastante ilegible.
3.5 Los modos de fallo, y qué significa cada uno
Es la parte más útil de conocer, porque el lector se los va a encontrar.
Sobre los marcadores literales. Circulan por foros y por documentación de terceros dos cadenas que no existen en jadx:
// decompilation failedy// JADX WARNING: inconsistent code. Las reales son las de abajo, verificadas contra el árbol del proyecto. La «sección 6 de Diagnóstico de fallos · jadx» explica por qué la segunda no se puede encontrar grepeando el repositorio: se compone en ejecución.
/* JADX ERROR: <mensaje> */ — con dos espacios tras /*, que es como lo escribe
codegen/utils/CodeGenUtils.java. La etapa 3 no consiguió estructurar el método. Si hay una
causa asociada, la traza de pila Java completa va incrustada dentro del comentario. Según
la configuración, debajo aparece el volcado de instrucciones de jadx. El método no está
decompilado en absoluto: lo que se lee es el desensamblado. Es el fallo honesto, porque se ve.
/* JADX WARN: <mensaje> */ (y sus hermanos JADX INFO: y JADX DEBUG:). jadx sí
produjo Java, pero detectó que su propia reconstrucción no es coherente con el grafo de
partida. Mensajes habituales tras el prefijo: «Multi-variable type inference failed», «Code
restructure failed», «Removed duplicated region». Por defecto --comments-level es info,
así que WARN e INFO se emiten y DEBUG no.
Code decompiled incorrectly, please refer to instructions dump. — acompañado de
To view partially-correct add '--show-bad-code' argument. Este es el peligroso: hay
código Java plausible en pantalla que puede no corresponder al comportamiento real. Por
defecto jadx oculta estos métodos; --show-bad-code los muestra. El aviso está para leerse,
no para ignorarse.
Class decompilation failed es otra cosa: es el mensaje de una JadxRuntimeException del
modo --single-class, no un comentario del código generado. Confundirlas lleva a buscar en la
salida algo que solo aparece en la consola.
Métodos que salen como volcado de instrucciones. Consecuencia del primero. jadx cae al modo
fallback para ese método concreto y escribe su representación interna de bajo nivel —que
no es smali: la escribe MethodGen.addFallbackInsns con InsnGen, y empieza por líneas
del tipo r0 = this;. El smali de verdad solo aparece en la pestaña correspondiente de
jadx-gui, vía JavaClass.getSmali(). Se puede forzar globalmente con -m fallback, que
renuncia a decompilar y vuelca esa representación de toda la aplicación — útil cuando la
estructura importa menos que no perder nada.
Nombres p0, i, str2, C0123a. No es un fallo: es la «sección 2.3 · Los nombres de las variables locales» más la ofuscación.
Los identificadores que empiezan por C seguidos de dígitos son la convención de jadx para
clases cuyo nombre original era inválido o colisionaba.
OutOfMemoryError y cuelgues. Habitual en aplicaciones grandes. La documentación del
proyecto recomienda bajar el número de hilos con -j, excluir paquetes que no interesan, usar
la caché en disco de jadx-gui, y subir el heap de la JVM (-Xmx8g en vez de -Xmx4g).
Este último punto tiene una consecuencia de arquitectura: jadx se embebe siempre en subproceso aislado con límite de memoria y timeout, nunca por JNI, porque un fallo de memoria del decompilador se llevaría por delante al proceso que lo hospeda.
3.6 --deobf y mapping.txt: dos cosas distintas
Se confunden constantemente y no tienen nada que ver.
--deobf es heurístico. Detecta identificadores que parecen ofuscados —por longitud, con
los umbrales --deobf-min y --deobf-max— y les asigna nombres generados y estables dentro
de una misma ejecución. No recupera nada: cambia a.b.c por algo pronunciable. Sirve para
poder hablar del código y para que las referencias cruzadas se sigan; no para saber cómo se
llamaba.
--mappings-path es determinista. Carga un fichero de correspondencias y aplica los
nombres reales. El plugin rename-mappings acepta, entre otros formatos, el mapping.txt de
ProGuard y R8, además de Tiny, Enigma, SRG, XSRG, JAM, CSRG, TSRG, migración de IntelliJ,
Recaf y el JOBF propio de jadx. --mappings-mode controla si los cambios se autoguardan.
Pero hay que invertirlo, y nadie avisa de ello. R8 escribe el mapping.txt en el sentido
original -> ofuscado; jadx aplica el mapeo de izquierda a derecha, así que con el fichero tal
cual busca en el DEX clases con el nombre original —que por definición no están— y no
renombra nada. No es un error recuperable: termina con exit=0, sin aviso, y produce
exactamente el informe falso contra el que previene esta sección. La opción que lo arregla es de
plugin y no sale en la lista principal de --help:
jadx -d salida/ -Prename-mappings.invert=yes --mappings-path mapping.txt app.apk
Comprobado con jadx 1.5.6 sobre un DEX con la clase a.b y el mapeo com.ejemplo.Real -> a.b:.
Sin el -P: sources/a/b.java. Con él: sources/com/ejemplo/Real.java, con el comentario
/* JADX INFO: renamed from: a.b */ que confirma que la correspondencia se aplicó. Y ojo con el
mismo silencio en el caso trivial: si el fichero de mapeo no existe, jadx también responde
done y 0.
La regla de lectura: si hay mapping.txt invertido, los nombres son los originales; si solo hay
--deobf, son invención; y si el mapping.txt va sin invertir, los nombres siguen siendo los
ofuscados aunque la orden parezca haber funcionado. Confundirlos es cómo se escriben informes
falsos.
4. La ruta alternativa: dex2jar más un decompilador de Java
4.1 El rodeo
classes.dex ──dex2jar──▶ classes.jar ──CFR / Vineflower / Procyon / Fernflower──▶ .java
Se traduce el bytecode de registros a bytecode de pila, y luego se usa cualquiera de los
decompiladores de Java de los últimos quince años. Dos pasos, dos oportunidades de fallar.
4.2 dex2jar
| Dato | Valor |
|---|---|
| Repositorio | https://github.com/pxb1988/dex2jar |
| Licencia | Apache-2.0 |
| Versión estable | 2.4, publicada el 3 de octubre de 2023 |
| Último push | 21 de julio de 2024 |
| Tracción | 13.132 ★ · 2.186 forks · 379 issues abiertas |
| Archivado | No |
Consultado el 12 de agosto de 2026. Estado real: mantenimiento mínimo. Dos años sin release y más de dos años sin commits, con 379 issues abiertas. No está archivado y no ha sido declarado muerto, pero tampoco se mueve.
El fork vivo sí está identificado. Consultada la lista de forks por número de estrellas el
13 de agosto de 2026 con gh api repos/pxb1988/dex2jar/forks:
| Fork | ★ | Último push |
|---|---|---|
ThexXTURBOXx/dex2jar |
308 | 22 de julio de 2026 |
DexPatcher/dex2jar |
219 | 11 de diciembre de 2019 |
El primero es el único con tracción y desarrollo reciente; el segundo lleva parado casi siete
años. Los demás forks del listado tienen cero estrellas y son clones personales. Quien
necesite dex2jar hoy debería mirar ThexXTURBOXx/dex2jar antes que el original.
⚠️ sin verificar: no se ha comparado la cobertura de ese fork con la del original, solo su
actividad. Su README enumera arreglos —«many NullPointerExceptions and other crashes», las
excepciones de firmas genéricas, TypeTransformer—, pero sus releases, de la 2.4.26 a la
2.4.38 (22 de julio de 2026), no llevan notas, y ninguna medición pública dice cuántas
aplicaciones más convierte. Que se mueva no garantiza que convierta más: hay que pasar los
dos por el mismo corpus y contar las conversiones fallidas.
El dato que importa para elegir ruta está medido: en el estudio de la «sección 6 · La comparación honesta: qué dicen las mediciones», dex2jar
falló al convertir 394 de las 13.601 aplicaciones de Google Play y 33 de las 24.553
muestras de malware, frente a 0 de las 3.018 de F-Droid. Es decir, la ruta alternativa
empieza perdiendo aproximadamente un 2,9 % de las aplicaciones comerciales antes de que el
decompilador de Java llegue a intervenir.
4.3 Los decompiladores de Java
| Herramienta | Repositorio | Licencia | Última versión | Último push | Estado |
|---|---|---|---|---|---|
| CFR | leibnitz27/cfr |
MIT | 0.152 (11-12-2021) | 04-06-2026 | Repositorio vivo, sin release desde 2021 |
| Vineflower | Vineflower/vineflower |
Apache-2.0 | 1.12.0 (29-04-2026) | 11-07-2026 | Activo |
| Procyon | mstrobel/procyon |
Apache-2.0, según su License.txt (GitHub no la reconoce) |
0.6.0 (21-02-2022) | 12-06-2022 | Parado desde 2022 |
| Fernflower | Dentro de JetBrains/intellij-community |
Apache-2.0 ⚠️ | — | — | Vive como componente de IntelliJ |
Consultado el 12 de agosto de 2026 vía la API de GitHub.
CFR es el más sólido de los clásicos y el que mejor sigue las versiones modernas del
lenguaje Java. Su anomalía de mantenimiento merece leerse con cuidado: el repositorio recibió
commits el 4 de junio de 2026, pero la última release etiquetada es la 0.152, de diciembre
de 2021, y la web oficial del autor sigue anunciando esa misma versión. Hay desarrollo; no
hay publicación. Quien lo use en producción está usando un binario de hace casi cinco años o
compilando de master.
Vineflower es el sucesor real de Fernflower: se declara en su propia descripción como «fork of the Fernflower decompiler», con énfasis en la calidad de salida. Es el único de los cuatro con release reciente (1.12.0, abril de 2026) y desarrollo continuo. Si hay que elegir un decompilador de Java hoy, es este.
Procyon está parado: última release en febrero de 2022, último commit en junio de 2022. Conserva valor histórico y sigue siendo interesante por su tratamiento de ciertas construcciones, pero no recibe correcciones.
Fernflower es el decompilador que JetBrains integra en IntelliJ IDEA y en Android Studio
— cuando se abre una clase compilada en el IDE, es él quien la muestra. No se distribuye como
proyecto independiente con releases propias; vive dentro del árbol de intellij-community.
Confirmado el 13 de agosto de 2026 vía la API de GitHub: está en
plugins/java-decompiler/, dividido en engine —el decompilador propiamente dicho— y
plugin —su integración con el IDE—, y plugins/java-decompiler/engine/LICENSE.txt es la
Apache License 2.0. El motor tiene además repositorio propio, JetBrains/fernflower, vivo y
con su uso por línea de órdenes documentado (java -jar fernflower.jar …), pero sin releases:
quien lo quiera compila el .jar o usa Vineflower, que es el mismo linaje mantenido aparte.
4.4 Cuándo esta ruta gana y cuándo pierde
Gana en un caso concreto y real: cuando jadx falla en un método determinado. El estudio de
la «sección 6 · La comparación honesta: qué dicen las mediciones» lo cuantifica — en aproximadamente el 96 % de los casos en que jadx falla al
decompilar un método, al menos otro decompilador lo consigue. La cifra sale de la tabla VIII,
medida solo sobre F-Droid y con CFR, Fernflower y Procyon —no con Vineflower—, y el propio
estudio pide tomarla como orientativa («indicative, rather than exact»). Si el objetivo es
leer ese método y jadx ha escrito JADX ERROR, probar CFR o Vineflower sobre el .jar de
dex2jar es exactamente lo que hay que hacer.
También gana cuando el objetivo no es un APK sino un .jar suelto, donde el rodeo
desaparece.
Pierde en todo lo demás:
dex2jarpuede fallar antes de empezar, y falla en un 2,9 % de las aplicaciones de Play.- Las tasas de fallo por método son entre 50 y 1.100 veces peores que las de jadx («sección 6 · La comparación honesta: qué dicen las mediciones»).
- No entiende de recursos: no decodifica el
AndroidManifest.xmlnires/. - No sabe nada de Kotlin metadata ni de las convenciones de Android.
- Son dos herramientas encadenadas en vez de una.
La conclusión operativa es que no es una alternativa a jadx: es un segundo intento para los métodos que jadx no ha podido.
5. Las comerciales y las suites
5.1 JEB (PNF Software)
Plataforma comercial y de código cerrado de PNF Software, descrita por el fabricante como una plataforma de ingeniería inversa para desensamblado, decompilación, depuración y análisis. Lo que la distingue del resto de este documento:
- Un decompilador de Dalvik propio, independiente de jadx, con reputación de aguantar mejor el código muy ofuscado.
- Decompiladores nativos en el mismo entorno: x86/x86-64, ARM/ARM64, MIPS/MIPS64, RISC-V, además de WebAssembly, EVM de Ethereum, PLC Simatic S7 y kernels SASS de Nvidia. Cubre en un solo producto lo que aquí se reparte entre este documento y «Análisis binario y estático».
- API de scripting en Java y Python para automatizar, con plugins de ejemplo publicados en GitHub aunque el núcleo sea propietario.
- Depurador que se acopla tanto al Dalvik como al nativo.
Ediciones: Community Edition y demo gratuitas; JEB Pro y JEB Android comerciales. Versión observada en la página del fabricante: JEB 5.36, de enero de 2026. Consultado el 12 de agosto de 2026.
Los precios se publican en la página de compra, por año: JEB Android, 1.200 $ por usuario (o 140 $ al mes); JEB Pro, 2.000 $ por usuario; JEB Pro Floating, 4.000 $ por puesto, y JEB Pro OEM, 8.000 $ por servidor. Consultado el 25 de septiembre de 2026.
El punto ciego: es de pago, es cerrado y no se puede auditar. Para trabajo reproducible o para una cadena de herramientas verificable, eso es un problema real.
5.2 Bytecode Viewer
| Dato | Valor |
|---|---|
| Repositorio | https://github.com/Konloch/bytecode-viewer |
| Licencia | GPL-3.0 |
| Última versión | 2.13.2 (1 de enero de 2026), etiquetada «2026 Release (In-Dev)» |
| Último push | 17 de julio de 2026 |
| Tracción | 15.590 ★ · 1.271 forks · 103 issues abiertas |
Consultado el 12 de agosto de 2026. Su propia descripción: «A Java 8+ Jar & Android APK Reverse Engineering Suite (Decompiler, Editor, Debugger & More)».
Lo que aporta es agregación: ejecuta varios decompiladores a la vez sobre la misma clase y
los muestra en paneles paralelos. Eso convierte el hallazgo del 96 % de la «sección 4.4 · Cuándo esta ruta gana y cuándo pierde» en una
operación de un clic en lugar de un montaje manual de dos herramientas. Añade además editor,
buscador y visor de bytecode.
El punto ciego: lo que aporta solo existe en la interfaz gráfica. Tiene línea de órdenes
(-i, -o, -t, -decompiler, -nowait), pero con un decompilador cada vez, así que en CI
no da nada que no dé el decompilador suelto; es el patrón que la evaluación de las trece
herramientas señala como enfermedad del sector. Y la licencia GPL-3.0 condiciona su
integración en productos con núcleo permisivo.
6. La comparación honesta: qué dicen las mediciones
Esto no se ha medido aquí: haría falta un corpus de decenas de miles de aplicaciones. Lo que
sigue son mediciones publicadas, con la fuente identificada. Las que sí se han ejecutado en esta
máquina, sobre un solo APK, están en la «sección 7 · Recetas de jadx CLI».
La referencia es A Large-Scale Empirical Study of Android App Decompilation, de Noah Mauthe (Universidad del Sarre), Ulf Kargén y Nahid Shahmehri (Universidad de Linköping), presentado en SANER 2021. Es el primer estudio sistemático a gran escala de tasas de fallo de decompilación en Android. Tres conjuntos:
| Conjunto | Aplicaciones | Media de métodos por aplicación |
|---|---|---|
| F-Droid (código abierto) | 3.018 | 13.532 |
| Google Play (crawl reciente) | 13.601 | 63.748 |
| Malware | 24.553 | 5.142 |
6.1 Tasa media de fallo por método
Tabla VI del estudio — porcentaje medio de métodos fallidos por aplicación, incluyendo los
timeouts como fallo total y excluyendo las aplicaciones en que dex2jar falló:
| Conjunto | CFR | Fernflower | jadx | Procyon |
|---|---|---|---|---|
| F-Droid | 0,741 % | 5,966 % | 0,005 % | 1,522 % |
| Google Play | 1,032 % | 58,418 % | 0,019 % | 11,006 % |
| Malware | 1,838 % | 9,726 % | 0,044 % | 1,672 % |
| Media ponderada | 1,204 % | 24,703 % | 0,023 % | 4,733 % |
Excluyendo también los timeouts (tabla V), la media ponderada queda en CFR 1,094 %, Fernflower 1,024 %, jadx 0,021 % y Procyon 0,564 %. La diferencia entre las dos tablas para Fernflower —del 24,7 % al 1,0 %— dice que su problema principal no es decompilar mal sino agotar el tiempo: el estudio registra un número muy grande de timeouts para él, frente a solo 5 para jadx.
Lectura: jadx es entre 50 y 1.100 veces mejor que la ruta alternativa, según el decompilador, en la media ponderada de la tabla VI. No es una mejora marginal.
6.2 El dato que de verdad importa
Tabla VII — porcentaje de aplicaciones en las que todos los métodos se decompilaron sin un solo fallo:
| Conjunto | Solo jadx | Ensemble de los cuatro | Los cuatro a la vez |
|---|---|---|---|
| F-Droid | 74,52 % | 74,69 % | 8,65 % |
| Google Play | 21,02 % | 21,03 % | 0,15 % |
| Malware | 78,81 % | 79,68 % | 0,60 % |
Aquí está la conclusión que hay que interiorizar: solo una de cada cinco aplicaciones de Google Play decompila entera sin un solo método fallido. Y combinar los cuatro decompiladores no arregla nada a nivel de aplicación — sube del 21,02 % al 21,03 %.
El propio estudio lo atribuye, «probably due in part», a que las aplicaciones de Play tienen 63.748 métodos de media. Pero la aritmética sola no lo explica: con una tasa de fallo por método de 0,019 % y fallos independientes, la probabilidad de que ninguno falle sería (1 − 0,00019)^63.748 ≈ 0,0006 %, no el 21 % observado. Los fallos no se reparten al azar: se concentran en unas aplicaciones y dejan limpias otras.
El corolario incómodo: si nadie te dice qué métodos fallaron, no sabes qué parte de lo que estás leyendo no es lo que la aplicación hace. De ahí que la «cobertura de decompilación explícita» sea lo que más se echa en falta: hoy los fallos son silenciosos.
6.3 Y una conclusión que consuela
El estudio también responde a si la ofuscación está rompiendo la decompilación a propósito, y la respuesta es que no: los autores no encontraron ni un caso de ofuscación de control de flujo con ramas falsas hacia posiciones inválidas, la técnica «irrompible» habitual en código nativo o JVM de escritorio. Los fallos se deben mayoritariamente a limitaciones técnicas de los decompiladores —control de flujo muy complejo, anidamiento profundo, agotamiento de recursos en métodos grandes— y no a defensas deliberadas. La conclusión de los autores es que la decompilación casi perfecta de Android es alcanzable con mejoras de implementación.
6.4 Límites de estos datos
Hay que decirlo con claridad:
- Es un estudio de 2021. Cita jadx con una referencia de 2020, sin fijar número de versión; la versión actual es la 1.5.6, de julio de 2026. Las cinco ramas de mejora desde entonces no están reflejadas. Las cifras absolutas son casi con seguridad mejores hoy; el orden de magnitud entre herramientas es lo que conserva valor.
- Mide fallo declarado, no corrección. Un método que decompila «bien» puede producir Java que no refleja el comportamiento — los propios autores lo advierten.
- No incluye Vineflower, que no existía como tal, ni JEB, que es de pago.
- Las tablas III y IV del estudio documentan que parte de los fallos contabilizados son artefactos de la medición: coincidencias superfluas por clases internas y métodos irreconciliables por genéricos. Para jadx, el 34,0 % de los fallos en Google Play entran en esta segunda categoría.
⚠️ sin verificar: no se han encontrado mediciones públicas equivalentes de 2025 o 2026 sobre las versiones actuales de estas herramientas. La más reciente localizada es la versión en revista del mismo estudio —Kargén, Mauthe y Shahmehri, Empirical Software Engineering 28:48, febrero de 2023—, que repite la metodología, añade 54.945 muestras de malware de AndroZoo y tampoco fija versiones de los decompiladores. Búsqueda del 25 de septiembre de 2026 en la API de arXiv y en buscadores generales; las cifras de arriba siguen siendo lo mejor disponible.
7. Recetas de jadx CLI
Todas las recetas de esta sección se han ejecutado con jadx 1.5.6 el 13 de agosto de 2026 sobre
com.termux_1002.apkdel muestrario, en la máquina de referencia.
Los seis modos principales, con lo que producen sobre el mismo APK de 20 MB:
| Orden | Salida | .java en sources/ |
Tiempo | Código de salida |
|---|---|---|---|---|
jadx -d o1 |
Código y recursos | 1.381 | 3,2 s | 3 |
jadx -d o2 --no-res |
Solo código | 1.381 | 2,6 s | 3 |
jadx -d o3 --no-src |
Solo recursos | 31 | 1,6 s | 0 |
jadx -d o4 --deobf … |
Código y recursos | 1.381 | 3,2 s | 3 |
jadx -d o5 --show-bad-code |
Código y recursos | 1.381 | 3,1 s | 3 |
jadx -d o6 -m fallback |
Código y recursos | 1.381 | 2,1 s | 0 |
Tres lecturas de esa tabla que no se ven en la documentación del proyecto:
--no-resahorra poco tiempo y bastantes ficheros. Los 1.381.javason los mismos; lo que desaparecen son los 954 ficheros deresources/. La ganancia de 0,6 s no justifica usarlo salvo que estorben.--no-srcy-m fallbackdevuelven0, y por el mismo motivo: si no se intenta estructurar el código, no hay métodos que fallen. El código de salida3de los otros cuatro es la señal de los seis errores de decompilación, no de un problema de la invocación.--deobfsobre una aplicación no ofuscada no cambia nada, como cabe esperar, pero tampoco protesta. Es fácil dejarlo puesto en un guion y creer que está haciendo algo.
Y -m fallback produce un Java radicalmente distinto, con los nombres completamente
cualificados y sin import, que es lo que se quiere cuando lo normal falla:
com.termux.app.terminal.TermuxTerminalSessionClient mTermuxTerminalSessionClient;
class TermuxActivityBroadcastReceiver extends android.content.BroadcastReceiver {
final /* synthetic */ com.termux.app.TermuxActivity this$0;
TermuxActivityBroadcastReceiver(com.termux.app.TermuxActivity r1) {
La misma clase en el modo normal empieza por un bloque de import y usa nombres cortos.
Decompilación completa a un directorio:
jadx -d salida/ app.apk
Solo el código, sin decodificar recursos — mucho más rápido, y suficiente si solo se va a leer lógica:
jadx -d salida/ --no-res app.apk
Solo los recursos, sin decompilar:
jadx -d salida/ --no-src app.apk
Con deofuscación heurística y umbrales de longitud:
jadx -d salida/ --deobf --deobf-min 3 --deobf-max 64 app.apk
Con mapping.txt real, que es lo que de verdad recupera nombres. El -P no es opcional: sin
él la orden termina bien y no renombra nada («sección 3.6 · --deobf y mapping.txt: dos cosas distintas»).
jadx -d salida/ -Prename-mappings.invert=yes --mappings-path mapping.txt app.apk
Mostrando los métodos marcados como incoherentes, para poder auditarlos:
jadx -d salida/ --show-bad-code app.apk
Renunciando a estructurar y volcando la representación de bajo nivel:
jadx -d salida/ -m fallback app.apk
Una sola clase, útil cuando la aplicación es enorme:
jadx --single-class com.ejemplo.MiClase -d salida/ app.apk
Control de recursos, que es el problema práctico en aplicaciones grandes:
JAVA_OPTS="-Xmx8g" jadx -j 4 -d salida/ app.apk
Gestión de plugins:
jadx plugins --list
jadx plugins --available
jadx plugins --install <locationId>
Códigos de salida. Verificados el 13 de agosto de 2026 de dos formas: ejecutando jadx
1.5.6 y leyendo JadxCLI.java en el repositorio. Son cuatro:
| Código | Cuándo | Origen en JadxCLI.java |
|---|---|---|
0 |
Todo salió bien, o solo se pidió la ayuda | return 0 al final de runSave |
1 |
Argumentos incorrectos o excepción no controlada | catch (JadxArgsValidateException) y catch (Throwable) |
2 |
Fallo al cargar la entrada: no hay clases que decompilar | if (checkForErrors(jadx)) return 2 |
3 |
Cargó bien pero la decompilación terminó con errores | if (errorsCount != 0) … return 3 |
Comprobado sobre el «muestrario»:
jadx -d salida $MUESTRAS/com.termux_1002.apk ; echo "EXIT=$?"
ERROR - finished with errors, count: 6
EXIT=3
jadx -d salida $MUESTRAS/NO-EXISTE.apk ; echo "EXIT=$?"
ERROR - Incorrect arguments: File not found …/NO-EXISTE.apk
EXIT=1
Esto corrige lo que se solía dar por bueno, incluida una revisión anterior de este
documento: jadx sí señala los fallos de decompilación en su código de salida. Termux, que
es software libre sin ofuscación ninguna, produce seis errores y devuelve 3 habiendo escrito
1.381 ficheros .java perfectamente utilizables.
Y ahí está el matiz que sigue importando: un 3 no significa que el resultado no sirva,
significa que hay métodos que fallaron. La decompilación parcial es el comportamiento normal
de jadx sobre cualquier aplicación real. Quien lo automatice no debe tratar el 3 como fallo
del pipeline; tiene que distinguir el 2 —no hay nada que decompilar, esto sí es un fallo— del
3, y para medir cuánto se perdió, contar los marcadores de
fallo en la salida generada:
find salida/sources -name '*.java' | wc -l
grep -rl 'JADX ERROR:' salida/sources | wc -l
grep -rl 'Method not decompiled:' salida/sources | wc -l
grep -rl 'Code decompiled incorrectly' salida/sources | wc -l
grep -rlE 'JADX ERROR:|Method not decompiled:|Code decompiled incorrectly' salida/sources | wc -l
1381
3
6
1
6
6 ficheros de 1.381, un 0,4 %. Ese cociente —y no el código de salida— es la medida útil de la calidad del resultado.
⚠️ Hay que grepear los tres marcadores. La cadena Failed to decompile no existe en la
salida de jadx: solo aparece en un comentario de AType.java y en el logger de la GUI
(DecompileTask.java), nunca en los .java generados. Una receta que la busque devuelve cero
por esa rama y deja fuera el marcador que más importa: en este APK, el único fichero con
Code decompiled incorrectly es androidx/recyclerview/widget/GridLayoutManager.java, y es
precisamente el peligroso —Java plausible que puede no corresponder al comportamiento real—.
8. Cómo elegir, en una tabla
| Situación | Qué usar |
|---|---|
Leer código de un APK, caso general |
jadx |
| Leer código, exploración interactiva | jadx-gui, con la vista de smali al lado |
jadx escribió JADX ERROR en el método que me importa |
dex2jar + Vineflower, o CFR; o Bytecode Viewer para probarlos a la vez |
Un .jar o .class suelto, sin APK |
Vineflower o CFR directamente |
| Código muy ofuscado y hay presupuesto | JEB |
| Necesito el comportamiento exacto, sin reconstrucción | No decompiles: «smali» |
| Automatizar en CI | jadx en subproceso, contando marcadores de fallo |
Y la regla que resume el documento: cuando el Java y el smali discrepan, el smali tiene
razón.
Fuentes
- jadx — repositorio oficial — https://github.com/skylot/jadx
Consultado el 12 de agosto de 2026 vía
https://api.github.com/repos/skylot/jadx. De aquí salen la licencia Apache-2.0, la fecha de creación, el recuento de estrellas, forks e issues, y la fecha del último push de la ficha de la «sección 3.1 · Ficha». - jadx — releases — https://api.github.com/repos/skylot/jadx/releases Consultado el 12 de agosto de 2026. De aquí salen la versión 1.5.6 y su fecha de publicación, y el historial de releases citado en la «sección 3.1 · Ficha».
- jadx — README oficial — https://github.com/skylot/jadx#readme
Consultado el 12 de agosto de 2026. De aquí salen la lista de opciones de la CLI de la
«sección 7 · Recetas de jadx CLI», la lista de plugins de entrada de la «sección 3.4 · Plugins», los subcomandos de
plugins, y la advertencia literal de que «in most cases jadx can't decompile all 100% of the code». - jadx — Troubleshooting Q&A (wiki) — https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A
Consultado el 12 de agosto de 2026. De aquí salen el significado de
--show-bad-codey las recomendaciones de memoria e hilos de la «sección 3.5 · Los modos de fallo, y qué significa cada uno». Los marcadores literales de la «sección 3.5 · Los modos de fallo, y qué significa cada uno» no salen de aquí: están verificados contra el árbol del proyecto y contrastados en la «sección 6 de Diagnóstico de fallos · jadx». - A Large-Scale Empirical Study of Android App Decompilation — Noah Mauthe, Ulf Kargén,
Nahid Shahmehri. SANER 2021. https://www.ida.liu.se/~ulfka17/papers/SANER2021.pdf
Consultado el 12 de agosto de 2026; PDF descargado y extraído con
pdftotextpara leer las tablas directamente. De aquí sale toda la «sección 6 · La comparación honesta: qué dicen las mediciones»: tamaños de los conjuntos, tablas V, VI y VII, medias de métodos por aplicación, el 96 % de complementariedad, los fallos dedex2jar, y las conclusiones sobre ofuscación. - dex2jar — repositorio oficial — https://github.com/pxb1988/dex2jar Consultado el 12 de agosto de 2026 vía la API de GitHub. De aquí salen la licencia, la versión 2.4, la fecha del último push y los recuentos de la «sección 4.2 · dex2jar».
- CFR — repositorio oficial — https://github.com/leibnitz27/cfr Consultado el 12 de agosto de 2026 vía la API de GitHub y su lista de tags. De aquí salen la licencia MIT, la release 0.152 y el contraste entre el push de junio de 2026 y la ausencia de releases desde 2021.
- CFR — página oficial del autor — https://www.benf.org/other/cfr/ Consultado el 12 de agosto de 2026. Confirma que la versión anunciada sigue siendo la 0.152 de diciembre de 2021, y de aquí sale su sintaxis de invocación.
- Vineflower — repositorio oficial — https://github.com/Vineflower/vineflower Consultado el 12 de agosto de 2026 vía la API de GitHub. De aquí salen la licencia Apache-2.0, la versión 1.12.0 del 29 de abril de 2026, y su declaración de ser un fork de Fernflower.
- Procyon — repositorio oficial — https://github.com/mstrobel/procyon Consultado el 12 de agosto de 2026 vía la API de GitHub y su lista de releases. De aquí salen la versión 0.6.0 de febrero de 2022 y la fecha del último push, que sostienen la afirmación de que está parado.
- Bytecode Viewer — repositorio oficial — https://github.com/Konloch/bytecode-viewer Consultado el 12 de agosto de 2026 vía la API de GitHub. De aquí salen la licencia GPL-3.0, la versión 2.13.2 y los recuentos de la «sección 5.2 · Bytecode Viewer».
- JEB Decompiler — página oficial de PNF Software — https://www.pnfsoftware.com/jeb/ Consultado el 12 de agosto de 2026. De aquí salen las ediciones, la lista de arquitecturas decompiladas y la versión 5.36 de la «sección 5.1 · JEB (PNF Software)».
skylot/jadx,jadx-cli/src/main/java/jadx/cli/JadxCLI.java— https://github.com/skylot/jadx/blob/master/jadx-cli/src/main/java/jadx/cli/JadxCLI.java Leído el 13 de agosto de 2026 vía la API de GitHub. De aquí sale la tabla de códigos de salida de la «sección 7 · Recetas de jadx CLI»: losreturn 0/1/2/3deexecuteyrunSave.- jadx 1.5.6 ejecutado localmente — instalado con Homebrew el 13 de agosto de 2026 y
ejecutado sobre
com.termux_1002.apkdel muestrario. De aquí salen los códigos de salida comprobados, el recuento de 1.381 ficheros.javay los 3 con marcador de fallo. - Forks de
pxb1988/dex2jar— https://github.com/ThexXTURBOXx/dex2jar Consultada la lista de forks por estrellas el 13 de agosto de 2026 congh api repos/pxb1988/dex2jar/forks. De aquí sale la tabla de la «sección 4.2 · dex2jar». JetBrains/intellij-community,plugins/java-decompiler/— https://github.com/JetBrains/intellij-community/tree/master/plugins/java-decompiler Consultado el 13 de agosto de 2026 vía la API de GitHub. De aquí salen la ubicación de Fernflower, su división enengineyplugin, y su licencia Apache-2.0 de la «sección 4.3 · Los decompiladores de Java».- JEB — página de compra de PNF Software — https://www.pnfsoftware.com/jeb/buy Consultado el 25 de septiembre de 2026. De aquí salen los precios anuales de JEB Android, JEB Pro, JEB Pro Floating y JEB Pro OEM de la «sección 5.1 · JEB (PNF Software)».
- jadx 1.5.6 —
jadx-core/src/main/java/jadx/core/Jadx.java— https://github.com/skylot/jadx/blob/v1.5.6/jadx-core/src/main/java/jadx/core/Jadx.java Consultado el 25 de septiembre de 2026. De aquí salen las pasadas de las cuatro etapas de la «sección 3.2 · La arquitectura, en cuatro etapas» y la lista de plugins de entrada de la versión 1.5.6. - Kargén, Mauthe y Shahmehri, «Android decompiler performance on benign and malicious apps: an empirical study», Empirical Software Engineering 28(2):48, 2023 — https://doi.org/10.1007/s10664-022-10281-9 (texto completo en https://liu.diva-portal.org/smash/get/diva2:1743022/FULLTEXT01) Consultado el 25 de septiembre de 2026. Es la versión en revista del estudio de SANER 2021, con el seguimiento sobre malware de la «sección 6 · La comparación honesta: qué dicen las mediciones».