Cómo reducir, ofuscar y optimizar tu app

Para que tu app sea lo más pequeña y rápida posible, debes optimizar y reducir la compilación de lanzamiento con isMinifyEnabled = true.

De esta manera, se habilita la reducción, que quita el código sin usar, la ofuscación, que acorta los nombres de las clases y los miembros de tu app, y la optimización, que aplica estrategias de optimización de código mejoradas para reducir aún más el tamaño y mejorar el rendimiento de tu app. En esta página, se describe cómo R8 realiza estas tareas de tiempo de compilación para tu proyecto y cómo puedes personalizarlas.

Cuando compilas tu proyecto mediante el complemento de Android para Gradle 3.4.0 o una versión posterior, el complemento ya no usa ProGuard a fin de realizar la optimización del código en tiempo de compilación. En cambio, el complemento trabaja con el compilador R8 para administrar las siguientes tareas en tiempo de compilación:

  • Reducción de código (o eliminación de código obsoleto): Detecta y quita de forma segura de tu app las clases, los campos, los métodos y los atributos que no se usan, y sus dependencias de biblioteca (lo que la convierte en una herramienta útil para evitar sobrepasar el límite de referencia de 64,000). Por ejemplo, si usas solo unas pocas API de una dependencia de biblioteca, la reducción puede identificar y quitar solo el código de biblioteca que tu app no usa. A fin de obtener más información, ve a la sección para reducir tu código.
  • Reducción de recursos: Quita los recursos que no se usan de tu aplicación empaquetada, incluidos los que no se usan en las dependencias de la biblioteca de tu app. Funciona junto con la reducción de código, de modo que cuando se quite el código que no se usa, también se puedan quitar de forma segura los recursos a los que ya no se haga referencia. A fin de obtener más información, consulta la sección para reducir tus recursos.
  • Optimización: Inspecciona y vuelve a escribir el código para mejorar el rendimiento del tiempo de ejecución y reducir aún más el tamaño de los archivos DEX de tu app. Esto mejora el rendimiento del tiempo de ejecución del código hasta en un 30%, lo que mejora de forma significativa el tiempo de inicio y la latencia de fotogramas. Por ejemplo, si R8 detecta que nunca se toma la rama else {} para una declaración "if/else" determinada, quita el código para esa rama else {}. Para obtener más información, consulta la sección sobre optimización de código.
  • Ofuscación (o reducción de identificadores): Acorta el nombre de las clases y los miembros, lo que genera archivos DEX de menor tamaño. Para obtener más información, consulta la sección sobre cómo ofuscar código.

Cuando compilas la versión de lanzamiento de tu app, R8 se puede configurar para que realice las tareas en tiempo de compilación descritas anteriormente por ti. También puedes inhabilitar ciertas tareas o personalizar el comportamiento de R8 por medio de los archivos de reglas ProGuard. De hecho, R8 funciona con todos tus archivos de reglas ProGuard existentes, por lo que no es necesario que cambies las reglas existentes si deseas actualizar el complemento de Gradle para Android a fin de que use R8.

Cómo habilitar la reducción, la ofuscación y la optimización

Cuando usas Android Studio 3.4 o el complemento de Gradle para Android 3.4.0 y versiones posteriores, R8 es el compilador predeterminado que convierte el código de bytes Java de tu proyecto al formato DEX, que se ejecuta en la plataforma de Android. Sin embargo, cuando creas un nuevo proyecto con Android Studio, las opciones de reducción, ofuscación y optimización de código no están habilitadas de manera predeterminada, ya que estas optimizaciones en tiempo de compilación aumentan el tiempo que tarda en compilarse tu proyecto y pueden introducir errores si no personalizas correctamente el código que se conservará.

Por lo tanto, es mejor que habilites estas tareas en tiempo de compilación cuando compiles la versión final de la app que probarás antes de publicar. Para habilitar la reducción, la ofuscación y la optimización, incluye lo siguiente en tu secuencia de comandos de compilación a nivel del proyecto.

Kotlin

android {
    buildTypes {
        getByName("release") {
            // Enables code shrinking, obfuscation, and optimization for only
            // your project's release build type. Make sure to use a build
            // variant with `isDebuggable=false`.
            isMinifyEnabled = true

            // Enables resource shrinking, which is performed by the
            // Android Gradle plugin.
            isShrinkResources = true

            proguardFiles(
                // Includes the default ProGuard rules files that are packaged with
                // the Android Gradle plugin. To learn more, go to the section about
                // R8 configuration files.
                getDefaultProguardFile("proguard-android-optimize.txt"),

                // Includes a local, custom Proguard rules file
                "proguard-rules.pro"
            )
        }
    }
    ...
}

Groovy

android {
    buildTypes {
        release {
            // Enables code shrinking, obfuscation, and optimization for only
            // your project's release build type. Make sure to use a build
            // variant with `debuggable false`.
            minifyEnabled true

            // Enables resource shrinking, which is performed by the
            // Android Gradle plugin.
            shrinkResources true

            // Includes the default ProGuard rules files that are packaged with
            // the Android Gradle plugin. To learn more, go to the section about
            // R8 configuration files.
            proguardFiles getDefaultProguardFile(
                    'proguard-android-optimize.txt'),
                    'proguard-rules.pro'
        }
    }
    ...
}

Archivos de configuración de R8

R8 usa archivos de reglas ProGuard para modificar su comportamiento predeterminado y comprender mejor la estructura de tu app, como las clases que actúan como puntos de entrada al código de tu app. Si bien puedes modificar algunos de los archivos de reglas, es posible que algunas reglas se generen automáticamente por medio de las herramientas de tiempo de compilación, como AAPT2, o se hereden de las dependencias de la biblioteca de tu app. En la siguiente tabla, se describen las fuentes de los archivos de reglas ProGuard que usa R8.

Fuente Ubicación Descripción
Android Studio <module-dir>/proguard-rules.pro Cuando creas un módulo nuevo con Android Studio, el IDE crea un archivo proguard-rules.pro en el directorio raíz de ese módulo.

De manera predeterminada, este archivo no aplica ninguna regla. Por lo tanto, deberás incluir aquí tus propias reglas ProGuard, como tus reglas de conservación personalizadas.

Complemento Gradle para Android El complemento de Gradle para Android lo genera en el momento de la compilación. El complemento de Gradle para Android genera el archivo proguard-android-optimize.txt, el cual incluye reglas que son útiles para la mayoría de los proyectos de Android y habilita las anotaciones @Keep*.

De forma predeterminada, cuando creas un módulo nuevo con Android Studio, la secuencia de comandos de compilación a nivel del módulo incluye por ti este archivo de reglas en la compilación de lanzamiento.

Nota: El complemento de Gradle para Android incluye otros archivos de reglas ProGuard predefinidos, pero te recomendamos que uses proguard-android-optimize.txt.

Dependencias de bibliotecas

En una biblioteca AAR:
proguard.txt

En una biblioteca JAR:
META-INF/proguard/<ProGuard-rules-file>

Además de estas ubicaciones, el complemento de Android para Gradle 3.6 o versiones posteriores también admite reglas de reducción segmentadas.

Si una biblioteca AAR o JAR se publica con su propio archivo de reglas y, además, se incluye esa biblioteca como una dependencia en tiempo de compilación, R8 aplica automáticamente esas reglas cuando compila tu proyecto.

Además de las reglas convencionales de ProGuard, el complemento de Android para Gradle 3.6 o versiones posteriores también admite reglas de reducción segmentadas. Estas son reglas que se orientan a reductoras específicas (R8 o ProGuard), así como a versiones específicas de reductoras.

El uso de archivos de reglas que se empaquetan con bibliotecas es útil si se requieren ciertas reglas para que la biblioteca funcione correctamente (es decir, si el desarrollador de la biblioteca realizó por ti los pasos para la solución de problemas).

Sin embargo, debes tener en cuenta que, como las reglas son aditivas, ciertas reglas que incluya una dependencia de la biblioteca no se pueden quitar y es posible que afecten la compilación de otras partes de tu app. Por ejemplo, si una biblioteca incluye una regla para inhabilitar las optimizaciones de código, esa regla inhabilitará las optimizaciones de todo tu proyecto.

Android Asset Package Tool 2 (AAPT2) Después de compilar tu proyecto con minifyEnabled true: <module-dir>/build/intermediates/aapt_proguard_file/.../aapt_rules.txt AAPT2 genera reglas de conservación que se basan en las referencias a las clases del manifiesto de tu app, a los diseños y a otros recursos de app. Por ejemplo, AAPT2 incluye una regla de conservación para cada Actividad que registras como punto de entrada en el manifiesto de tu app.
Archivos de configuración personalizados De manera predeterminada, cuando creas un nuevo módulo con Android Studio, el IDE crea el archivo <module-dir>/proguard-rules.pro para que agregues tus propias reglas. Puedes incluir otras opciones de configuración y R8 las aplicará en el momento de la compilación.

Cuando configuras la propiedad minifyEnabled en true, R8 combina reglas de todas las fuentes disponibles que se mencionaron anteriormente. Es importante que lo recuerdes cuando soluciones problemas con R8, ya que otras dependencias en tiempo de compilación (como las dependencias de la biblioteca) podrían introducir cambios en el comportamiento de R8 que desconoces.

Para generar un informe completo sobre todas las reglas que R8 aplica cuando compila tu proyecto, incluye lo siguiente en el archivo proguard-rules.pro de tu módulo:

// You can specify any path and filename.
-printconfiguration ~/tmp/full-r8-config.txt

Reglas de reducción segmentada

El complemento de Android para Gradle 3.6 o versiones posteriores admite reglas de bibliotecas que se orientan a reductoras específicas (R8 o ProGuard), así como versiones específicas de reductoras. Esto permite que los desarrolladores de bibliotecas adapten sus reglas para que funcionen de manera óptima en proyectos que usan versiones nuevas de reductor, a la vez que permite que las reglas existentes se sigan usando en proyectos con versiones anteriores de reductor.

Para especificar reglas de reducción segmentadas, los desarrolladores de bibliotecas deberán incluirlas en ubicaciones específicas dentro de una biblioteca AAR o JAR, como se describe a continuación.

In an AAR library:
    proguard.txt (legacy location)
    classes.jar
    └── META-INF
        └── com.android.tools (targeted shrink rules location)
            ├── r8-from-<X>-upto-<Y>/<R8-rules-file>
            └── proguard-from-<X>-upto-<Y>/<ProGuard-rules-file>

In a JAR library:
    META-INF
    ├── proguard/<ProGuard-rules-file> (legacy location)
    └── com.android.tools (targeted shrink rules location)
        ├── r8-from-<X>-upto-<Y>/<R8-rules-file>
        └── proguard-from-<X>-upto-<Y>/<ProGuard-rules-file>

Eso significa que las reglas de reducción segmentadas se almacenan en el directorio META-INF/com.android.tools de un archivo JAR o en el directorio META-INF/com.android.tools dentro de classes.jar de un AAR.

En ese directorio, puede haber varios directorios con nombres en forma de r8-from-<X>-upto-<Y> o proguard-from-<X>-upto-<Y> para indicar para qué versiones de qué reductor se escribieron las reglas dentro de los directorios. Ten en cuenta que las partes -from-<X> y -upto-<Y> son opcionales, la versión <Y> es exclusiva y los rangos de versiones deben ser continuos.

Por ejemplo, r8-upto-8.0.0, r8-from-8.0.0-upto-8.2.0 y r8-from-8.2.0 forman un conjunto válido de reglas de reducción segmentadas. R8 usará las reglas del directorio r8-from-8.0.0-upto-8.2.0 desde la versión 8.0.0 hasta la versión 8.2.0, sin incluirla.

Con esa información, el complemento de Android para Gradle 3.6 o versiones posteriores seleccionará las reglas de los directorios de R8 coincidentes. Si una biblioteca no especifica reglas de reducción segmentadas, el complemento de Android para Gradle seleccionará las reglas de las ubicaciones heredadas (proguard.txt para un AAR o META-INF/proguard/<ProGuard-rules-file> para un JAR).

Los desarrolladores de bibliotecas pueden elegir incluir reglas de reducción segmentadas o reglas de ProGuard heredadas en sus bibliotecas, o ambos tipos si desean mantener la compatibilidad con el complemento de Android para Gradle anterior a la versión 3.6 o con otras herramientas.

Cómo incluir opciones de configuración adicionales

Cuando creas un nuevo proyecto o módulo con Android Studio, el IDE crea un archivo <module-dir>/proguard-rules.pro para que incluyas tus propias reglas. También puedes incluir reglas adicionales de otros archivos agregándolas a la propiedad proguardFiles en la secuencia de comandos de compilación de tu módulo.

Por ejemplo, puedes agregar reglas específicas de cada variante de compilación incluyendo otra propiedad proguardFiles en el bloque productFlavor correspondiente. En el siguiente archivo de Gradle, se agrega flavor2-rules.pro a la variante de producto flavor2. Ahora, flavor2 usa las tres reglas de ProGuard, debido a que también se aplican las reglas del bloque release.

Además, puedes agregar la propiedad testProguardFiles, que especifica una lista de archivos de ProGuard que solo se incluyen en el APK de prueba:

Kotlin

android {
    ...
    buildTypes {
        getByName("release") {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                // List additional ProGuard rules for the given build type here. By default,
                // Android Studio creates and includes an empty rules file for you (located
                // at the root directory of each module).
                "proguard-rules.pro"
            )
            testProguardFiles(
                // The proguard files listed here are included in the
                // test APK only.
                "test-proguard-rules.pro"
            )
        }
    }
    flavorDimensions.add("version")
    productFlavors {
        create("flavor1") {
            ...
        }
        create("flavor2") {
            proguardFile("flavor2-rules.pro")
        }
    }
}

Groovy

android {
    ...
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles
                getDefaultProguardFile('proguard-android-optimize.txt'),
                // List additional ProGuard rules for the given build type here. By default,
                // Android Studio creates and includes an empty rules file for you (located
                // at the root directory of each module).
                'proguard-rules.pro'
            testProguardFiles
                // The proguard files listed here are included in the
                // test APK only.
                'test-proguard-rules.pro'
        }
    }
    flavorDimensions "version"
    productFlavors {
        flavor1 {
            ...
        }
        flavor2 {
            proguardFile 'flavor2-rules.pro'
        }
    }
}

Cómo reducir tu código

La reducción de código con R8 se habilita de forma predeterminada cuando configuras la propiedad minifyEnabled en true.

La reducción de código (también conocida como eliminación de código muerto) es el proceso de eliminación de código que R8 determina que no es necesario en el tiempo de ejecución. Este proceso puede reducir en gran medida el tamaño de tu app si, por ejemplo, esta incluye muchas dependencias de biblioteca, pero usa solo una pequeña parte de su funcionalidad.

Para reducir el código de tu app, R8 primero determina todos los puntos de entrada al código de tu app en función de la combinación del conjunto de archivos de configuración. Estos puntos de entrada incluyen todas las clases que la plataforma de Android puede usar para abrir las actividades o los servicios de tu app. R8 inspecciona el código de tu app a partir de cada punto de entrada a fin de compilar un gráfico de todos los métodos, todas las variables de miembro y otras clases a las que tu app podría acceder en el tiempo de ejecución. El código que no está conectado a ese gráfico se considera inaccesible y puedes quitarlo de tu app.

En la figura 1, se muestra una app con una dependencia de biblioteca en tiempo de ejecución. Cuando inspecciona el código de la app, R8 determina que es posible acceder a los métodos foo(), faz() y bar() desde el punto de entrada MainActivity.class. Sin embargo, tu app nunca usa la clase OkayApi.class o su método baz() en el tiempo de ejecución; por lo tanto, R8 quitará ese código cuando reduzca tu app.

Figura 1: En el tiempo de compilación, R8 compila un gráfico basado en las reglas de conservación combinadas de tu proyecto para determinar el código inaccesible.

R8 determina los puntos de entrada por medio de reglas -keep en los archivos de configuración de R8 del proyecto. Es decir, las reglas de conservación especifican las clases que R8 no debe descartar cuando reduce tu app, y R8 las considera como posibles puntos de entrada a tu app. El complemento de Gradle para Android y AAPT2 generan automáticamente las reglas de conservación que requiere la mayoría de los proyectos de apps, como las actividades, las vistas y los servicios de tu app. Sin embargo, si necesitas personalizar este comportamiento predeterminado con reglas de conservación adicionales, lee la sección para determinar de forma personalizada el código que se conservará.

En cambio, si solo quieres reducir el tamaño de los recursos de tu app, ve a la sección para reducir tus recursos.

Ten en cuenta que, si se reduce un proyecto de biblioteca, una app que depende de esa biblioteca incluye clases de biblioteca reducidas. Es posible que debas ajustar las reglas de conservación de bibliotecas si faltan clases en el APK de la biblioteca. Si compilas y publicas una biblioteca en formato AAR, los archivos JAR locales de los que depende tu biblioteca no se reducen en el archivo AAR.

Cómo personalizar el código que se conservará

En la mayoría de las situaciones, el archivo predeterminado de reglas ProGuard (proguard-android-optimize.txt) es suficiente para que R8 quite únicamente el código que no se usa. Sin embargo, R8 tiene dificultades para analizar algunas situaciones y es posible que quite código que tu app sí necesita. Los siguientes son ejemplos de situaciones en las cuales este podría quitar código de forma incorrecta:

  • Cuando tu app llama a un método desde la Interfaz nativa Java (JNI)
  • Cuando tu app busca código en el tiempo de ejecución (como con la reflexión)

Cuando pruebes tu app, deberías ver los errores que se produjeron por la eliminación incorrecta de código, pero también puedes generar un informe sobre el código que se quitó para consultar esa información.

Para corregir los errores y forzar a R8 a que conserve determinado código, agrega una línea -keep en el archivo de reglas ProGuard. Por ejemplo:

-keep public class MyClass

También puedes agregar la anotación @Keep al código que desees conservar. Si se agrega una anotación @Keep a una clase, se mantendrá toda la clase tal como esté. Si se agrega a un método o campo, se mantendrán intactos tanto el campo o método (y su nombre) como el nombre de la clase. Ten en cuenta que esta anotación solo está disponible cuando se usa la Biblioteca de anotaciones de AndroidX y además incluyes el archivo de reglas ProGuard que se empaqueta con el complemento de Gradle para Android, como se describe en la sección sobre cómo habilitar la reducción.

Existen muchas consideraciones que debes tener en cuenta cuando uses la opción -keep. A fin de obtener más información para personalizar tu archivo de reglas, lee el Manual de ProGuard. En la sección Solución de problemas, se describen otros problemas comunes que podrías encontrar cuando reduzcas tu código.

Cómo extraer bibliotecas nativas

De forma predeterminada, las bibliotecas de código nativo se extraen en las compilaciones de actualización de tu app. Esta extracción consiste en quitar la tabla de símbolos y la información de depuración de las bibliotecas nativas que usa tu app. La extracción de bibliotecas de código nativo genera un ahorro significativo de tamaño. Sin embargo, es imposible diagnosticar fallas en Google Play Console debido a la falta de información (como los nombres de clases y funciones).

Asistencia para fallas por error en código nativo

Google Play Console informa las fallas por error en código nativo, en Android vitals. Con solo unos pasos, puedes generar y subir un archivo nativo de símbolos de depuración para tu app. Este archivo habilita seguimientos de pila de fallas simbólicas por error en código nativo (que incluyen nombres de clases y funciones), en Android vitals para que te ayuden a depurar tu app en producción. Estos pasos varían según la versión del complemento de Android para Gradle que uses en tu proyecto y el resultado de la compilación de este.

Versión del complemento de Gradle para Android: 4.1 o posterior

Si tu proyecto involucra la compilación de un Android App Bundle, puedes incluir el archivo nativo de símbolos de depuración automáticamente. Para incluir este archivo en compilaciones de actualización, agrega lo siguiente al archivo build.gradle.kts de tu app:

android.buildTypes.release.ndk.debugSymbolLevel = { SYMBOL_TABLE | FULL }

Selecciona uno de los siguientes niveles de símbolos de depuración:

  • Usa el elemento SYMBOL_TABLE para obtener los nombres de las funciones en los seguimientos de pila simbólicos de Play Console. Este nivel es compatible con tombstones.
  • Usa el objeto FULL para obtener los nombres de funciones, los archivos y los números de línea en los seguimientos de pila simbólicos de Play Console.

Si tu proyecto involucra la creación de un APK, usa la configuración de compilación build.gradle.kts anterior para generar el archivo nativo de símbolos de depuración por separado. De forma manual, sube el archivo nativo de símbolos de depuración a Google Play Console. Como parte del proceso de compilación, el complemento de Gradle para Android genera este archivo en la siguiente ubicación del proyecto:

app/build/outputs/native-debug-symbols/variant-name/native-debug-symbols.zip

Complemento de Android para Gradle versión 4.0 o anterior (y otros sistemas de compilación)

Como parte del proceso de compilación, el complemento de Android para Gradle conserva una copia de las bibliotecas sin extraer en un directorio de proyecto. Esta estructura de directorios es similar a la siguiente:

app/build/intermediates/cmake/universal/release/obj/
├── armeabi-v7a/
│   ├── libgameengine.so
│   ├── libothercode.so
│   └── libvideocodec.so
├── arm64-v8a/
│   ├── libgameengine.so
│   ├── libothercode.so
│   └── libvideocodec.so
├── x86/
│   ├── libgameengine.so
│   ├── libothercode.so
│   └── libvideocodec.so
└── x86_64/
    ├── libgameengine.so
    ├── libothercode.so
    └── libvideocodec.so
  1. Comprime el contenido de este directorio:

    cd app/build/intermediates/cmake/universal/release/obj
    zip -r symbols.zip .
    
  2. De forma manual, sube el archivo symbols.zip a Google Play Console.

Cómo reducir tus recursos

La reducción de recursos funciona únicamente junto con la reducción de código. Una vez que el reductor de código quita todo el código que no se usa, el reductor de recursos puede identificar los recursos que la app todavía usa, en especial cuando agregas bibliotecas de código que los incluyen. En este caso, deberás quitar el código de biblioteca que no se usa, de modo que deje de hacerse referencia a los recursos de biblioteca y el reductor de recursos pueda quitarlos.

Para habilitar la reducción de recursos, establece la propiedad shrinkResources como true en la secuencia de comandos de compilación (junto con minifyEnabled para la reducción de código). Por ejemplo:

Kotlin

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

Groovy

android {
    ...
    buildTypes {
        release {
            shrinkResources true
            minifyEnabled true
            proguardFiles
                getDefaultProguardFile('proguard-android.txt'),
                'proguard-rules.pro'
        }
    }
}

Si todavía no compilaste tu app con minifyEnabled para la reducción del código, prueba lo anterior antes de habilitar shrinkResources, ya que probablemente debas modificar tu archivo proguard-rules.pro de modo que mantenga las clases o los métodos que creaste o invocaste de manera dinámica antes de comenzar a quitar recursos.

Cómo personalizar los recursos que se conservarán

Si deseas conservar o descartar algún recurso específico, crea un archivo XML en tu proyecto con una etiqueta <resources> y especifica los recursos que desees conservar en el atributo tools:keep y los que desees descartar en el atributo tools:discard. Ambos atributos aceptan una lista de nombres de recursos separados por comas. Puedes usar el carácter de asterisco como comodín.

Por ejemplo:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
    tools:discard="@layout/unused2" />

Guarda este archivo en los recursos de tu proyecto; por ejemplo, en res/raw/my.package.keep.xml. Durante la compilación, este archivo no se empaquetará en tu app.

Nota: Asegúrate de usar un nombre único para el archivo keep. Cuando se vinculan diferentes bibliotecas, sus reglas de retención entrarían en conflicto, lo que podría causar problemas con las reglas omitidas o los recursos mantenidos innecesarios.

Especificar los recursos que deben descartarse quizá te parezca absurdo, ya que puedes simplemente borrarlos, pero puede resultar útil cuando usas variantes de compilación. Por ejemplo, puedes colocar todos tus recursos en el directorio del proyecto común y, luego, crear un archivo my.package.build.variant.keep.xml diferente para cada variante de compilación cuando sepas que un recurso determinado parece usarse en el código (y, por lo tanto, el reductor no lo quita), pero sabes que, en realidad, no se necesitará para la variante de compilación determinada. También es posible que las herramientas de compilación hayan identificado incorrectamente un recurso como necesario, lo que es posible porque el compilador agrega los IDs de recursos intercalados y, luego, es posible que el analizador de recursos no conozca la diferencia entre un recurso al que se hace referencia de forma genuina y un valor entero en el código que tiene el mismo valor.

Cómo habilitar comprobaciones de referencia estrictas

Por lo general, el reductor de recursos puede determinar de manera precisa si un recurso se usa o no. Sin embargo, si tu código llama a la clase  Resources.getIdentifier(), o si alguna de tus bibliotecas lo hace (esto sucede en el caso de la biblioteca AppCompat), significa que busca nombres de recursos basados en strings generadas de manera dinámica. Cuando lo haces, el reductor de recursos adopta un comportamiento defensivo de forma predeterminada y marca todos los recursos cuyos formatos de nombre coincidan como que están potencialmente en uso y no pueden quitarse.

El siguiente código, por ejemplo, hace que todos los recursos con el prefijo img_ se marquen como usados.

Kotlin

val name = String.format("img_%1d", angle + 1)
val res = resources.getIdentifier(name, "drawable", packageName)

Java

String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());

El reductor de recursos también analiza todas las constantes de strings de tu código, así como los diferentes recursos res/raw/, y busca las URL de los recursos en un formato similar a file:///android_res/drawable//ic_plus_anim_016.png. Si encuentra strings como esta u otras que, al parecer, podrían usarse para construir URL como esta, no las quita.

Estos son ejemplos del modo de reducción seguro que se habilita de forma predeterminada. Sin embargo, puedes descartar esta estrategia "más vale prevenir que lamentar" y especificar que el reductor de recursos únicamente conserve los recursos que esté seguro de que se usan. Para hacerlo, fija shrinkMode en strict, en el archivo keep.xml, como se indica a continuación:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:shrinkMode="strict" />

Si habilitas el modo de reducción estricta y tu código también hace referencia a recursos con strings generadas de manera dinámica, como se observa más arriba, debes conservar manualmente esos recursos usando el atributo tools:keep.

Cómo quitar recursos alternativos que no se usan

El reductor de recursos de Gradle solo quita recursos a los que no se hace referencia en el código de tu app, lo que significa que no se quitarán los recursos alternativos para las diferentes opciones de configuración del dispositivo. De ser necesario, puedes usar la propiedad resConfigs del complemento de Android para Gradle a fin de quitar archivos de recursos alternativos que tu app no necesite.

Por ejemplo, si usas una biblioteca que incluye recursos de idioma (como AppCompat o los Servicios de Google Play), tu app incluirá todas las strings de idiomas traducidos para los mensajes en esas bibliotecas, sin importar si el resto de tu app está traducida en los mismos idiomas o no. Si solo deseas mantener los idiomas que tu app admite oficialmente, puedes especificarlos a través de la propiedad resConfig. Se quitarán los recursos para idiomas que no se especifiquen.

En el siguiente fragmento de código, se muestra cómo limitar tus recursos de idioma a inglés y francés únicamente:

Kotlin

android {
    defaultConfig {
        ...
        resourceConfigurations.addAll(listOf("en", "fr"))
    }
}

Groovy

android {
    defaultConfig {
        ...
        resConfigs "en", "fr"
    }
}

Cuando lanzas una app con el formato Android App Bundle, se descargan de forma predeterminada solo los idiomas configurados en el dispositivo de un usuario cuando se instala la app. Del mismo modo, solo se incluyen en la descarga los recursos que coincidan con la densidad de la pantalla del dispositivo y las bibliotecas nativas que coincidan con la ABI del dispositivo. Para obtener más información, consulta la configuración de Android App Bundle.

En el caso de las apps heredadas que se lanzan con APK (que se crearon antes de agosto de 2021), puedes personalizar los recursos de ABI o las densidades de pantalla que quieras incluir en tu APK mediante la compilación de varios APK, cada uno orientado a una configuración del dispositivo diferente.

Cómo fusionar recursos duplicados

De forma predeterminada, Gradle también fusiona los recursos con nombres idénticos, como los elementos de diseño con el mismo nombre que puedan ubicarse en diferentes carpetas de recursos. Este comportamiento no es controlado por la propiedad shrinkResources y no puede inhabilitarse, ya que es necesario evitar errores cuando varios recursos coinciden con el nombre que tu código busca.

La fusión de recursos se produce cuando dos o más archivos comparten un nombre, un tipo y un calificador de recurso idénticos. Gradle selecciona el archivo que considera que es la mejor opción entre los duplicados (en función del orden de prioridad descrito debajo) y transmite solo ese recurso al AAPT para su distribución en el artefacto definitivo.

Gradle busca recursos duplicados en las siguientes ubicaciones:

  • Los recursos principales, asociados con el conjunto de fuentes principales, que generalmente se ubican en src/main/res/
  • Las superposiciones de variantes, del tipo de compilación y de las variantes de compilación
  • Las dependencias del proyecto de biblioteca

Gradle fusiona recursos en el siguiente orden de prioridad escalonado:

Dependencias → Principal → Variante de compilación → Tipo de compilación

Por ejemplo, si un recurso duplicado aparece tanto en tus recursos principales como en una variante de compilación, Gradle seleccionará el que se encuentre en la variante de compilación.

Si aparecen recursos idénticos en el mismo conjunto de orígenes, Gradle no podrá fusionarlos y emitirá un error de fusión de recursos. Esto puede suceder si defines varios conjuntos de orígenes en la propiedad sourceSet de tu archivo build.gradle.kts; por ejemplo, si tanto src/main/res/ como src/main/res2/ contienen recursos idénticos.

Cómo ofuscar tu código

El propósito de la ofuscación es reducir el tamaño de tu app acortando los nombres de las clases, de los métodos y de los campos de tu app. A continuación, se muestra un ejemplo de ofuscación mediante R8:

androidx.appcompat.app.ActionBarDrawerToggle$DelegateProvider -> a.a.a.b:
androidx.appcompat.app.AlertController -> androidx.appcompat.app.AlertController:
    android.content.Context mContext -> a
    int mListItemLayout -> O
    int mViewSpacingRight -> l
    android.widget.Button mButtonNeutral -> w
    int mMultiChoiceItemLayout -> M
    boolean mShowTitle -> P
    int mViewSpacingLeft -> j
    int mButtonPanelSideLayout -> K

Si bien la ofuscación no quita el código de tu app, podrás reducir significativamente su tamaño en apps con archivos DEX que indexan varias clases, métodos y campos. Sin embargo, como la ofuscación renombra diferentes partes de tu código, algunas tareas (como la inspección de seguimientos de pila) requieren herramientas adicionales. Para comprender el seguimiento de pila luego de la ofuscación, lee la sección para decodificar un seguimiento de pila ofuscado.

De manera adicional, si tu código se basa en una asignación de nombres predecible para los métodos y las clases de tu app, cuando uses la reflexión, por ejemplo, deberías tratar esas firmas como puntos de entrada y especificar reglas de conservación para ellas, como se describe en la sección sobre cómo personalizar el código que se conservará. Estas reglas de conservación le indican a R8 que no solo debe conservar ese código en el DEX final de tu app sino también mantener el nombre original.

Cómo decodificar un seguimiento de pila oculto

Una vez que R8 ofusca tu código, podría resultar difícil (si no imposible) comprender el seguimiento de pila porque es probable que se hayan cambiado los nombres de las clases y de los métodos. Para obtener el seguimiento de pila original, debes volver a rastrearlo.

Optimización de código

Para optimizar aún más tu app, R8 inspeccionará tu código en mayor profundidad a fin de quitar el código que no se usa o, cuando sea posible, volverá a escribirlo de manera que no sea tan detallado. A continuación, se muestran algunos ejemplos de tales optimizaciones:

  • Si tu código nunca toma la rama else {} para una declaración "if/else", es posible que R8 quite el código para la rama else {}.
  • Si tu código llama a un método en solo algunos lugares, es posible que R8 quite el método y lo intercale en los pocos sitios de llamada.
  • Si R8 determina que una clase tiene una sola subclase única, y esta no se divide en instancias (por ejemplo, una clase de implementación concreta que solo usa una clase básica abstracta), entonces R8 puede combinar ambas clases y quitar una de la app.
  • Para obtener más información, lee la entrada de blog sobre la optimización de R8, de Jake Wharton.

R8 no te permite inhabilitar ni habilitar optimizaciones discretas, ni tampoco modificar el comportamiento de una optimización. De hecho, ignora cualquier regla ProGuard que intente modificar optimizaciones predeterminadas, como -optimizations y -optimizationpasses. Esta restricción es importante porque R8 continúa mejorando y mantener un comportamiento estándar para las optimizaciones ayuda al equipo de Android Studio a solucionar fácilmente cualquier problema que puedas experimentar.

Ten en cuenta que habilitar la optimización cambiará los seguimientos de pila de tu aplicación. Por ejemplo, el intercalado quitará los marcos de pila. Consulta la sección sobre rastreo para obtener información sobre cómo obtener los seguimientos de pila originales.

Impacto en el rendimiento del tiempo de ejecución

Si la reducción, la ofuscación y la optimización están habilitadas, R8 mejorará el rendimiento del entorno de ejecución del código (incluidos el inicio y el tiempo de fotogramas en el subproceso de la IU) hasta en un 30%. Inhabilitar cualquiera de estos elementos limita de forma drástica el conjunto de optimizaciones que usa R8.

Si R8 está habilitado, también debes crear perfiles de inicio para mejorar aún más el rendimiento del inicio.

Habilita las optimizaciones mejoradas

R8 incluye un conjunto de optimizaciones adicionales (denominadas "modo completo") que hacen que se comporte de manera diferente a ProGuard. Estas optimizaciones están habilitadas de forma predeterminada desde la versión 8.0.0 del complemento de Android para Gradle.

Para inhabilitarlas, incluye lo siguiente en el archivo gradle.properties de tu proyecto:

android.enableR8.fullMode=false

Como las optimizaciones adicionales hacen que R8 se comporte de manera diferente a ProGuard, es posible que estas requieran que incluyas reglas ProGuard adicionales para evitar problemas en el tiempo de ejecución si usas reglas diseñadas para ProGuard. Por ejemplo, supongamos que tu código hace referencia a una clase mediante la API de reflexión de Java. Cuando no usas el "modo completo", R8 supone que quieres examinar y manipular los objetos de esa clase en el tiempo de ejecución (incluso si tu código no lo hace) y conserva automáticamente la clase y su inicializador estático.

Sin embargo, cuando se usa el "modo completo", R8 no hace esta suposición y, si R8 confirma que, de otro modo, tu código nunca usa la clase en el tiempo de ejecución, quita la clase del DEX final de tu app. Es decir, si deseas conservar la clase y su inicializador estático, debes incluir una regla de conservación en tu archivo de reglas para hacerlo.

Si tienes algún problema al usar el "modo completo" de R8, consulta la Página de preguntas frecuentes de R8 para obtener una posible solución. Si no puedes resolver el problema, informa el error.

Rastreo de seguimientos de pila

El código que procesa R8 se modifica de varias maneras, lo que puede dificultar la comprensión de los seguimientos de pila, ya que no coincidirán de forma exacta con el código fuente. Este puede ser el caso de los cambios en los números de línea cuando no se conserva la información de depuración. Es posible que se deba a optimizaciones, como el intercalado y el esquema. El mayor colaborador es la ofuscación, en la que incluso las clases y los métodos cambiarán de nombre.

Para recuperar el seguimiento de pila original, R8 proporciona la herramienta de línea de comandos retrace, que se incluye con el paquete de herramientas de línea de comandos.

Para admitir el rastreo de los seguimientos de pila de la aplicación, debes asegurarte de que la compilación retenga información suficiente a fin de que se realice el rastreo agregando las siguientes reglas al archivo proguard-rules.pro de tu módulo:

-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile

El atributo LineNumberTable retiene información posicional en métodos, de modo que esas posiciones se imprimen en seguimientos de pila. El atributo SourceFile garantiza que todos los entornos de ejecución potenciales impriman la información posicional. La directiva -renamesourcefileattribute establece el nombre del archivo de origen en los seguimientos de pila en solo SourceFile. No es necesario el nombre verdadero del archivo de origen original cuando se hace un rastreo, ya que el archivo de asignación contiene el archivo de origen original.

R8 crea un archivo mapping.txt cada vez que se ejecuta, en el que se incluye la información necesaria para volver a asignar los seguimientos de pila a los seguimientos de pila originales. Android Studio guarda el archivo en el directorio <module-name>/build/outputs/mapping/<build-type>/.

Cuando publicas tu app en Google Play, puedes subir el archivo mapping.txt para cada versión de la app. Cuando se publica con Android App Bundle, este archivo se incluye automáticamente como parte del contenido del paquete de aplicación. Luego, Google Play rastreará los seguimientos de pila entrantes según los problemas informados por los usuarios para que puedas revisarlos en Play Console. A fin de obtener más información, consulta el artículo del Centro de ayuda para desofuscar los seguimientos de pila de falla.

Cómo solucionar problemas con R8

En esta sección, se describen algunas estrategias para solucionar problemas cuando habilitas la reducción, la ofuscación y la optimización con R8. Si no encuentras una solución para tu problema a continuación, también puedes leer la Página de preguntas frecuentes de R8 y la Guía de solución de problemas de ProGuard.

Cómo generar un informe sobre el código quitado (o conservado)

A fin de ayudarte a resolver determinados problemas de R8, podría resultarte útil ver un informe sobre todos los códigos que R8 quitó de tu app. En cada módulo para el que quieras generar este informe, agrega -printusage <output-dir>/usage.txt en tu archivo de reglas personalizado. Cuando habilites R8 y compiles tu app, R8 mostrará un informe con la ruta de acceso y el nombre de archivo que especificaste. El informe del código que se quitó es similar al siguiente:

androidx.drawerlayout.R$attr
androidx.vectordrawable.R
androidx.appcompat.app.AppCompatDelegateImpl
    public void setSupportActionBar(androidx.appcompat.widget.Toolbar)
    public boolean hasWindowFeature(int)
    public void setHandleNativeActionModesEnabled(boolean)
    android.view.ViewGroup getSubDecor()
    public void setLocalNightMode(int)
    final androidx.appcompat.app.AppCompatDelegateImpl$AutoNightModeManager getAutoNightModeManager()
    public final androidx.appcompat.app.ActionBarDrawerToggle$Delegate getDrawerToggleDelegate()
    private static final boolean DEBUG
    private static final java.lang.String KEY_LOCAL_NIGHT_MODE
    static final java.lang.String EXCEPTION_HANDLER_MESSAGE_SUFFIX
...

En cambio, si quieres ver un informe sobre los puntos de entrada que R8 determina desde las reglas de conservación de tu proyecto, incluye -printseeds <output-dir>/seeds.txt en tu archivo de reglas personalizado. Cuando habilites R8 y compiles tu app, R8 mostrará un informe con la ruta de acceso y el nombre de archivo que especificaste. El informe sobre los puntos de entrada conservados es similar al siguiente:

com.example.myapplication.MainActivity
androidx.appcompat.R$layout: int abc_action_menu_item_layout
androidx.appcompat.R$attr: int activityChooserViewStyle
androidx.appcompat.R$styleable: int MenuItem_android_id
androidx.appcompat.R$styleable: int[] CoordinatorLayout_Layout
androidx.lifecycle.FullLifecycleObserverAdapter
...

Cómo solucionar problemas de reducción de recursos

Cuando reduces recursos, en la ventana Build , se muestra un resumen de los recursos que se quitarán de la app (primero debes hacer clic en Toggle view  en el lado izquierdo de la ventana para mostrar la salida de texto detallada de Gradle). Por ejemplo:

:android:shrinkDebugResources
Removed unused resources: Resource data reduced from 2570KB to 1711KB: Removed 33%
:android:validateDebugSigning

Gradle crea, además, un archivo de diagnóstico llamado resources.txt en <module-name>/build/outputs/mapping/release/ (la misma carpeta de los archivos de salida de ProGuard). Este archivo incluye detalles como qué recursos hacen referencia a otros recursos y cuáles de estos se usan o se quitan.

Por ejemplo, para determinar la razón por la que @drawable/ic_plus_anim_016 aún está en tu app, abre el archivo resources.txt y busca ese nombre de archivo. Es posible que descubras que se hace referencia a este desde otro recurso, como se muestra a continuación:

16:25:48.005 [QUIET] [system.out] @drawable/add_schedule_fab_icon_anim : reachable=true
16:25:48.009 [QUIET] [system.out]     @drawable/ic_plus_anim_016

Luego, debes saber por qué se puede acceder a @drawable/add_schedule_fab_icon_anim y, si buscas de manera ascendente, encontrarás que el recurso se enumera en "The root reachable resources are:". Por ello, existe una referencia de código a add_schedule_fab_icon_anim (es decir, se encontró el ID de R.drawable en el código alcanzable).

Si no usas una comprobación estricta, los ID de recursos podrían marcarse como accesibles si hay constantes de strings que pareciera que se usan para crear los nombres de los recursos cargados de forma dinámica. En ese caso, si buscas la salida de la compilación del nombre de recurso, es posible que encuentres un mensaje como el siguiente:

10:32:50.590 [QUIET] [system.out] Marking drawable:ic_plus_anim_016:2130837506
    used because it format-string matches string pool constant ic_plus_anim_%1$d.

Si ves una de estas strings y estás seguro de que no se usa para subir el recurso especificado de manera dinámica, puedes usar el atributo tools:discard a fin de notificar al sistema de compilación que lo quite, como se describe en la sección sobre cómo personalizar los recursos que se conservarán.