Novedades de productos

Configura y soluciona problemas de las reglas de conservación de R8

Lectura de 7 min

En el desarrollo moderno de Android, enviar una aplicación pequeña, rápida y segura es una expectativa fundamental de los usuarios. La herramienta principal del sistema de compilación de Android para lograrlo es el optimizador R8 , el compilador que controla el código inactivo y la eliminación de recursos para la reducción, el cambio de nombre o la reducción del código, y la optimización de la app.

Habilitar R8 es un paso fundamental para preparar una app para su lanzamiento, pero requiere que los desarrolladores proporcionen orientación en forma de "reglas de conservación".

Después de leer este artículo, mira el video de la Semana de Performance Spotlight sobre cómo habilitar, depurar y solucionar problemas del optimizador de R8 en YouTube.

 

 

Por qué se necesitan las reglas de conservación

La necesidad de escribir reglas de conservación surge de un conflicto central: R8 es una herramienta de análisis estático, pero las apps para Android suelen depender de patrones de ejecución dinámicos, como la reflexión o las llamadas dentro y fuera del código nativo con la JNI (interfaz nativa de Java).

R8 compila un gráfico del código usado analizando las llamadas directas. Cuando se accede al código de forma dinámica, el análisis estático de R8 no puede predecirlo y lo identificará como sin usar y lo quitará, lo que provocará fallas en el tiempo de ejecución.

Una regla de conservación es una instrucción explícita para el compilador de R8 que indica lo siguiente: "Esta clase, método o campo específico es un punto de entrada al que se accederá de forma dinámica durante el tiempo de ejecución. Debes conservarlo, incluso si no encuentras una referencia directa a él".

Consulta la guía oficial para obtener más detalles sobre las reglas de conservación.

Dónde escribir las reglas de conservación

Las reglas de conservación personalizadas para una aplicación se escriben en un archivo de texto. Por convención, este archivo se llama proguard-rules.pro y se encuentra en la raíz del módulo de la app o biblioteca. Luego, este archivo se especifica en el tipo de compilación release del archivo build.gradle.kts de tu módulo.

release {

    isShrinkResources = true

    isMinifyEnabled = true

    proguardFiles(

        getDefaultProguardFile("proguard-android-optimize.txt"),

        "proguard-rules.pro",

    )

}

Usa el archivo predeterminado correcto

El método getDefaultProguardFile importa un conjunto predeterminado de reglas que proporciona el SDK de Android. Si usas el archivo incorrecto, es posible que tu app no se optimice. Asegúrate de usar proguard-android-optimize.txt. Este archivo proporciona las reglas de conservación predeterminadas para los componentes estándar de Android y habilita las optimizaciones de código de R8. La versión desactualizada de proguard-android.txt solo proporciona las reglas de conservación, pero no habilita las optimizaciones de R8.

progaurd.png

Como se trata de un problema de rendimiento grave, comenzaremos a advertir a los desarrolladores sobre el uso del archivo incorrecto a partir de la actualización de funciones de Android Studio Narwhal 3. Además, a partir de la versión 9.0 del complemento de Android para Gradle, ya no admitiremos el archivo proguard-android.txt obsoleto. Por lo tanto, asegúrate de actualizar a la versión optimizada.

Cómo escribir reglas de conservación

Una regla de conservación consta de tres partes principales:

  1. Una opción como -keep o -keepclassmembers
  2. Modificadores opcionales como allowshrinking
  3. Una especificación de clase que define el código que debe coincidir

Para obtener la sintaxis completa y ejemplos, consulta la guía para agregar reglas de conservación.

Antipatrones de reglas de conservación

Es importante conocer las prácticas recomendadas, pero también los antipatrones. Estos antipatrones suelen surgir de malentendidos o atajos para solucionar problemas, y pueden ser catastróficos para el rendimiento de una compilación de producción.

Opciones globales

Estas marcas son parámetros de configuración globales que nunca deben usarse en una compilación de lanzamiento. Solo se usan para la depuración temporal con el objetivo de aislar un problema.

El uso de -dontotptimize inhabilita de manera eficaz las optimizaciones de rendimiento de R8, lo que genera una app más lenta.

Cuando usas -dontobfuscate, inhabilitas todos los cambios de nombre, y cuando usas -dontshrink, desactivas la eliminación de código no utilizado. Ambas reglas globales aumentan el tamaño de la app.

Siempre que sea posible, evita usar estas marcas globales en un entorno de producción para que la experiencia del usuario de la app sea más eficiente.

Reglas de conservación demasiado amplias

La forma más fácil de anular los beneficios de R8 es escribir reglas de conservación demasiado amplias. Las reglas de conservación como la que se muestra a continuación indican al optimizador de R8 que no reduzca, no ofusque y no optimice ninguna clase de este paquete ni ninguno de sus subpaquetes. Esto quita por completo los beneficios de R8 para todo ese paquete. En su lugar, intenta escribir reglas de conservación limitadas y específicas.
 

-keep class com.example.package.** { *;} // WIDE KEEP RULES CAUSE PROBLEMS

El operador de inversión (!)

El operador de inversión (!) parece una forma potente de excluir un paquete de una regla. Pero no es tan sencillo. Veamos este ejemplo.

-keep class !com.example.my_package.** { *; } // USE WITH CAUTION

Tal vez pienses que esta regla significa "no conservar las clases encom.example.package", pero, en realidad, significa "conservar todas las clases, los métodos y las propiedades de toda la aplicación que no estén en com.example.package". Si esto te sorprende, te recomendamos que verifiques si hay negaciones en tu configuración de R8.

Reglas redundantes para componentes de Android

Otro error común es agregar manualmente reglas de conservación para los archivos Activities, Services o BroadcastReceivers de tu app. Esto es innecesario. El archivo proguard-android-optimize.txt predeterminado ya incluye las reglas pertinentes para que estos componentes estándar de Android funcionen de inmediato.

Además, muchas bibliotecas traen sus propias reglas de conservación. Por lo tanto, no deberías tener que escribir tus propias reglas para estos. En caso de que haya un problema con las reglas de conservación de una biblioteca que estés usando, lo mejor es que te comuniques con el autor de la biblioteca para ver cuál es el problema.

Prácticas recomendadas para las reglas de conservación

Ahora que sabes lo que no debes hacer, hablemos de las prácticas recomendadas.

Escribe reglas de conservación específicas

Las reglas de conservación adecuadas deben ser lo más restringidas y específicas posible. Solo deben conservar lo que sea necesario y permitir que R8 optimice todo lo demás.
 

ReglaCalidad

 

-keep class com.example.** { ; }

 

Bajo: Conserva un paquete completo y sus subpaquetes.

 

-keep class com.example.MyClass { ; }

 

Bajo: Conserva una clase completa que, probablemente, aún sea demasiado amplia.
-keepclassmembers class com.example.MyClass {

    private java.lang.String secretMessage;

    public void onNativeEvent(java.lang.String);

}
Alto: Solo se conservan los métodos y las propiedades relevantes de una clase específica.

Usa ancestros comunes

En lugar de escribir reglas de conservación independientes para varios modelos de datos diferentes, escribe una regla que se oriente a una clase o interfaz base común. La siguiente regla le indica a R8 que conserve cualquier miembro de las clases que implementen esta interfaz y es altamente escalable.

# Keep all fields of any class that implements SerializableModel

-keepclassmembers class * implements com.example.models.SerializableModel {

    <fields>;

}

Usa anotaciones para segmentar varias clases

Crea una anotación personalizada (p.ej., @Serialize) y úsala para "etiquetar" las clases que necesitan que se conserven sus campos. Este es otro patrón limpio, declarativo y altamente escalable. También puedes crear reglas de conservación para las anotaciones ya existentes de los frameworks que usas.

# Keep all fields of any class annotated with @Serialize

-keepclassmembers class * {

    @com.example.annotations.Serialize <fields>;

}

Elige la opción de conservación adecuada

La opción Conservar es la parte más importante de la regla. Elegir la opción incorrecta puede inhabilitar la optimización de forma innecesaria.

Opción Keep¿Qué hace?
-keepEvita que se quiten o se cambien los nombres de la clase y los miembros mencionados en la declaración .
-keepclassmembersImpide que se quiten o se cambie el nombre de los miembros especificados, pero permite que se quite la clase en sí, aunque solo en las clases que no se quiten de otra manera.
-keepclasseswithmembersUna combinación: Conserva la clase y sus miembros, solo si están presentes todos los miembros especificados.

Puedes obtener más información sobre la opción de conservar en nuestra documentación sobre las opciones de conservación.

Permitir la optimización con modificadores

Los modificadores como allowshrinking y allowobfuscation relajan una regla -keep amplia, lo que devuelve el poder de optimización a R8. Por ejemplo, si una biblioteca heredada te obliga a usar -keep en una clase completa, es posible que puedas recuperar cierta optimización permitiendo la reducción y la ofuscación:

# Keep this class, but allow R8 to remove it if it's unused and allow R8 to rename it.

-keep,allowshrinking,allowobfuscation class com.example.LegacyClass

Agrega opciones globales para obtener una optimización adicional

Además de las reglas de conservación, puedes agregar marcas globales a tu archivo de configuración de R8 para fomentar aún más la optimización.

-repackageclasses es una opción potente que le indica a R8 que mueva todas las clases ofuscadas a un solo paquete. Esto ahorra mucho espacio en el archivo DEX, ya que quita las cadenas de nombres de paquetes redundantes.

-allowaccessmodification permite que R8 amplíe el acceso (p.ej., private a public) para habilitar una inserción más agresiva. Ahora, esta opción está habilitada de forma predeterminada cuando se usa proguard-android-optimize.txt.

Advertencia: Los autores de bibliotecas nunca deben agregar estas marcas de optimización global a sus reglas de consumidor, ya que se aplicarían de forma forzosa a toda la app.

Para que quede aún más claro, en la versión 9.0 del complemento de Android para Gradle, comenzaremos a ignorar por completo las marcas de optimización globales de las bibliotecas. 

Prácticas recomendadas para bibliotecas

Todas las apps para Android dependen de bibliotecas de una forma u otra. Hablemos de las prácticas recomendadas para las bibliotecas.

Para desarrolladores de bibliotecas

Si tu biblioteca usa reflexión o JNI, tienes la responsabilidad de proporcionar las reglas de conservación necesarias a sus consumidores. Estas reglas se colocan en un archivo consumer-rules.pro, que luego se empaqueta automáticamente dentro del archivo AAR de la biblioteca.

android {

    defaultConfig {

        consumerProguardFiles("consumer-rules.pro")

    }

    ...

}

Para consumidores de bibliotecas

Cómo filtrar las reglas de conservación problemáticas

Si debes usar una biblioteca que incluya reglas de conservación problemáticas, puedes filtrarlas en tu archivo build.gradle.kts a partir de AGP 9.0. Esto le indica a R8 que ignore las reglas que provienen de una dependencia específica.

release {

    optimization.keepRules {

        // Ignore all consumer rules from this specific library

        it.ignoreFrom("com.somelibrary:somelibrary")

    }

}

La mejor regla de conservación es no tener ninguna

La estrategia definitiva de configuración de R8 es eliminar por completo la necesidad de escribir reglas de conservación. En muchas apps, esto se puede lograr eligiendo bibliotecas modernas que favorezcan la generación de código por sobre la reflexión. Con la generación de código, el optimizador puede determinar con mayor facilidad qué código se usa realmente en el entorno de ejecución y qué código se puede quitar. Además, no usar ninguna reflexión dinámica significa que no hay puntos de entrada "ocultos" y, por lo tanto, no se necesitan reglas de conservación. Cuando elijas una biblioteca nueva, siempre prefiere una solución que use la generación de código en lugar de la reflexión.

Para obtener más información sobre cómo elegir bibliotecas, consulta Elige la biblioteca con prudencia.

Depura y soluciona problemas de tu configuración de R8

Cuando R8 quite código que debería haber conservado o tu APK sea más grande de lo esperado, usa estas herramientas para diagnosticar el problema.

Cómo encontrar reglas de conservación duplicadas y globales

Como R8 combina reglas de decenas de fuentes, puede ser difícil saber cuál es el conjunto de reglas "final". Si agregas esta marca a tu archivo proguard-rules.pro, se generará un informe completo:

# Outputs the final, merged set of rules to the specified file

-printconfiguration build/outputs/logs/configuration.txt

Puedes buscar en este archivo para encontrar reglas redundantes o rastrear una regla problemática (como -dontoptimize) hasta la biblioteca específica que la incluyó.

Pregunta para R8: ¿Por qué conservas esto?

Si una clase que esperabas que se quitara todavía está en tu app, R8 puede decirte por qué. Solo agrega esta regla:

# Asks R8 to explain why it's keeping a specific class

class com.example.MyUnusedClass

-whyareyoukeeping 

Durante la compilación, R8 imprimirá la cadena exacta de referencias que provocó que se conservara esa clase, lo que te permitirá rastrear la referencia y ajustar tus reglas.

Para obtener una guía completa, consulta la sección Soluciona problemas de R8.

Próximos pasos

R8 es una herramienta potente para mejorar el rendimiento de las apps para Android. Su eficacia depende de que se comprenda correctamente su funcionamiento como motor de análisis estático.

Si escribes reglas específicas a nivel de miembro, aprovechas los ancestros y las anotaciones, y eliges con cuidado las opciones de conservación adecuadas, puedes preservar exactamente lo que es necesario. La práctica más avanzada es eliminar por completo la necesidad de reglas eligiendo bibliotecas modernas basadas en codegen en lugar de sus predecesoras basadas en reflexión.

Mientras sigues la Semana de Spotlight sobre el rendimiento, asegúrate de mirar el video de hoy en YouTube y continuar con nuestro desafío de R8. Usa #optimizationEnabled para cualquier pregunta sobre cómo habilitar o solucionar problemas de R8. Estamos aquí para ayudarte.

Es hora de que veas los beneficios por tu cuenta.

Te desafiamos a habilitar el modo completo de R8 para tu app hoy mismo.

  1. Sigue nuestras guías para desarrolladores y comienza: Habilita la optimización de apps.
  2. Verifica si aún usas proguard-android.txt y reemplázalo por proguard-android-optimize.txt.
  3. Luego, mide el impacto. No solo sientas la diferencia, compruébala. Para medir las mejoras de rendimiento, adapta el código de nuestra app de ejemplo de Macrobenchmark en GitHub para medir los tiempos de inicio antes y después.

Estamos seguros de que notarás una mejora significativa en el rendimiento de tu app.

Mientras tanto, usa la etiqueta #AskAndroid en redes sociales para enviar tus preguntas. Durante la semana, nuestros expertos supervisarán y responderán tus preguntas.

No te pierdas el contenido de mañana, en el que hablaremos sobre la optimización guiada por perfiles con perfiles de Baseline y de inicio, compartiremos cómo mejoró el rendimiento de renderización de Compose en las versiones anteriores y compartiremos consideraciones de rendimiento para el trabajo en segundo plano.

Escrito por:
Continuar leyendo