τTau SolutionsHerramientas

Decompiladores a Java

Antes conviene leer «Anatomía de un APK»

panorama · Actualizado el 25 de septiembre de 2026

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 failed y // 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:

  • dex2jar puede 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.xml ni res/.
  • 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.apk del 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-res ahorra poco tiempo y bastantes ficheros. Los 1.381 .java son los mismos; lo que desaparecen son los 954 ficheros de resources/. La ganancia de 0,6 s no justifica usarlo salvo que estorben.
  • --no-src y -m fallback devuelven 0, y por el mismo motivo: si no se intenta estructurar el código, no hay métodos que fallen. El código de salida 3 de los otros cuatro es la señal de los seis errores de decompilación, no de un problema de la invocación.
  • --deobf sobre 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

  1. 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».
  2. 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».
  3. 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».
  4. 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-code y 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».
  5. 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 pdftotext para 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 de dex2jar, y las conclusiones sobre ofuscación.
  6. 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».
  7. 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.
  8. 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.
  9. 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.
  10. 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.
  11. 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».
  12. 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)».
  13. 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»: los return 0/1/2/3 de execute y runSave.
  14. jadx 1.5.6 ejecutado localmente — instalado con Homebrew el 13 de agosto de 2026 y ejecutado sobre com.termux_1002.apk del muestrario. De aquí salen los códigos de salida comprobados, el recuento de 1.381 ficheros .java y los 3 con marcador de fallo.
  15. Forks de pxb1988/dex2jar — https://github.com/ThexXTURBOXx/dex2jar Consultada la lista de forks por estrellas el 13 de agosto de 2026 con gh api repos/pxb1988/dex2jar/forks. De aquí sale la tabla de la «sección 4.2 · dex2jar».
  16. 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 en engine y plugin, y su licencia Apache-2.0 de la «sección 4.3 · Los decompiladores de Java».
  17. 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)».
  18. 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.
  19. 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».