τTau SolutionsOfuscación

R8 y ProGuard

referencia técnica · Revisado el 12 de agosto de 2026

1. Qué es y por qué existe

Casi todo el código ofuscado que se encuentra dentro de un APK no lo ofuscó nadie a propósito: lo ofuscó la cadena de compilación oficial de Android, porque el desarrollador puso minifyEnabled = true para que la aplicación ocupase menos. R8 es la herramienta de Google que hace ese trabajo, y desde el plugin de Gradle 3.4.0 viene activada por defecto.

Eso convierte a R8 en el punto de partida obligatorio de este bloque. No es un producto de seguridad ni pretende serlo: es un reductor de tamaño cuyo efecto secundario —renombrar todo lo que puede a a, b, c— resulta ser la barrera de análisis más extendida del ecosistema. Un ofuscador comercial como DexGuard (Ofuscadores comerciales) empieza donde R8 acaba.

La pieza que hace que todo esto sea reversible se llama mapping.txt. R8 la emite en cada compilación y contiene la correspondencia exacta entre el nombre original y el ofuscado. Con ella, deshacer la ofuscación es una sustitución determinista sin riesgo; sin ella, hay que inferir, y ahí el techo es bajo (Deofuscación). Casi todo el valor práctico de este documento está en la sección 6, que fija la gramática de ese fichero con suficiente detalle para implementar un lector.

Este documento se queda en el lado de la identificación: qué hace R8, qué formato produce y cómo se reconoce, desde un APK ya construido, que pasó por ahí. La sección 10 publica una medición propia sobre el corpus.

2. Historia y relevo

ProGuard lo escribió Eric Lafortune; su primer commit es del 1 de mayo de 2002 y lo describe como un proyecto personal. En diciembre de 2010 rondaba el medio millón de descargas y ya venía integrado en el SDK de Android: la decisión de Google de incluirlo en Android 2.3 Gingerbread es la que le dio escala. En 2012 nace DexGuard, su hermano comercial, y en 2014 se funda Guardsquare, la empresa belga que hoy mantiene los dos. ProGuard sigue vivo y es software libre bajo GPL-2.0 con excepciones; el tag más reciente visible en su repositorio es v7.9.1. Sus cuatro pasos, según su propio manual, son shrinking, optimization, obfuscation y preverification.

ProGuard opera sobre bytecode de la JVM: entra .class, sale .class, lo que en la cadena antigua de Android obligaba a encadenar javac → ProGuard → dx. Google lo colapsó en dos herramientas nuevas que comparten repositorio y código:

Herramienta Entrada Salida Qué hace
D8 .class DEX Traduce y desugariza. Sustituye a dx. No renombra nada
R8 .class DEX Todo lo de D8 más shrinking, optimización y ofuscación. Sustituye a ProGuard + dx
L8 .class DEX/.class Compila la biblioteca del núcleo reescrita del core library desugaring

R8 se introdujo en el plugin de Gradle 3.3.0 y quedó activado por defecto en la 3.4.0, para módulos de aplicación y de biblioteca. La documentación de esa versión lo resume sin rodeos: antes, «ProGuard era un paso de compilación distinto del dexing y del desugaring»; ahora es uno solo.

Qué queda de ProGuard hoy, en la práctica:

  • El formato de configuración. Las keep rules de R8 son sintaxis de ProGuard, y los ficheros se siguen llamando proguard-rules.pro.
  • El formato de mapping.txt. R8 produce un superconjunto compatible (sección 6).
  • La marca PG en el atributo SourceFile (sección 9.2), que R8 sigue reconociendo como valor centinela heredado.
  • Proyectos antiguos y builds no-Android que siguen invocando ProGuard directamente, y DexGuard, que es «compatible hacia atrás con ProGuard» y reutiliza su configuración.

En el corpus, 69 de los 86 contenedores llevan un marcador ~~R8 dentro del DEX (sección 9.1). ProGuard puro no aparece en ninguno.

3. Los tres trabajos, que no son el mismo

minifyEnabled = true enciende tres transformaciones distintas a la vez. Se confunden constantemente, y separarlas importa porque solo una de las tres es la que mapping.txt deshace.

                 ┌──────────────────────────────────────────────┐
   .class  ────► │ 1. SHRINKING       elimina lo inalcanzable    │
   librerías     │    (tree shaking)  desde los entry points     │
   keep rules    ├──────────────────────────────────────────────┤
                 │ 2. OPTIMIZACIÓN    inlining, fusión de clases │
                 │                    propagación de constantes  │
                 ├──────────────────────────────────────────────┤
                 │ 3. OFUSCACIÓN      renombra lo que queda      │
                 │    (minification)  a a, b, c…                 │
                 └──────────────────────────────────────────────┘
                        │                          │
                        ▼                          ▼
                  classes.dex                 mapping.txt

1. Shrinking. «R8 identifica y elimina el código inalcanzable de la aplicación y de sus dependencias. Analizando los entry points de la aplicación (como las Activities o los Services declarados en el manifiesto), R8 construye un grafo del código referenciado y elimina todo lo que quede sin referenciar». Lo eliminado se lista en usage.txt.

2. Optimización. Las dos técnicas que la documentación nombra son method inlining —«R8 sustituye el punto de llamada por el cuerpo del método llamado»— y class merging —«R8 combina conjuntos de clases e interfaces en una sola clase»—. A eso se añaden propagación de constantes, eliminación de argumentos sin uso y outlining (sección 6.6). Desde AGP 8.12.0, R8 optimiza además los recursos.

3. Ofuscación. «Para reducir el tamaño del fichero DEX, R8 acorta los nombres de clases, campos y métodos (por ejemplo, com.example.MyActivity podría convertirse en a.b.a)». Es un efecto de tamaño, no de seguridad: los nombres viven en string_ids (Formato DEX, sección 6) y acortarlos ahorra bytes.

Por qué la optimización estorba al decompilador tanto o más que el renombrado. Un nombre renombrado sigue siendo un nombre: el decompilador produce código correcto y feo, y el lector puede renombrar mentalmente. La optimización destruye la correspondencia entre el fuente y el binario: un método inlineado ya no existe como método, y su cuerpo aparece repetido dentro de cada llamador; dos clases fusionadas aparecen como una, con métodos que solo tienen sentido para la mitad del objeto; una constante propagada borra el parámetro que la transportaba; y el outlining hace lo contrario, inventa un método que no estaba en el fuente y lo llama desde sitios que no tenían relación.

mapping.txt recupera los nombres y, gracias a los rangos de línea, también la pila de llamadas original de un método inlineado (sección 6.5). Lo que no recupera es la forma del código: el DEX sigue teniendo el cuerpo repetido. Esa asimetría es el motivo de que retrace funcione perfectamente sobre una traza de pila y de que deofuscar un proyecto decompilado sea otro problema.

Cada pieza se puede pedir por separado: -dontobfuscate deja shrinking y optimización, -dontoptimize deja shrinking y renombrado, -dontshrink deja los otros dos. Varias aplicaciones del corpus están en ese estado intermedio: Signal y Candy Crush llevan marcador de R8 y SourceFile de clases sintéticas de R8, pero 2.580 y 2.486 nombres de fichero fuente reales y medianas de longitud de identificador de 13 y 15. Es R8 sin ofuscación.

4. Cómo se enciende y dónde viven las reglas

La forma clásica, todavía la mayoritaria, y la DSL nueva de AGP 9.3, que enciende las dos cosas de golpe y ya incluye las reglas por defecto de la plataforma:

buildTypes { release {                       buildTypes { release {
    isMinifyEnabled = true                       optimization { enable = true }
    isShrinkResources = true                 } }
    proguardFiles(
        getDefaultProguardFile("proguard-android-optimize.txt"),
        "proguard-rules.pro")
} }

Con la DSL nueva, las reglas van en ficheros con sufijo .keep dentro del source set src/<variant>/keepRules, por ejemplo src/main/keepRules/custom-rules.keep.

Dónde caen los ficheros de salida. mapping.txt se escribe en app/build/outputs/mapping/<variante>/mapping.txt y, además, AGP lo empaqueta automáticamente dentro del AAB. La documentación advierte de lo importante: «el fichero de mapping se sobrescribe cada vez que se compila el proyecto, así que hay que guardar una copia cada vez que se publica una release». Junto a él aparecen seeds.txt —lo que las reglas impidieron eliminar—, usage.txt —lo eliminado— y configuration.txt —«todas las reglas fusionadas de cada biblioteca y módulo que se usaron para configurar R8»—.

⚠️ sin verificar: la página oficial de diagnóstico publica la ruta de configuration.txt sin segmento de variante (./app/build/outputs/mapping/configuration.txt) mientras que la del APK Analyzer escribe el directorio en plural (build/outputs/mappings/release/). Las dos páginas oficiales se contradicen y no se ha podido comprobar cuál describe el comportamiento real de AGP.

5. Keep rules

Una keep rule le dice a R8 qué no puede tocar. Las opciones se dividen en tres ejes que se combinan: qué se conserva (la clase, sus miembros, o ambos), si además se conserva el nombre, y si se permite optimizar por dentro.

Opción Conserva la clase Conserva los miembros Se aplica cuando
-keep Los que se especifiquen Siempre
-keepclassmembers No por sí sola Si la clase se conserva por otra vía
-keepclasseswithmembers Si la clase tiene esos miembros
-keepnames Solo el nombre La clase puede eliminarse; si sobrevive, no se renombra
-keepclassmembernames Solo el nombre Ídem para miembros
-keepclasseswithmembernames Solo el nombre Solo el nombre La usa la regla estándar de métodos nativos

Modificadores entre corchetes tras la opción: allowshrinking, allowoptimization, allowobfuscation, allowaccessmodification, allowrepackage, includedescriptorclasses. Comodines: * (un segmento), ** (varios, incluidos puntos), ? (un carácter), *** (cualquier tipo), ... (cualquier lista de argumentos), % (cualquier primitivo). Existen reglas condicionales -if … -keep … con retrorreferencias <1>, <2>.

Un detalle que sorprende: si una regla -keep no especifica miembros, R8 la convierte implícitamente en una regla que conserva el constructor por defecto.

5.1 Los ficheros y su procedencia

Fichero Quién lo pone Qué contiene
proguard-android-optimize.txt AGP, vía getDefaultProguardFile() Reglas base de la plataforma
proguard-rules.pro El desarrollador Las reglas propias del módulo
consumer-rules.pro La biblioteca, vía consumerProguardFiles Reglas que la biblioteca impone a quien la usa
META-INF/proguard/* Un JAR de biblioteca Equivalente al anterior para artefactos JAR
(generado) AAPT2 Reglas derivadas de los componentes del manifiesto

La distinción entre proguardFiles y consumerProguardFiles está bien enunciada en la documentación de Google: los primeros «se usan en tiempo de compilación para definir qué parte de tu biblioteca debe conservarse durante la compilación de la biblioteca — como mínimo, su API pública»; los segundos «se empaquetan dentro de la biblioteca para afectar a qué optimizaciones ocurren después, durante la compilación de la aplicación que la consume». Consecuencia práctica para quien analiza: buena parte de las reglas que gobiernan una aplicación no las escribió su autor, vinieron dentro de sus dependencias.

⚠️ sin verificar: el mecanismo concreto por el que las reglas de los componentes del manifiesto llegan a R8 —habitualmente citado como un aapt_rules.txt generado por AAPT2— no está descrito en ninguna página oficial de las consultadas. Lo confirmado es la afirmación funcional: los componentes del manifiesto son entry points del grafo de R8.

5.2 Las reglas que AGP empaqueta de verdad

Desde AGP 2.2 los ficheros de $ANDROID_HOME «ya no se mantienen y serán ignorados»; lo que getDefaultProguardFile() desempaqueta son tres ficheros que viven en el propio plugin: proguard-header.txt (solo comentarios), proguard-optimizations.txt (una única línea, -allowaccessmodification) y proguard-common.txt, que es el interesante:

-keepattributes AnnotationDefault, EnclosingMethod, InnerClasses,
                RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations,
                RuntimeVisibleTypeAnnotations, Signature
-keepclasseswithmembernames,includedescriptorclasses class * { native <methods>; }
-keepclassmembers public class * extends android.view.View { void set*(***); *** get*(); }
-keepclassmembers class * extends android.app.Activity { public void *(android.view.View); }
-keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); }
-keepclassmembers class * implements android.os.Parcelable { public static final ** CREATOR; }
-keepclassmembers class * { @android.webkit.JavascriptInterface <methods>; }
-keep class android.support.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}

Cada línea es una pista de análisis: que los set*/get* de las subclases de View conserven su nombre, que los values()/valueOf() de los enum sobrevivan, que el campo CREATOR de todo Parcelable se siga llamando CREATOR y que los métodos anotados con @JavascriptInterface mantengan el suyo son anclas fijas en un DEX por lo demás ilegible, y la base de las heurísticas de Deofuscación.

Nota verificable: el fichero de AGP solo entiende la anotación legacy android.support.annotation.Keep, no androidx.annotation.Keep. ⚠️ sin verificar: lo previsible es que la versión AndroidX aporte sus propias reglas como consumer rules dentro de la biblioteca androidx.annotation, pero no se ha comprobado.

5.3 Qué se conserva siempre, y qué rompe

Siempre: los componentes declarados en el manifiesto, que el framework instancia por nombre; los métodos native, por la regla de arriba, «para que se puedan seguir enlazando con la biblioteca nativa»; y lo que digan las reglas de las dependencias.

Lo que no se conserva y rompe en ejecución es todo lo alcanzado por reflexión: «R8 no puede identificar cuándo se accede a clases, campos o métodos mediante reflexión… Podría renombrarlos o eliminarlos por completo, lo que produce una ClassNotFoundException o una NoSuchMethodException en tiempo de ejecución». Y lo mismo con JNI: «cuando código nativo llama a un método Java o Kotlin… la llamada se basa en una búsqueda dinámica por cadena del nombre del método».

6. mapping.txt: la gramática exacta

Todo lo que sigue está contrastado contra tres fuentes: la gramática publicada en el manual de ProGuard, el código de R8 (ProguardMapReader.java, ClassNamingForNameMapper.java, Range.java, ProguardMapMarkerInfo.java) y doc/retrace.md del repositorio de R8. Los ejemplos son salida real de R8 9.2.4-dev, el que viene en build-tools/37.0.0/lib/d8.jar de esta máquina; el experimento está en la sección 11.2.

Es un fichero de texto plano en UTF-8, orientado a líneas, con indentación significativa de cuatro espacios.

6.1 El preámbulo de metadatos

Salida real:

# compiler: R8
# compiler_version: 9.2.4-dev
# min_api: 21
# compiler_hash: 084a89126ae0596c599143df57a654646af28313
# common_typos_disable
# {"id":"com.android.tools.r8.mapping","version":"2.2"}
# pg_map_id: 5d9ebd539f55ab9c5999893268740017c8d4b9b6acfe65239fb4fbcdfc906ac5
# pg_map_hash: SHA-256 5d9ebd539f55ab9c5999893268740017c8d4b9b6acfe65239fb4fbcdfc906ac5

El orden y la condicionalidad los fija ProguardMapMarkerInfo.toPreamble():

Línea ¿Siempre? Significado
# compiler: <Tool> D8, L8, R8, R8Partial, Relocator, TraceReferences
# compiler_version: <v> Versión de R8
# min_api: <n> Solo si genera DEX minSdkVersion numérico
# compiler_hash: <sha> Solo en builds de desarrollo SHA del build de R8
# common_typos_disable Desactiva el linting del fichero en algunos sistemas de build
# {"id":"com.android.tools.r8.mapping","version":"…"} Si la versión > none Versión del formato
# pg_map_id: <id> Identificador del mapping
# pg_map_hash: SHA-256 <hash> Hash del contenido

Dos trampas para un parser. min_api va antes de common_typos_disable, no al final. Y compiler_hash no aparece en releases estables: quien lo dé por obligatorio falla contra mapping.txt de producción. El prefijo SHA-256 de pg_map_hash es literal y R8 lo valida como tal (line.startsWith("# pg_map_hash: SHA-256 ")).

La versión estable del formato hoy es la 2.2 (MapVersion.STABLE). Existe una 2.3 en el enumerado, pero no es la estable. La regla de compatibilidad está en doc/retrace.md y es la que hace que el formato pueda crecer: «la información de versión se aplica al contenido del fichero que sigue a la posición de la entrada de versión hasta el final del fichero o hasta otra entrada de versión», y «al interpretar el fichero, cualquier información adicional correspondiente a una versión posterior debe ignorarse; dicho de otro modo, tratarse como un comentario normal». Un lector correcto avisa cuando ve una versión superior a la que soporta, y sigue.

6.2 Mapeo de clase

Empieza en columna 0 y termina en dos puntos (originalclassname -> obfuscatedclassname:). Ese : final es su discriminador. Salida real:

com.ejemplo.Calculadora -> com.ejemplo.b:
com.ejemplo.Calculadora$Auxiliar -> com.ejemplo.a:
com.ejemplo.Main -> com.ejemplo.Main:
com.ejemplo.Punto -> a:
com.ejemplo.Lambdas -> R8$$REMOVED$$CLASS$$0:

Cinco cosas de la vida real en cinco líneas: los nombres van con puntos, no con barras ni en formato descriptor; las clases internas se nombran con $ como en el DEX; una clase puede mapearse a sí misma (Main -> Main, porque una keep rule la conservó); R8 puede sacar una clase de su paquete y ponerla en el paquete raíz (Punto -> a), que es -repackageclasses y lo que explica los La;, Laai; de las aplicaciones de Google; y una clase eliminada por completo recibe el nombre centinela R8$$REMOVED$$CLASS$$<n>.

6.3 Mapeo de campo

Cuatro espacios de indentación, sin rango de líneas: originalfieldtype originalfieldname -> obfuscatedfieldname. Salida real:

com.ejemplo.Calculadora -> com.ejemplo.b:
    int acumulador -> a
    java.lang.String etiqueta -> b

Dos campos de tipos distintos pueden mapearse al mismo nombre ofuscado, porque en el DEX un field_id lleva class_idx, type_idx y name_idx: a de tipo int y a de tipo String son campos distintos. El mapeo no es biyectivo por el nombre solo.

6.4 Mapeo de método y los rangos de línea

La forma completa, tal como la publica el manual de ProGuard:

    [startline:endline:]originalreturntype [originalclassname.]originalmethodname(args)[:origstart[:origend]] -> obfuscatedmethodname

Y así es como R8 la serializa, en ClassNamingForNameMapper.MappedRange.toString():

if (minifiedRange != null) builder.append(minifiedRange).append(':');
builder.append(signature);
if (originalRange != null && !originalRange.equals(minifiedRange))
    builder.append(":").append(originalRange);
builder.append(" -> ").append(renamedName);

Descompuesto sobre el ejemplo del enunciado clásico, 23:25:void foo():42:44 -> a:

Trozo Nombre en el código de R8 Qué es
23:25 minifiedRange Rango de líneas en el binario producido. Es lo que aparecerá en la traza de pila del APK
void foo() signature Firma original: tipo de retorno, nombre y argumentos originales
42:44 originalRange Rango de líneas en el fichero fuente
a renamedName Nombre ofuscado del método

Tres reglas que un parser tiene que respetar y que casi ninguna descripción informal recoge:

  1. R8 omite originalRange cuando es igual a minifiedRange. La condición literal es !originalRange.equals(minifiedRange). Así que 1:3:void <init>(java.lang.String):7:7 y 1:7:void main(java.lang.String[]):5:11 conviven con líneas sin la segunda mitad.
  2. Un Range «cardinal» se serializa como un solo número, no como from:to. De ahí la tercera forma que documenta el javadoc de ProguardMapReader: range COLON signature COLON number ARROW name.
  3. Puede no haber rango en absoluto. Salida real: 8:8:java.lang.String com.ejemplo.Repetido.todo(java.lang.String) -> main.

Salida real completa de una clase con métodos renombrados:

com.ejemplo.Calculadora -> com.ejemplo.b:
    int acumulador -> a
    java.lang.String etiqueta -> b
    1:3:void <init>(java.lang.String):7:7 -> <init>
    4:6:void <init>(java.lang.String):8:8 -> <init>
    7:9:void <init>(java.lang.String):9:9 -> <init>
    1:3:int cuentaDeOperaciones():30:30 -> a
    1:1:void registrar(java.lang.String):23:23 -> b
    2:2:void registrar(java.lang.String):25:25 -> b
    1:7:int resta(int,int):18:18 -> c
    1:7:int suma(int,int):13:13 -> d

Obsérvese que cuatro métodos originales se llaman ahora a, b, c y d y que los rangos de minifiedRange se solapan entre métodos distintos: los dos empiezan en 1. No es un error. El rango solo desambigua dentro del mismo nombre ofuscado: 1:1 -> b y 2:2 -> b son dos líneas del mismo método b, mientras que 1:7 -> c y 1:7 -> d son métodos distintos. La clave de búsqueda es el par (nombre ofuscado, línea).

El rango catch-all. Cuando basta una sola posición, R8 emite el rango completo:

0:65535:void foo():33:33 -> a

doc/retrace.md lo explica: «para asegurar la compatibilidad, R8 emite un rango catch-all 0:65535… Los rangos catch-all nunca deben usarse para sobrecargas».

6.5 Métodos inlineados

Aquí es donde el formato deja de ser una tabla y se convierte en algo con estructura. El javadoc de ProguardMapReader lo enuncia: la forma range COLON signature COLON range ARROW name significa «que signature fue inlineado desde el segundo rango a los números de línea nuevos del primero», y a continuación vienen las entradas «con la información de la traza de llamada hasta donde el miembro fue inlineado».

Salida real de una compilación con optimización, sobre el mismo código:

com.ejemplo.Main -> com.ejemplo.Main:
    1:1:void com.ejemplo.Calculadora.registrar(java.lang.String):24:24 -> main
    1:1:int com.ejemplo.Calculadora.suma(int,int):13 -> main
    1:1:void main(java.lang.String[]):6 -> main
    2:2:double com.ejemplo.Punto.distancia(com.ejemplo.Punto):8:8 -> main
    2:2:void main(java.lang.String[]):9 -> main

La lectura correcta del bloque 1:1, de arriba abajo, es de marco más interno a marco más externo:

línea 1 del método `main` del APK  ──►  Calculadora.registrar(String)  línea 24   ← el fallo
                                        llamado desde
                                        Calculadora.suma(int,int)      línea 13
                                        llamado desde
                                        Main.main(String[])            línea 6

Una sola línea de la traza de pila real se expande en tres marcos. Eso es lo que hace retrace, y es la razón por la que una traza deofuscada puede ser más larga que la original. La señal sintáctica de que una entrada es un marco inlineado venido de otra clase es que la firma lleva el nombre de clase cualificado delante (void com.ejemplo.Calculadora.registrar(...) en vez de void registrar(...)).

6.6 Los comentarios de metadatos por elemento

Además del preámbulo, R8 emite comentarios JSON pegados al elemento al que se refieren. El conjunto exacto de identificadores, extraído de las constantes ID del paquete com.android.tools.r8.naming.mappinginformation:

id Desde Claves Qué dice
com.android.tools.r8.mapping 1.0 version Versión del formato (solo en el preámbulo)
sourceFile 0 fileName Nombre del fichero fuente de la clase
com.android.tools.r8.synthesized 1.0 Este elemento lo generó el compilador, no estaba en el fuente
com.android.tools.r8.residualsignature 2.2 signature Firma residual (la del binario) cuando difiere de la original
com.android.tools.r8.rewriteFrame 2.0 conditions, actions Reescritura condicional de marcos
com.android.tools.r8.outline 2.0 Este método es un outline
com.android.tools.r8.outlineCallsite 2.0 positions, outline Correspondencia posición-en-el-outline → posición-en-el-llamador
com.android.tools.r8.mergedClasses Clases fusionadas
partitionSourceFiles fileNameMappings Ficheros fuente en mappings particionados

Dos correcciones a lo que circula por ahí, verificadas en el código: no existe com.android.tools.r8.compilerSynthesized —la clase Java se llama CompilerSynthesizedMappingInformation pero su ID es com.android.tools.r8.synthesized— y no existe com.android.tools.r8.sourceFile: el identificador es el literal sourceFile, sin cualificar, porque, según doc/retrace.md, «es el único identificador sin cualificar permitido, ya que se introdujo antes del esquema de versionado». Los dos últimos de la tabla existen en el código pero no están documentados en doc/retrace.md.

Salida real con clases sintéticas de lambda y firma residual:

com.ejemplo.Lambdas$1 -> b:
# {"id":"sourceFile","fileName":"R8$$SyntheticClass"}
# {"id":"com.android.tools.r8.synthesized"}
    int com.ejemplo.Lambdas$$InternalSyntheticLambda$1$5e2a…$2.f$0 -> a
      # {"id":"com.android.tools.r8.synthesized"}
    1:1:boolean com.ejemplo.Lambdas.lambda$run$2(int,int,java.lang.String):19:19 -> test
      # {"id":"com.android.tools.r8.residualsignature","signature":"(Ljava/lang/Object;)Z"}

Tres cosas que enseña ese fragmento. El fileName de una clase sintética es la cadena literal R8$$SyntheticClass, que aparece tal cual dentro del DEX y es una huella excelente (sección 9.2). El nombre original de una lambda desugarizada es …$$InternalSyntheticLambda$<n>$<sha256>$<m>, con un hash de 64 hexadecimales dentro. Y residualsignature existe porque la firma del binario no se puede deducir de la original: aquí el parámetro String se convirtió en Object al implementar Predicate. Según doc/retrace.md, «no tiene efecto sobre el retrace de trazas de pila, pero es necesaria al interactuar con firmas residuales a través de la API de Retrace o al componer ficheros de mapping».

Reglas de colocación, verbatim: la información de fichero fuente «debe colocarse directamente debajo del mapeo de clase al que se refiere»; la de synthesized, «directamente debajo del mapeo de clase, campo o método al que se refiere», y «nunca debe colocarse sobre marcos inlineados».

rewriteFrame es el más raro y merece nota: su condición throws(<descriptor>) «será cierta si la excepción lanzada arriba es <descriptor>» —varias condiciones se combinan con AND, y para hacer un OR hay que duplicar la información— y su acción removeInnerFrames(<n>) «elimina ese número de marcos empezando por el más interno», y solo se aplica «si la línea que se está deofuscando está directamente debajo de la línea de la excepción». Sirve para que los NullPointerException sintéticos que R8 introduce al inlinear no salgan en la traza.

⚠️ Los ejemplos de doc/retrace.md usan comillas simples en el JSON; el fichero real que emite R8 usa comillas dobles, como se ve arriba. Un parser tolerante acepta ambas.

6.7 El algoritmo de lectura

estado: clase_actual = ninguna
para cada línea:
  1. empieza por '#'                 → metadato
       · preámbulo                   → compilador, versión, min_api, map id
       · JSON con "id"               → asociarlo al último elemento emitido
       · versión desconocida         → avisar y tratar como comentario
  2. no empieza por espacio, acaba en ':'
                                     → clase: partir por " -> ", clase_actual = esta
  3. empieza por espacios            → miembro de clase_actual, partir por " -> "
       · empieza por dígito y ':'    → método con rango
       · contiene '('                → método sin rango
       · en otro caso                → campo

La operación inversa —la de retrace— está desarrollada en Deofuscación, sección 3.1, con sus tres casos límite: entradas sin minifiedRange, rangos solapados, y la no inyectividad del mapping, que hace la deofuscación ambigua cuando la traza no trae número de línea.

7. retrace

retrace es la herramienta que aplica un mapping.txt a una traza de pila. La definición oficial: «R8 retrace es una herramienta para obtener la traza de pila original a partir de una traza ofuscada. La traza se reconstruye emparejando los nombres de clase y método de un fichero de mapping con sus definiciones originales».

retrace  <fichero-de-mapping> [<fichero-de-traza>] [opciones]

Se instala con el SDK Manager en cmdline-tools/<version>/bin/. Sin fichero de traza lee de la entrada estándar. Opciones: --verbose («imprime más información, como los parámetros y el tipo de retorno del método»), --info, --quiet, --regex <expresión> y --verify-mapping-file-hash. El retrace.jar de ProGuard acepta la misma idea con una sintaxis de comodines propia (%c clase, %m método, %l línea, %s fichero fuente…) y exige que las expresiones usen solo grupos sin captura, (?:...).

Ejecutado de verdad sobre el mapping.txt generado en la sección 11.2, con java -cp $BT/lib/d8.jar com.android.tools.r8.retrace.Retrace mapping2.txt traza.txt:

java.lang.IllegalStateException: demo:suma        java.lang.IllegalStateException: demo:suma
	at com.ejemplo.b.b(SourceFile:2)          →   	at com.ejemplo.Calculadora.registrar(Calculadora.java:25)
	at com.ejemplo.b.d(SourceFile:1)              	at com.ejemplo.Calculadora.suma(Calculadora.java:13)
	at com.ejemplo.Main.main(SourceFile:6)        	at com.ejemplo.Main.main(Main.java:10)

Y sobre el mapping.txt con optimización, una sola línea de entrada produce tres:

java.lang.NullPointerException                     java.lang.NullPointerException
	at com.ejemplo.Main2.main(SourceFile:4)    →   	at com.ejemplo.Lambdas.filtra(Lambdas.java:12)
                                                   	at com.ejemplo.Lambdas.run(Lambdas.java:19)
                                                   	at com.ejemplo.Main2.main(Main2.java:5)

Por qué es deofuscación determinista y sin riesgo. Porque no infiere nada. Es una búsqueda en una tabla que produjo el mismo compilador que produjo el binario: no hay heurística, no hay confianza que declarar, no hay falsos positivos posibles. O el mapping.txt corresponde al APK —y el hash pg_map_id permite comprobarlo, sección 9.1— o no corresponde y la respuesta es «no lo sé».

8. R8 full mode

R8 tiene dos modos. El modo compatibilidad imita a ProGuard; el modo completo (full mode) hace suposiciones más fuertes sobre el código y optimiza más. android.enableR8.fullMode pasó a valer true por defecto en AGP 8.0.

Las siete diferencias, tomadas literalmente de compatibility-faq.md del repositorio de R8:

  1. El constructor por defecto <init>() no se conserva implícitamente al conservar una clase.
  2. Tampoco se conserva para tipos que solo se usan con ldc, instanceof o checkcast.
  3. Las clases que contienen campos o métodos casados por una -keepclassmembers no se consideran implícitamente instanciadas.
  4. Los métodos default no se conservan implícitamente como métodos abstractos.
  5. Los atributos (como Signature) y las anotaciones solo se conservan para las clases, métodos y campos casados por una keep rule, aunque se haya puesto -keepattributes.
  6. Al optimizar o minificar, el atributo SourceFile siempre se reescribe a SourceFile salvo que se use -renamesourcefileattribute.
  7. La modificación de acceso (-allowaccessmodification) está activada por defecto al optimizar.

Los puntos 1 a 4 son la razón de que el modo completo «rompa más código»: cada uno elimina una conservación implícita en la que muchos proyectos se apoyaban sin saberlo, y el síntoma es una ClassNotFoundException o una NoSuchMethodException en ejecución.

El punto 5 es el que más duele al analizar, no al compilar: sin Signature desaparecen los genéricos, que en el DEX viven en la anotación de sistema dalvik.annotation.Signature (Formato DEX, sección 9). Un List<String> decompila como List y ya no hay forma de recuperar el parámetro de tipo.

⚠️ Discrepancia observada entre fuentes oficiales: el mismo compatibility-faq.md afirma que «el modo compatibilidad de R8 es el predeterminado en Android Studio», lo que contradice las notas de AGP 8.0. El texto de la FAQ parece desactualizado. Los datos del corpus apoyan las notas de AGP: de los 90 marcadores con campo r8-mode encontrados, 64 dicen full y 26 dicen compatibility, y los compatibility se concentran en bibliotecas compiladas con versiones antiguas de R8 que el AAR traía ya dexificadas.

9. Cómo se detecta que una app pasó por R8

En orden de fiabilidad, de la certeza a la estadística.

9.1 El marcador ~~R8{…} dentro del DEX

R8, D8 y L8 escriben su propia firma como una cadena del DEX. Es un objeto JSON precedido de dos virgulillas, y es la huella más fuerte que existe porque no es una inferencia: es una declaración. Salida real de la compilación de la sección 11.2:

~~R8{"backend":"dex","compilation-mode":"release","has-checksums":false,"min-api":21,
     "pg-map-id":"5d9ebd539f55ab9c5999893268740017c8d4b9b6acfe65239fb4fbcdfc906ac5",
     "r8-mode":"full","sha-1":"084a89126ae0596c599143df57a654646af28313","version":"9.2.4-dev"}

(partido en tres líneas aquí; en el fichero es una sola cadena).

Qué se saca de ahí, de un APK cualquiera y sin herramientas: qué herramienta lo procesó, qué versión exacta, si se compiló en release o en debug, contra qué minSdkVersion, si estaba en modo full o compatibility, y el pg-map-id, que es literalmente el mismo valor que la línea # pg_map_id: del mapping.txt. Es decir: se puede verificar que un mapping.txt dado corresponde a un APK dado sin ejecutar nada.

Buscarlo es trivial:

CORPUS=~/corpus-apk
unzip -p $CORPUS/com.looker.droidify_710.apk classes.dex | strings | grep '~~'

Medido sobre el corpus completo, extrayendo el marcador de los 323 classes*.dex de los 86 contenedores:

Observación Valor
Contenedores con al menos un marcador ~~R8 69 de 86
Contenedores sin ningún marcador ~~D8/~~R8/~~L8 5
Versiones distintas de R8/D8/L8 observadas 45
Marcadores con r8-mode 90: 64 full, 26 compatibility

Los cinco sin marcador son com.whatsapp.apks, com.supercell.clashroyale.apks, com.moonactive.coinmaster.apks, org.gnucash.android_2.4.0.apk y io.github.muntashirakon.AppManager_445.apk. La ausencia del marcador es en sí una señal: en WhatsApp, que sí está claramente ofuscado (sección 10), significa que algo posterior a R8 reescribió el DEX y se llevó la cadena por delante.

Un matiz que evita conclusiones erróneas: una aplicación trae muchos marcadores, no uno. Twitch trae 12 con 8 pg-map-id distintos y versiones desde la 2.2.66 hasta la 8.12.x; los AAR de terceros vienen ya dexificados y con su propio marcador. El marcador del DEX que contiene el código de la aplicación es el que importa; los demás describen sus dependencias —y, de paso, delatan qué bibliotecas usa.

9.2 El atributo SourceFile

Cada class_def_item tiene un source_file_idx (Formato DEX, sección 10). En un binario compilado sin ofuscar hay un nombre de fichero distinto por clase; tras pasar por R8 hay uno solo, y su valor identifica el camino recorrido:

Valor de SourceFile Qué significa Origen
Muchos valores distintos (Foo.java, Bar.kt…) Sin renombrar
SourceFile Valor por defecto de R8 DexItemFactory.defaultSourceFileAttributeString
PG Valor centinela heredado de ProGuard DexItemFactory.pgSourceFileAttributeString
R8$$SyntheticClass / D8$$SyntheticClass Clase sintética generada por el compilador mapping.txt, {"id":"sourceFile"}
r8-map-id-<64 hex> El pg_map_id incrustado en el SourceFile SourceFileRewriter.computeSourceFileProvider
Cadena vacía Reescrito a vacío por algo posterior

La lógica exacta está en SourceFileRewriter.java: -renamesourcefileattribute solo se aplica si SourceFile está en -keepattributes; en modo compatibilidad, además, solo cuando se están minificando nombres; y si no está, R8 reescribe al valor por defecto "SourceFile".

El caso r8-map-id-… merece una nota, porque el código y la realidad no coinciden. En el fuente de R8 esa rama está detrás de options.getTestingOptions().enableMapIdInSourceFile, una opción de testing. Y sin embargo aparece en 19 de los 85 contenedores medidos, incluidos Twitch, Reddit, K-9 Mail, NewPipe, Wikipedia, AnkiDroid y siete aplicaciones de Fossify. La conclusión razonable es que AGP la activa en sus builds; ⚠️ sin verificar: no se ha encontrado documentación oficial que lo confirme, y el dato procede de medición propia, no de una fuente de Google. Es además la huella más útil de todas, porque el valor contiene el identificador del mapping.txt correspondiente.

Distribución del valor mayoritario de SourceFile sobre los 85 contenedores con DEX:

Valor mayoritario Contenedores
SourceFile 31
r8-map-id-<sha256> 19
D8$$SyntheticClass 11
R8$$SyntheticClass 9
PG 9
Cadena vacía 2
Otros (R.java, lambda, nombre de artefacto Maven) 4

Las nueve aplicaciones con PG son las nueve de Google del corpus: Gmail, Maps, YouTube, Photos, Drive, Keep —en sus dos empaquetados— más Authenticator y Calculator. Es una huella de la cadena de compilación interna de Google, no de AGP.

Y 59 de los 85 contenedores tienen un único valor de SourceFile para todas sus clases, que es el indicador binario más barato de «esto pasó por un reescritor de atributos».

9.3 Otras huellas

  • com.google.android.gms sin renombrar junto a clases La;, Lb;: es el patrón normal de una aplicación ofuscada con dependencias de Google, cuyas consumer rules conservan buena parte de su superficie. Netflix, con DexGuard, tiene 475 clases en com/google/android/gms/internal/cast sin ofuscar al lado de 17.329 clases en un paquete llamado o.
  • Clases sintéticas con nombres reconocibles: $$ExternalSyntheticLambda<n>, $$InternalSyntheticLambda$<n>$<sha256>$<m>, $$ExternalSyntheticApiModelOutline. R8 y D8 los generan y no los renombran; su sola presencia identifica la herramienta.
  • El paquete raíz poblado, resultado de -repackageclasses/-flattenpackagehierarchy. El classes.dex de Gmail empieza literalmente por La;, Laai;, Laaj;.
  • META-INF/*.kotlin_module intacto: conservan el nombre del módulo Gradle original y R8 no los toca. En Shazam, ofuscado al máximo, siguen ahí AMSKit_release.kotlin_module y app_googleRelease.kotlin_module; en Reddit, account_impl.kotlin_module, awards_public.kotlin_module, deeplinkdispatch_release.kotlin_module.
  • Ausencia de debug_info_item y source_file_idx a NO_INDEX: barato y sin coste en ejecución; ver Formato DEX, sección 12.

10. Medición propia: la fracción de identificadores cortos

10.1 Definición de la métrica

La métrica de partida para caracterizar la ofuscación de una aplicación es la fracción de identificadores de uno o dos caracteres en el primer DEX. Aquí se ha implementado desde cero —un lector de DEX en Python que abre el base.apk del contenedor, lee su classes.dex y recorre class_defs, class_data_item, field_ids, method_ids y string_ids— con esta definición precisa:

  • Universo: los identificadores definidos en el primer DEX, no los referenciados. Es decir, para los miembros se recorre el class_data_item de cada class_def_item y se siguen los field_idx_diff/method_idx_diff, no se toman las tablas enteras: las tablas incluyen referencias a java.lang.String.length y a medio framework, que no ofusca nadie.
  • Clases: el nombre simple de la clase de nivel superior, deduplicado. De Lcom/foo/Bar$Baz$1; se toma Bar. Los sufijos $1, $2 de clases anónimas los pone el compilador esté ofuscado el código o no, y contarlos como identificadores cortos infla la cifra en un 20 % incluso en aplicaciones limpias.
  • Métodos: se excluyen <init> y <clinit>, que nunca se renombran.
  • Longitud: en unidades UTF-16, leída del utf16_size del string_data_item, lo que evita tener que decodificar MUTF-8 para contar.

10.2 El resultado, y por qué el umbral de dos caracteres es el instrumento equivocado

App Identificadores ≤ 1 ≤ 2 ≤ 3 Mediana SourceFile
YouTube 70.622 62,7 % 85,4 % 92,2 % 1 PG
Maps 60.397 64,2 % 85,4 % 91,0 % 1 PG
Gmail 61.604 69,4 % 83,1 % 91,3 % 1 PG
Photos 63.919 63,1 % 81,4 % 90,7 % 1 PG
Snapchat 97.035 51,2 % 81,2 % 89,7 % 1 SourceFile
Keep 63.545 64,2 % 77,6 % 89,2 % 1 PG
Drive 87.393 72,3 % 76,4 % 87,1 % 1 PG
VLC Remote 72.826 74,5 % 82,1 % 82,7 % 1 SourceFile
WhatsApp 67.796 0,0 % 0,3 % 79,8 % 3 (vacío)
Spotify 82.402 61,4 % 69,3 % 73,8 % 1 SourceFile
Shazam 88.457 50,1 % 54,3 % 70,0 % 1 SourceFile
Duolingo 90.728 45,8 % 55,8 % 64,9 % 2 SourceFile
Twitch 59.406 54,5 % 61,8 % 62,6 % 1 r8-map-id-…
Reddit 97.480 48,2 % 52,8 % 61,2 % 2 r8-map-id-…
My Talking Tom 85.947 51,6 % 55,4 % 56,5 % 1 SourceFile
Telegram 86.187 1,1 % 1,3 % 29,9 % 8 SourceFile
Netflix 80.238 16,7 % 19,2 % 22,0 % 10 (vacío)
Clash Royale 105.944 1,7 % 2,0 % 19,6 % 11 SourceFile
Coin Master 96.598 8,2 % 9,2 % 14,5 % 17 SourceFile (810 distintos)
Signal 83.604 0,3 % 0,6 % 7,4 % 13 R8$$SyntheticClass (2.580)
LinkedIn Learning 81.305 0,7 % 0,9 % 7,1 % 13 SourceFile
Candy Crush 95.783 0,7 % 0,8 % 4,0 % 15 R8$$SyntheticClass (2.486)

La fila de WhatsApp es la que enseña algo. Con el umbral de dos caracteres da 0,3 %: «sin ofuscar». Con tres da 79,8 %. Mirando los nombres se ve por qué: sus clases se llaman LX/000;, LX/001;, LX/00A;exactamente tres caracteres, dentro de un paquete de uno. Es de las aplicaciones más ofuscadas del corpus y el umbral de dos la declara limpia.

La causa es estructural: R8 genera nombres por orden lexicográfico creciente, a, b, …, z, aa, ab, …, aaa, así que cuando el espacio de nombres de un ámbito se llena, los nombres crecen. Una aplicación con 60.000 identificadores en el paquete raíz necesariamente tiene miles de nombres de tres y cuatro caracteres. El umbral fijo no mide «cuánto está ofuscada la app», mide «cuántos identificadores caben en el ámbito antes de desbordar», que depende del tamaño y del esquema de repaquetado.

El indicador robusto es la mediana de la longitud. La distribución es netamente bimodal: en el grupo ofuscado la mediana vale 1 o 2 —3 en WhatsApp—, y en el no ofuscado, entre 8 y 17. No hay nada en medio salvo cuatro aplicaciones con mediana 4 o 6, las de ofuscación parcial.

10.3 El grupo de control de F-Droid

De los 57 APK standalone de F-Droid medidos, 37 caen por debajo del 20 % de identificadores de tres o menos caracteres (mediana de la métrica: 4,4 %; medianas de longitud entre 10 y 17) y 20 caen por encima (mediana: 72,3 %; medianas de longitud de 1 a 6).

Es decir: la premisa de que el corpus de F-Droid es un grupo de control «mayoritariamente sin ofuscar» solo es cierta para dos tercios. F-Droid compila desde el fuente con la configuración Gradle del propio proyecto, y si el proyecto pone minifyEnabled = true, entrega un APK ofuscado. Las siete aplicaciones de Fossify, net.typeblog.shelter, io.github.subhamtyagi.ocr, Aurora Store, AnkiDroid y Bitwarden están tan ofuscadas como Twitch. Como grupo de control real quedan los 37 del extremo bajo: Termux (2,3 %, mediana 14), osmdroid (2,6 %, 15), AntennaPod (2,8 %, 17), F-Droid mismo, VLC, NewPipe, Briar, Signal.

Eso no es un defecto del corpus: es mejor de lo previsto, porque los 20 ofuscados de F-Droid son los únicos casos en los que el código fuente está disponible, y por tanto los únicos donde se puede construir la verdad de terreno de un evaluador de deofuscación.

10.4 Lo que deja la medición

El corpus tiene un gradiente utilizable, del 4,0 % de Candy Crush al 92,2 % de YouTube, con casos intermedios y con casos de ofuscación parcial.

Hay un resultado que conviene destacar, porque contradice la intuición: las aplicaciones de Google están entre las más ofuscadas del corpus. La primera clase de classes.dex de Gmail es La;, seguida de Laai;, Laaj;, Laax;; su SourceFile es PG para las 5.452 clases; su ≤ 3 es del 91,3 % y la mediana de longitud de identificador es 1.

Y el resultado metodológico: usar la mediana de longitud, no un umbral fijo, como indicador principal. Todo lo que afirma la sección 10.2 se ha comprobado byte a byte sobre los binarios.

11. Recetas

11.1 Sobre un APK cualquiera (ejecutadas)

BT=~/Library/Android/sdk/build-tools/37.0.0
CORPUS=~/corpus-apk
Objetivo Orden
Marcador de compilador unzip -p $CORPUS/com.termux_1002.apk classes.dex | strings | grep '~~'
Valores de SourceFile $BT/dexdump -h classes.dex | grep source_file_idx | sort | uniq -c
Nombres de clase definidos $BT/dexdump -h classes.dex | grep 'Class descriptor'
Módulos Kotlin supervivientes unzip -l app.apk 'META-INF/*.kotlin_module'

11.2 Generar un mapping.txt real (ejecutada)

R8 y retrace están disponibles en esta máquina dentro de build-tools/37.0.0/lib/d8.jar, aunque no aparezcan como binarios sueltos. Es la forma de obtener salida real del formato sin instalar nada:

BT=~/Library/Android/sdk/build-tools/37.0.0
java -cp $BT/lib/d8.jar com.android.tools.r8.R8 --version
# R8 9.2.4-dev (build 084a89126ae0596c599143df57a654646af28313 from go/r8bot …)

javac -g -d cls $(find src -name '*.java')
printf -- '-keep class com.ejemplo.Main { public static void main(java.lang.String[]); }\n-keepattributes SourceFile,LineNumberTable\n-dontoptimize\n-dontshrink\n' > reglas.pro
mkdir -p out
java -cp $BT/lib/d8.jar com.android.tools.r8.R8 --release --min-api 21 \
  --lib ~/Library/Android/sdk/platforms/android-34/android.jar \
  --pg-conf reglas.pro --pg-map-output mapping.txt --output out cls/com/ejemplo/*.class

--output debe ser un directorio que ya exista o un .zip/.jar; si no, R8 falla con Invalid output. --pg-compat selecciona el modo compatibilidad; sin él, R8 compila en full mode, que es lo que confirma la sección 8. Todo el mapping.txt de las secciones 6.1 a 6.6 y la ejecución de retrace de la sección 7 salen de este experimento.

11.3 No ejecutadas

java -jar proguard.jar @config.pro                          # ProGuard independiente
java -jar retrace.jar -verbose mapping.txt traza.txt
$ANDROID_HOME/cmdline-tools/latest/bin/retrace mapping.txt trace.txt

⚠️ no ejecutado localmente: ni ProGuard ni cmdline-tools están instalados. La sintaxis de las dos primeras procede del manual de Guardsquare y la de la tercera, de la documentación oficial de Android; esta última es equivalente a la invocación de la sección 11.2.

Fuentes

  1. Enable app optimization with R8 — https://developer.android.com/build/shrink-code Consultado el 12 de agosto de 2026. De aquí salen la separación entre shrinking, optimización y ofuscación (sección 3), las dos DSL y el source set keepRules (sección 4), y el estado de full mode por versión de AGP (sección 8).
  2. Troubleshoot app optimization — https://developer.android.com/topic/performance/app-optimization/troubleshoot-the-optimization Consultado el 12 de agosto de 2026. De aquí salen la ruta de mapping.txt, su empaquetado en el AAB, configuration.txt y la invocación de retrace (secciones 4 y 7).
  3. Add keep rules / Keep rules overview / Optimize your library — https://developer.android.com/topic/performance/app-optimization/add-keep-rules, .../keep-rules-overview y .../library-optimization Consultados el 12 de agosto de 2026. Del primero, la tabla de opciones, modificadores y comodines de la sección 5 y la regla del constructor por defecto implícito; del segundo, las citas sobre reflexión y JNI de la sección 5.3; del tercero, consumerProguardFiles, META-INF/proguard/ y la distinción de la sección 5.1.
  4. Analyze your build with APK Analyzer — https://developer.android.com/studio/debug/apk-analyzer Consultado el 12 de agosto de 2026. De aquí salen seeds.txt y usage.txt y la discrepancia de ruta señalada en la sección 4.
  5. Retrace — https://developer.android.com/tools/retrace Consultado el 12 de agosto de 2026. De aquí salen la definición, la sintaxis y las opciones de retrace de la sección 7.
  6. Android Gradle plugin 8.0.0 release notes — https://developer.android.com/build/releases/past-releases/agp-8-0-0-release-notes Consultado el 12 de agosto de 2026. De aquí sale que android.enableR8.fullMode pasa a true por defecto (sección 8).
  7. Android Gradle plugin 3.4.0 release notes — https://developer.android.com/build/releases/past-releases/agp-3-4-0-release-notes Consultado el 12 de agosto de 2026. De aquí sale el relevo de ProGuard por R8 (sección 2).
  8. R8 — compatibility FAQ — https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md Consultado el 12 de agosto de 2026. De aquí salen las siete diferencias del full mode y la discrepancia señalada al final de la sección 8.
  9. R8 — doc/retrace.md — https://r8.googlesource.com/r8/+/refs/heads/main/doc/retrace.md Consultado el 12 de agosto de 2026. De aquí salen el versionado del formato, el rango catch-all 0:65535, y la semántica de sourceFile, synthesized, rewriteFrame, outline, outlineCallsite y residualsignature (secciones 6.1, 6.4 y 6.6).
  10. R8 — código fuente — https://r8.googlesource.com/r8/ Consultado el 12 de agosto de 2026. Ficheros leídos: src/main/java/com/android/tools/r8/naming/ProguardMapReader.java (gramática del javadoc), ClassNamingForNameMapper.java y Range.java (serialización exacta de la sección 6.4), ProguardMapMarkerInfo.java y MapVersion.java (preámbulo y versión estable de la sección 6.1), SourceFileRewriter.java y graph/DexItemFactory.java (valores centinela SourceFile, PG y r8-map-id- de la sección 9.2), dex/Marker.java (valores de # compiler:) y naming/mappinginformation/*.java (los id de la sección 6.6).
  11. Reglas por defecto que empaqueta AGP — https://android.googlesource.com/platform/tools/base/+/refs/heads/mirror-goog-studio-main/build-system/gradle-core/src/main/resources/com/android/build/gradle/ Consultado el 12 de agosto de 2026. De aquí salen íntegros proguard-header.txt, proguard-common.txt y proguard-optimizations.txt de la sección 5.2.
  12. ProGuard manual — Retrace — https://www.guardsquare.com/manual/tools/retrace Consultado el 12 de agosto de 2026. De aquí sale la gramática formal de la sección 6 y la sintaxis de comodines de retrace.jar de la sección 7.
  13. ProGuard manual — Usage / Home — https://www.guardsquare.com/manual/configuration/usage y https://www.guardsquare.com/manual/home Consultados el 12 de agosto de 2026. De aquí salen -renamesourcefileattribute, -keepattributes, -printmapping, -applymapping, -printseeds, -printusage, y los cuatro pasos de ProGuard de la sección 2.
  14. The ProGuard story: 20 years of innovation — https://www.guardsquare.com/blog/the-proguard-story-20-years-of-innovation-in-java-optimization-guardsquare Consultado el 12 de agosto de 2026. De aquí salen Eric Lafortune, el primer commit del 1 de mayo de 2002, Gingerbread, 2012 y 2014 (sección 2).
  15. Guardsquare/proguard — https://github.com/Guardsquare/proguard Consultado el 12 de agosto de 2026. De aquí salen la licencia GPL-2.0 con excepciones y el tag v7.9.1 (sección 2). ⚠️ El año de esa release no es visible en la interfaz.
  16. Dalvik executable format — https://source.android.com/docs/core/runtime/dex-format Consultado el 12 de agosto de 2026. De aquí sale source_file_idx del class_def_item y el papel de dalvik.annotation.Signature (secciones 8 y 9.2).
  17. Mediciones propias sobre el corpus y el SDK — ejecutadas el 12 de agosto de 2026 sobre ~/corpus-apk con build-tools 37.0.0 (R8/Retrace 9.2.4-dev de lib/d8.jar, dexdump, aapt2), Java 21.0.10 y Python 3.12. De aquí salen: la salida real de R8 y retrace de las secciones 6, 7 y 11.2; el recuento de marcadores ~~R8 de la sección 9.1; la distribución de SourceFile de la sección 9.2; y toda la medición de identificadores de la sección 10.