BookinglyTech News
Software

R8 y los cierres que solo aparecen en release: cómo depurarlos sin desactivar la optimización

Un build de debug funciona y el de release se cae solo con la minificación activa. El truco de meter wildcards o apagar R8 tapa el síntoma, pero no dice qué se rompió.

3 min de lecturaDev.to0 vistas

Un build de debug funciona. El de release instala. Y una pantalla concreta se cae solo cuando la minificación está activa. La reacción habitual es meter un -keep class com.example.** { *; } o, directamente, desactivar R8 y enviar. Las dos cosas hacen desaparecer el síntoma, pero ninguna explica qué se rompió. Un fallo de R8 que solo aparece en release se resuelve antes si lo tratas como un problema de referencias indirectas y no como un concurso de adivinanzas con ProGuard.

Demuestra que la optimización es la frontera

No empieces editando las keep rules. Reproduce el fallo con la misma variante de release que reciben los usuarios: el tipo de build, el flavor, el grafo de dependencias, el resource shrinking, la firma y el código generado pueden diferir del debug. La primera comparación útil es simple. Debug funciona, release con R8 falla, release sin R8 funciona. Eso apunta a optimización, shrinking, ofuscación o a una regla que viene de una dependencia, aunque todavía no te dice qué clase hay que conservar. Si el pipeline ya está automatizado, conviene que compile las variantes reales de release y no solo APKs de debug; el artículo sobre CI de Android lo detalla.

Retrace antes de leer el crash

Un stack trace ofuscado es una prueba con las etiquetas arrancadas. R8 genera un mapping.txt por variante optimizada. Guarda ese archivo de cada build publicado, porque uno posterior puede sobrescribir la copia local. Android recomienda pasar retrace cuando el stack original no se desofusca solo. Después, la pregunta no es solo qué clase se cayó, sino qué mecanismo en tiempo de ejecución esperaba que esa clase, método, campo, constructor, anotación o nombre siguiera siendo descubrible. Ahí suele estar la regla que falta.

R8 sigue bien las referencias estáticas. Los problemas llegan cuando el programa alcanza código por caminos que el análisis estático no puede inferir: reflexión, clases cargadas desde cadenas, JNI, frameworks de serialización que inspeccionan miembros o librerías que descubren implementaciones de forma dinámica. ClassNotFoundException, NoSuchMethodException, NoClassDefFoundError y compañía son señales útiles. Cuando algo hace Class.forName(className), para el runtime esa clase es obligatoria; para el optimizador, una cadena con un nombre no es una referencia de código normal.

Mira la configuración real y escribe la regla más estrecha

Tu proguard-rules.pro no es toda la configuración. Hay reglas del propio proceso, del tooling de Android y consumer rules empaquetadas por dependencias, así que conviene inspeccionar la configuración fusionada que R8 evaluó de verdad.

Para el caso contrario, código que esperabas que se eliminara y sigue presente, está -whyareyoukeeping, que muestra la cadena de referencias que lo retiene. Úsalo como instrumento temporal, no como configuración permanente: encarece el build.

Una regla ancha vale como experimento. Si conservar un paquete entero arregla el crash, la frontera está ahí dentro, pero eso no debería ser el arreglo final. Describe el límite real: si solo se cargan dinámicamente implementaciones de una interfaz, conserva esas implementaciones y el constructor que necesita el cargador. La mejor keep rule no es la más corta, es la que coincide con el comportamiento que sabes explicar.