Optimización para autores de bibliotecas

Como autor de una biblioteca, debes asegurarte de que los desarrolladores de apps puedan incorporar fácilmente tu biblioteca a sus apps y, al mismo tiempo, mantener una experiencia del usuario final de alta calidad. Esto significa que tu biblioteca debe ser compatible con la optimización de Android (R8) sin requerir configuración adicional por parte del desarrollador, o bien debes documentar que la biblioteca podría ser inadecuada para su uso en Android. Es fundamental que las bibliotecas diseñadas para usarse en Android no impidan las optimizaciones importantes de las apps y cumplan con los requisitos de optimización adicionales.

Esta documentación está dirigida a los desarrolladores de bibliotecas publicadas, pero también puede ser útil para los desarrolladores de módulos de bibliotecas internas en una app grande y modularizada.

Si eres desarrollador de apps y quieres obtener información para optimizar tu app para Android, consulta Cómo habilitar la optimización de apps. Para obtener información sobre qué bibliotecas son adecuadas para usar, consulta Elige bibliotecas con prudencia.

Información sobre los tipos de reglas de conservación

Existen dos tipos distintos de reglas de conservación que puedes tener en las bibliotecas:

  • Las reglas de conservación del consumidor deben especificar reglas que conserven lo que refleje la biblioteca. Si una biblioteca usa la reflexión o JNI para llamar a su código, o al código definido por una app cliente, estas reglas deben describir qué código se debe conservar. Las bibliotecas deben empaquetar reglas de conservación del consumidor, que usan el mismo formato que las reglas de conservación de la app. Estas reglas se incluyen en los artefactos de la biblioteca (AAR o JAR) y se consumen automáticamente durante la optimización de la app para Android cuando se usa la biblioteca. Estas reglas se mantienen en el archivo especificado con la propiedad consumerProguardFiles en tu archivo build.gradle.kts (o build.gradle). Para obtener más información, consulta Cómo escribir reglas de conservación del consumidor.
  • Las reglas de conservación de la compilación de la biblioteca se aplican cuando se compila la biblioteca. Solo son necesarios si decides optimizar parcialmente tu biblioteca en el tiempo de compilación. Deben evitar que se quite la API pública de la biblioteca; de lo contrario, la API pública no estará presente en la distribución de la biblioteca, lo que significa que los desarrolladores de apps no podrán usarla. Estas reglas se mantienen en el archivo especificado con la propiedad proguardFiles en tu archivo build.gradle.kts (o build.gradle). Para obtener más información, consulta Cómo optimizar la compilación de la biblioteca de AAR.

Requisitos y lineamientos de optimización

La configuración de R8 en las bibliotecas tiene un impacto global en el rendimiento y el tamaño del archivo binario final de la app que las consume. Además de las prácticas recomendadas para las reglas de conservación generales, los autores de bibliotecas deben cumplir con requisitos específicos y tener en cuenta lineamientos adicionales.

Cumple con los requisitos de optimización

La ineficiencia en las bibliotecas es un factor importante que contribuye al aumento del tamaño de las apps, el desperdicio de memoria, los inicios lentos y los errores de ANR (errores de aplicación no responde). Las bibliotecas deben evitar incumplir los siguientes requisitos para no reducir significativamente la calidad de la app y la experiencia del usuario.

  • No hay reglas de conservación amplias ni a nivel del paquete: Tu biblioteca no debe incluir reglas de conservación amplias que conserven la mayor parte del código de tu biblioteca o de otra biblioteca. Las reglas de conservación amplias pueden resolver fallas a corto plazo, pero aumentan el tamaño de la app de todas las apps que consumen tu biblioteca.

    No incluyas reglas de conservación para todo el paquete (como -keep class com.mylibrary.** {*; }) para los paquetes de tu biblioteca o de otras bibliotecas a las que se haga referencia. Estas reglas limitan la optimización de estos paquetes en todas las apps que consumen tu biblioteca.

  • No hay reglas globales inapropiadas: Nunca uses opciones globales como -dontobfuscate o -allowaccessmodification.

  • Usa la generación de código en lugar de la reflexión siempre que sea posible: Cuando sea posible, usa la generación de código (codegen) en lugar de la reflexión. La generación de código y la reflexión son enfoques comunes para evitar el código estándar cuando se programa, pero la generación de código es más compatible con un optimizador de apps como R8.

    Con codegen, el código se analiza y modifica durante el proceso de compilación. Dado que no hay modificaciones importantes después del tiempo de compilación, el optimizador sabe qué código se necesita en última instancia y qué se puede quitar de forma segura.

    Con la reflexión, el código se analiza y manipula en el tiempo de ejecución. Como el código no se finaliza hasta que se ejecuta, el optimizador no sabe qué código se puede quitar de forma segura. Es probable que quite el código que se usa de forma dinámica a través de la reflexión durante el tiempo de ejecución, lo que provoca fallas en la app para los usuarios.

    Muchas bibliotecas modernas usan codegen en lugar de reflexión. Consulta KSP para ver un punto de entrada común que usan Room, Dagger2 y muchos otros.

  • Compatibilidad con el modo completo de R8: Tu biblioteca no debería fallar cuando se habilita el modo completo de R8. El modo completo de R8 es el modo recomendado para usar R8 y es el predeterminado desde AGP 8.0, que se lanzó como estable en 2023. Si tu biblioteca falla en R8, la solución es identificar el punto de entrada específico de reflexión o JNI y agregar una regla segmentada, no conservar todo el paquete.

Recomendaciones adicionales

Además de los requisitos de optimización, a continuación se incluyen recomendaciones adicionales.

  • No uses -repackageclasses en el archivo de reglas de conservación del consumidor de tu biblioteca. Sin embargo, para optimizar la compilación de tu biblioteca, puedes usar -repackageclasses con un nombre de paquete interno, como <your.library.package>.internal, en el archivo de reglas de conservación de la compilación de tu biblioteca. Esto puede mejorar la eficiencia de tu biblioteca en apps no optimizadas. Sin embargo, por lo general, no es necesario, ya que las apps también deben optimizarse.
  • Declara los atributos que necesites para que tu biblioteca funcione en los archivos de reglas de conservación de la biblioteca, incluso si puede haber una superposición con los atributos definidos en proguard-android-optimize.txt.
  • Si necesitas los siguientes atributos en la distribución de tu biblioteca, mantenlos en el archivo de reglas de conservación de la compilación de la biblioteca y no en el archivo de reglas de conservación del consumidor de la biblioteca:
    • AnnotationDefault
    • EnclosingMethod
    • Exceptions
    • InnerClasses
    • RuntimeInvisibleAnnotations
    • RuntimeInvisibleParameterAnnotations
    • RuntimeInvisibleTypeAnnotations
    • RuntimeVisibleAnnotations
    • RuntimeVisibleParameterAnnotations
    • RuntimeVisibleTypeAnnotations
    • Signature
  • Los autores de bibliotecas deben conservar el atributo RuntimeVisibleAnnotations en sus reglas de conservación del consumidor si se usan anotaciones en el tiempo de ejecución.
  • Los autores de bibliotecas no deben usar las siguientes opciones globales en sus reglas de conservación del consumidor:
    • -include
    • -basedirectory
    • -injars
    • -outjars
    • -libraryjars
    • -repackageclasses
    • -flattenpackagehierarchy
    • -allowaccessmodification
    • -overloadaggressively
    • -renamesourcefileattribute
    • -ignorewarnings
    • -addconfigurationdebugging
    • -printconfiguration
    • -printmapping
    • -printusage
    • -printseeds
    • -applymapping
    • -obfuscationdictionary
    • -classobfuscationdictionary
    • -packageobfuscationdictionary

Cuándo es aceptable la reflexión

Si debes usar la reflexión, solo debes reflejarte en uno de los siguientes elementos:

  • Tipos específicos segmentados (implementadores o subclases de interfaces específicos)
  • Código que usa una anotación de tiempo de ejecución específica

Usar la reflexión de esta manera limita el costo del tiempo de ejecución y permite escribir reglas de conservación de consumidores segmentadas.

Esta forma específica y segmentada de reflexión es un patrón que puedes ver en el framework de Android (por ejemplo, cuando se inflan actividades, vistas y elementos de diseño) y en las bibliotecas de AndroidX (por ejemplo, cuando se construyen WorkManager ListenableWorkers o RoomDatabases). Por el contrario, la reflexión abierta de Gson no es adecuada para su uso en apps para Android.

Conceptos erróneos comunes

Algunas ideas erróneas comunes pueden llevarte a configurar R8 de forma incorrecta. Se incluyen los siguientes:

  • Comprensión incorrecta de las optimizaciones de R8: A diferencia de lo que se cree, las optimizaciones de R8 no se limitan solo a la ofuscación, sino que también incluyen la reducción de código y las optimizaciones lógicas con técnicas de intercalación de métodos y combinación de clases. Para obtener más información, consulta la descripción general de la optimización de R8.

  • Cómo omitir la optimización de bibliotecas ofuscadas: Un error común es omitir una biblioteca de la optimización porque se optimizó o se ofuscó cuando se compiló en un AAR (archivo de Android) o JAR (archivo de Java). Las optimizaciones durante el tiempo de compilación de la biblioteca son limitadas, y tu app no debe inhabilitar la optimización de la biblioteca incluyéndola en una regla de conservación. Para obtener más información, consulta Cómo optimizar la compilación de la biblioteca de AAR.

  • Comprensión incorrecta de la opción -keep La regla -keep impide que R8 ejecute cualquiera de sus etapas de optimización. Para obtener más información, consulta Cómo elegir la opción de conservación adecuada.

Configura el empaquetado de reglas

Para garantizar que tus reglas de conservación del consumidor se apliquen correctamente, debes empaquetarlas de forma adecuada según el formato de tu biblioteca.

Bibliotecas AAR

Para agregar reglas de consumidor para una biblioteca AAR, usa la opción consumerProguardFiles en la secuencia de comandos de compilación del módulo de biblioteca de Android. Para obtener más información, consulta nuestra guía para crear módulos de biblioteca.

Kotlin

android {
    defaultConfig {
        consumerProguardFiles("consumer-proguard-rules.pro")
    }
    ...
}

Groovy

android {
    defaultConfig {
        consumerProguardFiles 'consumer-proguard-rules.pro'
    }
    ...
}

Bibliotecas JAR

Para incluir reglas en tu biblioteca de Kotlin o Java que se distribuye como un archivo JAR, coloca el archivo de reglas en el directorio META-INF/proguard/ del archivo JAR final, con cualquier nombre de archivo. Por ejemplo, si tu código está en <libraryroot>/src/main/kotlin, coloca un archivo de reglas del consumidor en <libraryroot>/src/main/resources/META-INF/proguard/consumer-proguard-rules.pro y las reglas se incluirán en la ubicación correcta de tu archivo JAR de salida.

Verifica que las reglas del paquete JAR final se agrupen correctamente. Para ello, comprueba que las reglas estén en el directorio META-INF/proguard.

Optimiza la compilación de la biblioteca AAR (avanzado)

En general, no es necesario optimizar la compilación de una biblioteca directamente, ya que las posibles optimizaciones en el tiempo de compilación de la biblioteca son muy limitadas. Como desarrollador de bibliotecas, debes analizar varias etapas de optimización y mantener el comportamiento, tanto en el tiempo de compilación de la biblioteca como de la app, antes de optimizar esa biblioteca.

Si aún quieres optimizar tu biblioteca en el momento de la compilación, el complemento de Android para Gradle admite esta opción.

Kotlin

android {
    buildTypes {
        release {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
        configureEach {
            consumerProguardFiles("consumer-rules.pro")
        }
    }
}

Groovy

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles
                getDefaultProguardFile('proguard-android-optimize.txt'),
                'proguard-rules.pro'
        }
        configureEach {
            consumerProguardFiles "consumer-rules.pro"
        }
    }
}

Ten en cuenta que el comportamiento de proguardFiles es muy diferente del de consumerProguardFiles:

  • proguardFiles se usan en el momento de la compilación, a menudo junto con getDefaultProguardFile("proguard-android-optimize.txt"), para definir qué parte de tu biblioteca se debe conservar durante la compilación de la biblioteca. Como mínimo, esta es tu API pública.
  • En cambio, los consumerProguardFiles se empaquetan en la biblioteca para afectar las optimizaciones que se realizan más adelante, durante la compilación de una app que consume tu biblioteca.

Por ejemplo, si tu biblioteca usa la reflexión para construir clases internas, es posible que debas definir las reglas de conservación tanto en proguardFiles como en consumerProguardFiles.

Si usas -repackageclasses en la compilación de tu biblioteca, vuelve a empaquetar las clases en un subpaquete dentro del paquete de tu biblioteca. Por ejemplo, usa -repackageclasses 'com.example.mylibrary.internal' en lugar de -repackageclasses 'internal'.

Compatibilidad con diferentes versiones de R8 (avanzado)

Puedes adaptar las reglas para segmentar versiones específicas de R8. Esto permite que tu biblioteca funcione de manera óptima en proyectos que usan versiones más recientes de R8, a la vez que permite que se sigan usando las reglas existentes en proyectos con versiones anteriores de R8.

Para especificar reglas de R8 segmentadas, debes incluirlas en el directorio META-INF/com.android.tools dentro de classes.jar de un AAR o en el directorio META-INF/com.android.tools de un JAR.

In an AAR library:
    proguard.txt (legacy location, the file name must be "proguard.txt")
    classes.jar
    └── META-INF
        └── com.android.tools (location of targeted R8 rules)
            ├── r8-from-<X>-upto-<Y>/<R8-rule-files>
            └── ... (more directories with the same name format)

In a JAR library:
    META-INF
    ├── proguard/<ProGuard-rule-files> (legacy location)
    └── com.android.tools (location of targeted R8 rules)
        ├── r8-from-<X>-upto-<Y>/<R8-rule-files>
        └── ... (more directories with the same name format)

En el directorio META-INF/com.android.tools, puede haber varios subdirectorios con nombres en formato r8-from-<X>-upto-<Y> para indicar para qué versiones de R8 se escribieron las reglas. Cada subdirectorio puede tener uno o más archivos que contengan las reglas de R8, con cualquier nombre de archivo y extensión.

Ten en cuenta que las partes -from-<X> y -upto-<Y> son opcionales, la versión <Y> es exclusiva y los rangos de versiones suelen ser continuos, pero también pueden superponerse.

Por ejemplo, r8, r8-upto-8.0.0, r8-from-8.0.0-upto-8.2.0 y r8-from-8.2.0 son nombres de directorios que representan un conjunto de reglas de R8 segmentadas. Cualquier versión de R8 puede usar las reglas del directorio r8. R8 puede 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 incluir esta última.

El complemento de Gradle para Android usa esa información para seleccionar todas las reglas que puede usar la versión actual de R8. Si una biblioteca no especifica reglas de R8 segmentadas, el complemento de Android Gradle seleccionará las reglas de las ubicaciones heredadas (proguard.txt para un AAR o META-INF/proguard/<ProGuard-rule-files> para un JAR).