R8 Optimized Resource Shrinking en AGP 9: Cuando el análisis estático se encuentra con el acceso dinámico
por Nikolay Vlasov
La transición a Android Gradle Plugin (AGP) 9.0 ha introducido un cambio significativo en el funcionamiento de la reducción de recursos (resource shrinking). Cuando se activa isShrinkResources = true en tu tipo de compilación, AGP utiliza ahora el Optimized Resource Shrinking. Este mecanismo vincula estrechamente el análisis de recursos con el análisis de código, eliminando cualquier recurso referenciado únicamente por código no utilizado. Aunque esto reduce el tamaño de los APK, también puede causar problemas en proyectos que dependen del acceso dinámico a recursos, problemas que podrían haber pasado desapercibidos en versiones anteriores. (Android Developers)
¿Por qué desaparecen los recursos en Dynamic Feature Modules?
Los reductores de recursos modernos dependen en gran medida del análisis estático. Si se hace referencia a un recurso directamente en el código (por ejemplo, R.raw.my_resource), el reductor puede rastrear fácilmente su uso. Sin embargo, cuando se accede a los recursos dinámicamente mediante nombres de cadena y búsquedas en tiempo de ejecución (runtime lookups), el análisis estático ya no puede demostrar de forma fiable que el recurso es necesario. Con AGP 9, esto se vuelve aún más pronunciado, ya que la reducción optimizada de recursos forma parte de la canalización unificada de optimización de código y recursos. (Android Developers)
En proyectos que utilizan Dynamic Feature Modules (DFM), el módulo base suele carecer de dependencias en tiempo de compilación de las clases R de la función, especialmente cuando los módulos se entregan de forma independiente a través de Play Feature Delivery. Aunque el nuevo reductor de recursos soporta funciones dinámicas desde hace tiempo, es precisamente en esta arquitectura donde los problemas de "recursos faltantes" ocurren con más frecuencia debido a las búsquedas dinámicas. (Android Developers)
Tomemos como ejemplo un fragmento del proyecto Smart Directory Agent Center. La carga de un índice de búsqueda de texto completo se implementa de la siguiente manera:
private fun getFtsId(): Int {
return dynamicContextWrapper.getContextForFts().resources.getIdentifier(
"fts",
"raw",
featuresManager.ftsModulePackage,
)
}
Para el reductor de recursos, esta llamada es una "caja negra": el nombre del recurso se pasa como una cadena en lugar de una referencia estática. Como resultado, las herramientas de compilación podrían asumir incorrectamente que el recurso no se utiliza y eliminarlo durante la compilación de la versión de lanzamiento, lo que provocaría una excepción Resources$NotFoundException en tiempo de ejecución. (Android Developers)
La evolución de Resource Shrinking en AGP
Es importante entender que AGP 9.0 no reinventó la reducción de recursos desde cero, sino que hizo que su comportamiento fuera más riguroso y predecible dentro de la canalización optimizada. El Precise Resource Shrinking ya estaba activado por defecto en AGP 8.3. En AGP 9.0, el Optimized Resource Shrinking se activa automáticamente cuando isShrinkResources = true. Para las versiones entre la 8.6 y la 9.0, este comportamiento solía requerir el flag explícito android.r8.optimizedResourceShrinking=true. (Android Developers)
Ten en cuenta la regresión en AGP 8.9: las notas de la versión documentaron un error en la reducción de recursos que causaba la desaparición de recursos en los módulos de funciones dinámicas. Esto se solucionó posteriormente en las versiones 8.9.2 y 8.10. Si experimentas un fallo en tu compilación de lanzamiento, puede que no sea un fallo arquitectónico; podría ser un error en una versión específica del reductor. (Android Developers)
La solución real: archivos keep y retención explícita de recursos
La forma correcta de proteger los recursos utilizados indirectamente es definir un archivo keep en los recursos de tu proyecto. La documentación recomienda crear un archivo XML en res/raw/ (por ejemplo, res/raw/my.package.keep.xml) en lugar de confiar en un "keep.xml global" como mecanismo especial. El uso de un nombre de archivo único para el archivo keep evita conflictos durante la fusión de recursos en proyectos multimodulares. (Android Developers)
En nuestro caso, la solución fue la siguiente:
<!-- Ruta: app/src/main/res/raw/com.example.app.keep.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@style/Theme.StartingScreen_*,@style/AppTheme_*,@raw/fts" />
Este enfoque hace que tu intención sea explícita: el recurso @raw/fts no debe ser eliminado, aunque solo sea accesible mediante la resolución dinámica de nombres. Esto es fundamental para cualquier recurso leído a través de Resources.getIdentifier(). (Android Developers)
Buenas prácticas para AGP 9 y R8
Primero, para los recursos a los que se accede dinámicamente, es mejor enumerarlos explícitamente en tools:keep en lugar de confiar en comodines genéricos como @raw/*. Cuanto más específicas sean tus reglas, menos probable será que conserves recursos innecesarios y más predecible será el comportamiento de tu compilación. (Android Developers)
Segundo, ten en cuenta el shrinkMode. Por defecto, se utiliza el modo seguro (safe mode), que intenta preservar los recursos que podrían usarse dinámicamente mediante getIdentifier(). Si cambias a tools:shrinkMode="strict", solo se conservarán los recursos referenciados explícitamente, por lo que la precisión de tus reglas keep se vuelve crítica. (Android Developers)
Tercero, no confundas la causa con el síntoma. El problema no es que tu proyecto utilice DFM; es que algunos recursos solo son alcanzables mediante búsquedas en tiempo de ejecución y permanecen invisibles para el análisis estático. DFM simplemente hace que este escenario sea más común y más evidente durante la optimización. (Android Developers)
Resumen
La falta de recursos en AGP 9 no es fruto de la magia ni de un error aleatorio. Son una consecuencia lógica de un análisis estático más estricto en la reducción optimizada de recursos. Si tu arquitectura depende del acceso dinámico a los recursos, debes protegerlos explícitamente mediante archivos keep y verificar el comportamiento con el modo de reducción elegido. Si aparecen fallos específicamente después de actualizar a AGP 8.9–9.0, comprueba la versión de tu plugin, ya que las regresiones conocidas en esta área ya se han solucionado en parches posteriores. (Android Developers)
Recursos útiles
- 1 Enable app optimization with R8 — Guía oficial para configurar R8.
- 2 Android Gradle Plugin 7.1.0 Release Notes — Registro de cambios y soporte de Dynamic Features.
- 3 Tools attributes reference — Descripción de los atributos
keepyshrinkMode. - 4 Android Gradle Plugin 8.3.0 Release Notes — Detalles sobre Precise Resource Shrinking.
- 5 Android Gradle Plugin 8.9.0 Release Notes — Información sobre correcciones de errores en la reducción de recursos.
- 6 Customize which resources to keep — Guía detallada sobre el trabajo con
keep.xml.