R8 y ProGuard
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
R8son sintaxis deProGuard, y los ficheros se siguen llamandoproguard-rules.pro. - El formato de
mapping.txt.R8produce un superconjunto compatible (sección 6). - La marca
PGen el atributoSourceFile(sección 9.2), queR8sigue reconociendo como valor centinela heredado. - Proyectos antiguos y builds no-Android que siguen invocando
ProGuarddirectamente, yDexGuard, que es «compatible hacia atrás conProGuard» 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 |
Sí | Los que se especifiquen | Siempre |
-keepclassmembers |
No por sí sola | Sí | Si la clase se conserva por otra vía |
-keepclasseswithmembers |
Sí | Sí | 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> |
Sí | D8, L8, R8, R8Partial, Relocator, TraceReferences… |
# compiler_version: <v> |
Sí | 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 |
Sí | 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> |
Sí | Identificador del mapping |
# pg_map_hash: SHA-256 <hash> |
Sí | 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:
R8omiteoriginalRangecuando es igual aminifiedRange. La condición literal es!originalRange.equals(minifiedRange). Así que1:3:void <init>(java.lang.String):7:7y1:7:void main(java.lang.String[]):5:11conviven con líneas sin la segunda mitad.- Un
Range«cardinal» se serializa como un solo número, no comofrom:to. De ahí la tercera forma que documenta el javadoc deProguardMapReader:range COLON signature COLON number ARROW name. - 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:
- El constructor por defecto
<init>()no se conserva implícitamente al conservar una clase. - Tampoco se conserva para tipos que solo se usan con
ldc,instanceofocheckcast. - Las clases que contienen campos o métodos casados por una
-keepclassmembersno se consideran implícitamente instanciadas. - Los métodos
defaultno se conservan implícitamente como métodos abstractos. - 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. - Al optimizar o minificar, el atributo
SourceFilesiempre se reescribe aSourceFilesalvo que se use-renamesourcefileattribute. - 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.gmssin renombrar junto a clasesLa;,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 encom/google/android/gms/internal/castsin ofuscar al lado de 17.329 clases en un paquete llamadoo.- Clases sintéticas con nombres reconocibles:
$$ExternalSyntheticLambda<n>,$$InternalSyntheticLambda$<n>$<sha256>$<m>,$$ExternalSyntheticApiModelOutline.R8yD8los generan y no los renombran; su sola presencia identifica la herramienta. - El paquete raíz poblado, resultado de
-repackageclasses/-flattenpackagehierarchy. Elclasses.dexde Gmail empieza literalmente porLa;,Laai;,Laaj;. META-INF/*.kotlin_moduleintacto: conservan el nombre del módulo Gradle original yR8no los toca. En Shazam, ofuscado al máximo, siguen ahíAMSKit_release.kotlin_moduleyapp_googleRelease.kotlin_module; en Reddit,account_impl.kotlin_module,awards_public.kotlin_module,deeplinkdispatch_release.kotlin_module.- Ausencia de
debug_info_itemysource_file_idxaNO_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 elclass_data_itemde cadaclass_def_itemy se siguen losfield_idx_diff/method_idx_diff, no se toman las tablas enteras: las tablas incluyen referencias ajava.lang.String.lengthy 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 tomaBar. Los sufijos$1,$2de 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_sizedelstring_data_item, lo que evita tener que decodificarMUTF-8para 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 |
| 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-… |
| 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
- 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 deAGP(sección 8). - 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 elAAB,configuration.txty la invocación deretrace(secciones 4 y 7). - Add keep rules / Keep rules overview / Optimize your library —
https://developer.android.com/topic/performance/app-optimization/add-keep-rules,
.../keep-rules-overviewy.../library-optimizationConsultados 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 yJNIde la sección 5.3; del tercero,consumerProguardFiles,META-INF/proguard/y la distinción de la sección 5.1. - 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.txtyusage.txty la discrepancia de ruta señalada en la sección 4. - 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
retracede la sección 7. - 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.fullModepasa atruepor defecto (sección 8). - 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
ProGuardporR8(sección 2). - 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.
- 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-all0:65535, y la semántica desourceFile,synthesized,rewriteFrame,outline,outlineCallsiteyresidualsignature(secciones 6.1, 6.4 y 6.6). - 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.javayRange.java(serialización exacta de la sección 6.4),ProguardMapMarkerInfo.javayMapVersion.java(preámbulo y versión estable de la sección 6.1),SourceFileRewriter.javaygraph/DexItemFactory.java(valores centinelaSourceFile,PGyr8-map-id-de la sección 9.2),dex/Marker.java(valores de# compiler:) ynaming/mappinginformation/*.java(losidde la sección 6.6). - 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.txtyproguard-optimizations.txtde la sección 5.2. - 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.jarde la sección 7. - 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 deProGuardde la sección 2. - 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).
- 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. - Dalvik executable format — https://source.android.com/docs/core/runtime/dex-format
Consultado el 12 de agosto de 2026. De aquí sale
source_file_idxdelclass_def_itemy el papel dedalvik.annotation.Signature(secciones 8 y 9.2). - Mediciones propias sobre el corpus y el SDK — ejecutadas el 12 de agosto de 2026
sobre
~/corpus-apkconbuild-tools37.0.0 (R8/Retrace9.2.4-dev delib/d8.jar,dexdump,aapt2), Java 21.0.10 y Python 3.12. De aquí salen: la salida real deR8yretracede las secciones 6, 7 y 11.2; el recuento de marcadores~~R8de la sección 9.1; la distribución deSourceFilede la sección 9.2; y toda la medición de identificadores de la sección 10.