Genera reglas de conservación a partir de anotaciones

Jetpack Keep te permite declarar el uso de la reflexión y la interfaz nativa de Java (JNI) con anotaciones directamente en tu código fuente de Kotlin o Java, en lugar de en archivos de reglas de conservación independientes. El complemento de Gradle de Jetpack Keep lee estas anotaciones en el momento de la compilación y genera las reglas de conservación que necesita R8.

Jetpack Keep ayuda a que la optimización de R8 siga siendo eficaz a medida que cambia tu base de código:

  • Las reglas de conservación se mantienen junto al código que las necesita. Cuando cambias el nombre o borras el código que usa la reflexión, actualizas la anotación en el mismo lugar. No hay un archivo de reglas independiente que se deba mantener sincronizado.

  • Las reglas generadas son condicionales. R8 conserva el destino de una llamada reflexiva solo si también conserva el código que realiza la llamada. Si no se usa el código de llamada y R8 lo quita (lo que suele ocurrir en las funciones opcionales de la biblioteca), R8 también puede quitar el destino, de modo que las reglas de las funciones que no se usan no agreguen código innecesario.

  • Lint encuentra la reflexión por ti. La biblioteca incluye verificaciones de lint que marcan las llamadas reflexivas que no están anotadas y que pueden agregar muchas de las anotaciones automáticamente.

Cómo funciona Jetpack Keep

Considera una función que crea una instancia de una clase por nombre. Si R8 no puede determinar que tu código usa la clase, quita el constructor de la clase y la app falla durante el tiempo de ejecución. Para evitar la falla, puedes agregar una regla como la siguiente a tu archivo de reglas de conservación:

# Unconditional: R8 keeps this constructor even if it removes the reflection that invokes it.
-keep class com.example.ResourceLocatorImpl {
    <init>();
}

Con Jetpack Keep, anotas la función que realiza la reflexión:

// ResourceLocatorImpl isn't on this module's compile classpath, so both the
// annotation and the code refer to it by name.
@UsesReflectionToConstruct(
    className = "com.example.ResourceLocatorImpl",
    // Omitting parameterTypes keeps every constructor. An empty array keeps
    // only the no-argument constructor, which the code calls.
    parameterTypes = [],
)
fun loadResourceLocator(): ResourceLocator {
    val implClass = Class.forName("com.example.ResourceLocatorImpl")
    return implClass.getDeclaredConstructor().newInstance() as ResourceLocator
}

En el momento de la compilación, el complemento de Gradle genera una regla de conservación para el constructor. A diferencia de la regla escrita de forma manual anterior, la regla generada es condicional: R8 conserva el constructor solo si loadResourceLocator permanece en la app optimizada.

Requisitos

Para usar Jetpack Keep, tu proyecto necesita el complemento de Android para Gradle (AGP) 8.7 o una versión posterior.

El complemento de Gradle admite los siguientes tipos de módulos:

Module type Complemento que aplica el módulo Configuración adicional
App para Android com.android.application Ninguno
Biblioteca de Android com.android.library Ninguno
Biblioteca de Kotlin Multiplatform con un destino de Android com.android.kotlin.multiplatform.library Ninguno
Biblioteca de JVM (Java o Kotlin) java-library o org.jetbrains.kotlin.jvm Registra una transformación de JAR

Configura Jetpack Keep

Para usar Jetpack Keep, agrega la biblioteca y el complemento de Gradle a cada módulo que use reflexión o JNI. La biblioteca proporciona las anotaciones y las verificaciones de lint, y el complemento convierte las anotaciones en reglas de conservación.

  1. En tu archivo libs.versions.toml, agrega la biblioteca y el complemento. La biblioteca y el complemento comparten la misma versión:

    [versions]
    # The library and the plugin ship together. Use one version for both so
    # that they stay in sync when you upgrade.
    annotationKeep = "1.0.0-alpha01"
    
    [libraries]
    androidx-annotation-keep = { group = "androidx.annotation.keep", name = "keep", version.ref = "annotationKeep" }
    
    [plugins]
    androidx-annotation-keep = { id = "androidx.annotation.keep", version.ref = "annotationKeep" }
    

    Gradle descarga el complemento del repositorio de Maven de Google, por lo que debes asegurarte de que los repositorios pluginManagement de tu archivo settings.gradle.kts incluyan google().

  2. En el archivo build.gradle.kts raíz, declara el complemento junto con el AGP sin aplicarlo:

    plugins {
        alias(libs.plugins.android.application) apply false
        // Other plugins.
    
        // The plugin relies on AGP classes that it doesn't bundle, so declare
        // it next to AGP. Declaring it here also gives every module the same
        // plugin version.
        alias(libs.plugins.androidx.annotation.keep) apply false
    }
    
  3. En el archivo build.gradle.kts de cada módulo que use reflexión o JNI, aplica el complemento y agrega la dependencia de la biblioteca:

    plugins {
        alias(libs.plugins.android.application)
        // The plugin reads annotations only from this module's own code, so
        // apply it in every module that uses Jetpack Keep.
        alias(libs.plugins.androidx.annotation.keep)
    }
    
    dependencies {
        implementation(libs.androidx.annotation.keep)
    }
    
  4. Sincroniza tu proyecto con archivos de Gradle.

Para cada variante de compilación de un módulo de app para Android, el complemento registra una tarea <variant>ExtractKeepRules, como releaseExtractKeepRules, y pasa las reglas generadas a R8 para esa variante. En el caso de los módulos de biblioteca, el complemento empaqueta las reglas generadas como reglas de conservación del consumidor en el resultado de la biblioteca. Los módulos de biblioteca de JVM necesitan una configuración adicional.

Cómo encontrar la reflexión con lint

Jetpack Keep incluye las siguientes verificaciones de lint:

ReflectionWithoutKeepAnnotations
Marca las llamadas reflexivas, como Class.forName, getDeclaredConstructor, getDeclaredMethod y getDeclaredField, que no tienen una anotación de conservación coincidente.
MissingKeepAnnotationPlugin
Marca los módulos que dependen de androidx.annotation.keep:keep, pero no aplican el complemento de Gradle androidx.annotation.keep. Sin el complemento, tu compilación no genera reglas de conservación a partir de las anotaciones.

Para usar lint y anotar un módulo de Android, sigue estos pasos:

  1. Ejecuta lint en el módulo para encontrar llamadas reflexivas que necesiten anotaciones:

    ./gradlew :app:lint
    
  2. Aplica las correcciones que lint puede realizar automáticamente:

    ./gradlew :app:lintFix
    

    Lint puede agregar una anotación automáticamente cuando puede determinar en qué clase y miembros se refleja el código.

  3. Revisa cada anotación agregada para confirmar que coincida con lo que hace tu código. Por ejemplo, verifica que los tipos de parámetros coincidan con el constructor o el método al que llama tu código.

  4. Resuelve los hallazgos restantes en Android Studio. Cuando lint no puede determinar la clase de destino, la corrección rápida inserta la anotación con un marcador de posición TODO para que lo completes. También puedes agregar la anotación de forma manual.

    Por ejemplo, la siguiente función recibe el nombre de la clase como un parámetro, por lo que lint no puede determinar qué clase construye. La corrección rápida agrega una anotación con un marcador de posición:

    @UsesReflectionToConstruct(
        className = TODO(),
        parameterTypes = []
    )
    fun createInstance(className: String): Any =
        Class.forName(className).getDeclaredConstructor().newInstance()
    

    Reemplaza el marcador de posición por la clase que construye tu código. Si la clase está disponible en el tiempo de compilación, usa classConstant en lugar de className. En Kotlin, tu código no se compilará hasta que reemplaces TODO().

  5. Verifica qué reglas de conservación genera el complemento a partir de tus anotaciones antes de quitar las reglas existentes. En el caso de un módulo de app, ejecuta la tarea de extracción para la variante de lanzamiento:

    ./gradlew :app:releaseExtractKeepRules
    

    El complemento escribe las reglas en app/build/generated/release/androidx.annotation.keep.rules.pro. Para los módulos de biblioteca, consulta Verifica las reglas generadas.

  6. Quita las reglas de conservación que reemplazan las anotaciones nuevas de tus archivos de reglas de conservación. Una regla también puede abarcar la reflexión que lint no marcó, como la reflexión en otro módulo o en una biblioteca, por lo que debes verificar cada regla antes de quitarla:

    • Para una regla que abarque todo el paquete, como -keep class com.example.** { *; }, asegúrate de que las anotaciones o las otras reglas cubran toda la reflexión en las clases del paquete.
    • En el caso de una regla que conserva clases o miembros específicos, asegúrate de que coincida con una de las reglas generadas o de que ninguna otra reflexión dependa de ella.
  7. Prueba tu compilación de lanzamiento.

Anota tu código

Agrega una anotación Keep de Jetpack al código que realiza la reflexión: la función, el constructor, el campo o la clase que contiene la llamada reflexiva. La anotación describe el destino de la reflexión, y la regla generada conserva ese destino solo si R8 conserva el código anotado.

Elige una anotación según lo que haga tu código:

Anotación Usa when cuando tu código
@UsesReflectionToConstruct Llama a un constructor a través de la reflexión, por ejemplo, con getDeclaredConstructor().newInstance().
@UsesReflectionToAccessMethod Llama a un método a través de la reflexión, por ejemplo, con getMethod o getDeclaredMethod.
@UsesReflectionToAccessField Lee o escribe un campo a través de la reflexión, por ejemplo, con getField o getDeclaredField.
@UnconditionallyKeep Se llama desde fuera del código Kotlin o Java de tu app, por ejemplo, desde el código nativo a través de JNI. Usa esta anotación solo cuando no puedas usar las anotaciones @UsesReflection*, que generan reglas condicionales.

Todas las anotaciones de @UsesReflection* siguen estas convenciones:

  • Identifica la clase de destino con classConstant o className. Si la clase está disponible en el tiempo de compilación, usa classConstant. El compilador verifica la referencia, y las refactorizaciones del IDE, como el cambio de nombre, la actualizan. Usa className solo cuando la clase no esté disponible en el tiempo de compilación, como una clase en una dependencia opcional. Estos dos parámetros son mutuamente excluyentes.
  • Segmenta los anuncios para la clase más específica. Las reglas generadas mantienen los miembros coincidentes en la clase que especificas y en sus subclases o implementaciones de interfaz. Cuanto más general sea la clase, más código conservará R8.
  • Especifica los tipos de parámetros para mantener una sola sobrecarga. Si no configuras parameterTypes ni parameterTypeNames, las reglas conservarán todos los constructores o todas las sobrecargas del método. Para conservar solo el constructor o método sin argumentos, pasa un array vacío.
  • Repite la anotación para varios destinos. Si una función se refleja en más de una clase o miembro, agrega una anotación para cada destino.

Construye objetos con reflexión

Usa @UsesReflectionToConstruct cuando tu código cree una instancia de una clase a través de la reflexión. En el siguiente ejemplo, un ejecutor de tareas crea una instancia de las implementaciones de una interfaz StartupTask por nombre. Debido a que la anotación especifica la interfaz, las reglas generadas conservan el constructor sin argumentos de cada implementación de StartupTask, siempre que R8 conserve execute:

interface StartupTask {
    fun run()
}

object TaskRunner {
    // The task class is known only at runtime, so target the interface that
    // every task implements.
    @UsesReflectionToConstruct(classConstant = StartupTask::class, parameterTypes = [])
    fun execute(className: String) {
        val task = Class.forName(className)
            .getDeclaredConstructor()
            .newInstance() as StartupTask
        task.run()
    }
}

Llama a métodos con reflexión

Usa @UsesReflectionToAccessMethod cuando tu código llame a un método a través de la reflexión. Establece methodName como el nombre del método. En el siguiente ejemplo, el código llama a un método estático en una dependencia opcional. La clase no está disponible en el tiempo de compilación, por lo que la anotación usa className en lugar de classConstant:

object CrashReporting {
    @UsesReflectionToAccessMethod(
        className = "com.example.crashlib.CrashReporter",
        methodName = "log",
        // Omitting parameterTypes keeps every log overload. Keep only the one
        // that the code calls.
        parameterTypes = [String::class],
    )
    fun log(message: String) {
        val reporterClass = try {
            Class.forName("com.example.crashlib.CrashReporter")
        } catch (e: ClassNotFoundException) {
            // Crash reporting is optional, so skip it instead of failing.
            return
        }
        reporterClass.getMethod("log", String::class.java).invoke(null, message)
    }
}

Accede a campos con reflexión

Usa @UsesReflectionToAccessField cuando tu código lea o escriba un campo a través de la reflexión. Establece fieldName como el nombre del campo:

class UserProfileFetcher {
    // User doesn't expose internalId, so reading it requires reflection.
    @UsesReflectionToAccessField(classConstant = User::class, fieldName = "internalId")
    fun readInternalId(user: User): Any? {
        val field = User::class.java.getDeclaredField("internalId")
        field.isAccessible = true
        return field.get(user)
    }
}

Cómo conservar el código al que se llama desde fuera de tu app

Usa @UnconditionallyKeep para el código al que se llama desde fuera del código Kotlin o Java de tu app, como un método al que llama el código nativo a través de JNI. Anota el elemento al que se llama, no el código que lo llama:

class NativeBridge {
    // Only native code calls this method, through a JNI lookup by name that
    // R8 can't see. Keep the method and its name unconditionally.
    @UnconditionallyKeep
    fun onNativeEvent(message: String) {
        Log.d("NativeBridge", "Received from native code: $message")
    }

    // The default proguard-android-optimize.txt file already keeps native
    // method names, so this method doesn't need an annotation.
    external fun startNativeProcess()
}

La regla generada conserva el método anotado y la clase que lo contiene, con sus nombres originales. No conserva otras clases a las que hace referencia la firma del método. Si el código nativo depende de esas clases, también debes anotarlas o agregarles reglas de conservación. Para obtener más información sobre JNI y R8, consulta Interacción con código nativo a través de JNI.

De forma predeterminada, @UnconditionallyKeep conserva el nombre del elemento. Si el emisor externo no depende del nombre, configura shouldPreserveName = false para que R8 pueda cambiar el nombre del elemento.

Verifica las reglas generadas

En esta sección, se explica cómo verificar las reglas generadas para cada tipo de módulo. En el caso de un módulo de app, el complemento pasa las reglas directamente a R8. En el caso de un módulo de biblioteca, el complemento empaqueta las reglas como reglas de conservación del consumidor en el resultado de la biblioteca. Las apps que dependen de tu biblioteca aplican estas reglas automáticamente cuando ejecutan R8, incluso si no usan Jetpack Keep. Para obtener más información sobre las reglas de conservación de consumidores, consulta Optimización para autores de bibliotecas.

Apps para Android

Para verificar qué reglas de conservación genera el complemento para un módulo de app, ejecuta la tarea de extracción de una variante de compilación:

./gradlew :app:releaseExtractKeepRules

El complemento escribe las reglas para cada variante en app/build/generated/<variant>/androidx.annotation.keep.rules.pro. Un comentario # context: antes de cada regla identifica el código anotado que la produjo.

Para verificar cómo las reglas generadas afectan la optimización junto con tus otras reglas de conservación, usa el analizador de configuración de R8.

Bibliotecas de Android

Los módulos de biblioteca de Android no necesitan ninguna configuración adicional. Cuando compilas el AAR, el complemento agrega las reglas generadas al archivo proguard.txt del AAR, después de las reglas de conservación del consumidor que ya contiene el AAR. Para ello, el complemento registra una tarea <variant>KeepRulesTransformAar para cada variante de compilación.

Para verificar las reglas, compila el AAR, por ejemplo, con ./gradlew :mylibrary:assembleRelease, y abre proguard.txt en el AAR generado. Las reglas generadas aparecen en una sección con el siguiente encabezado:

#################################################################
#   Rules generated from androidx.annotation.keep annotations   #
#################################################################

En macOS y Linux, también puedes imprimir las reglas desde la línea de comandos:

unzip -p mylibrary/build/intermediates/aar/release/releaseKeepRulesTransformAar/mylibrary-release.aar proguard.txt

Bibliotecas de Kotlin Multiplatform

Los módulos de biblioteca de Kotlin Multiplatform con un destino de Android tampoco necesitan ninguna configuración adicional. Cuando compilas el AAR para el destino de Android, el complemento agrega las reglas generadas al archivo proguard.txt del AAR de la misma manera que para las bibliotecas de Android. El complemento registra una tarea androidMainKeepRulesTransformAar para hacerlo.

Para verificar las reglas, ejecuta esa tarea y abre proguard.txt en el AAR que crea. En macOS y Linux, puedes imprimir las reglas desde la línea de comandos:

./gradlew :mylibrary:androidMainKeepRulesTransformAar
unzip -p mylibrary/build/intermediates/aar/androidMain/androidMainKeepRulesTransformAar/mylibrary.aar proguard.txt

Bibliotecas de JVM

Antes de poder verificar las reglas de un módulo de biblioteca de JVM, configura el módulo para que el complemento agregue las reglas generadas a una copia de su JAR. Luego, ejecuta la tarea de transformación que registraste y verifica los archivos de reglas en el JAR que crea. Por ejemplo, si asignaste el nombre keepRulesJar a la tarea, puedes imprimir las reglas en macOS y Linux con los siguientes comandos:

./gradlew :mylibrary:keepRulesJar
unzip -p mylibrary/build/intermediates/annotation-keep/keepRulesJar/mylibrary-keep-rules.jar \
    "META-INF/com.android.tools/*" "META-INF/proguard/*"

Configura bibliotecas de JVM

En el caso de los módulos de biblioteca de JVM que producen un JAR, el complemento no modifica el JAR automáticamente. En su lugar, usa la función annotationKeep.registerJavaArchiveTransform para registrar una tarea que cree una copia del JAR con las reglas generadas y, luego, publica ese JAR. Agrega lo siguiente al archivo build.gradle.kts del módulo de la biblioteca:

plugins {
    id("java-library")
    alias(libs.plugins.jetbrains.kotlin.jvm)
    alias(libs.plugins.androidx.annotation.keep)
}

val jarTask = tasks.named<Jar>("jar")

// Apps run R8 on this library's code, so the generated rules must be inside
// the JAR. The plugin handles this automatically only for Android modules.
val keepRulesJarTask = annotationKeep.registerJavaArchiveTransform(
    taskName = "keepRulesJar",
    input = jarTask.flatMap { it.archiveFile },
    fileName = jarTask.flatMap { it.archiveFileName }.map { it.replace(".jar", "-keep-rules.jar") },
)

// Consumers of this library resolve apiElements and runtimeElements, so add
// the JAR that contains the generated rules to them.
configurations.configureEach {
    if (name == "apiElements" || name == "runtimeElements") {
        outgoing.artifact(keepRulesJarTask.flatMap { it.outputJar })
    }
}

dependencies {
    implementation(libs.androidx.annotation.keep)
}

La transformación agrega las reglas generadas a la ubicación de las reglas de R8 de destino del JAR, META-INF/com.android.tools/r8/proguard.txt. Si el JAR ya contiene reglas en META-INF/com.android.tools/ o META-INF/proguard/, la transformación agrega las reglas generadas al archivo de reglas existente. Para obtener más información sobre estas ubicaciones, consulta Cómo brindar compatibilidad con diferentes versiones de R8.

Después de registrar la transformación, verifica las reglas en el JAR.

Ejemplo: Carga una biblioteca de complementos opcional

Las bibliotecas suelen usar la reflexión para cargar complementos opcionales. Por ejemplo, un SDK de Analytics podría enviar una biblioteca principal y un complemento de video. El complemento depende de la biblioteca principal, por lo que esta no puede hacer referencia a las clases del complemento de forma directa. En su lugar, la biblioteca principal busca el nombre de la clase del complemento en el entorno de ejecución y activa las estadísticas de video solo si encuentra la clase.

Debido a que la biblioteca principal usa la clase del complemento solo a través de la reflexión, es posible que R8 quite la clase o su constructor de la compilación de lanzamiento de una app, lo que hace que falle la búsqueda. La falla solo ocurre en las compilaciones de lanzamiento de las apps que incluyen el complemento, por lo que es difícil detectarla durante las pruebas.

Sin Jetpack Keep, el complemento envía una regla de conservación del consumidor para su clase, como se muestra en Reflexión para admitir dependencias opcionales:

# In the add-on's consumer keep rules file.
-keep class com.example.analytics.video.VideoEventTracker {
    <init>();
}

Esta regla se encuentra en una biblioteca diferente de la búsqueda que la necesita, por lo que ambas pueden desincronizarse. Con Jetpack Keep, anotas la búsqueda en la biblioteca principal:

// In the core library.
interface AnalyticsPlugin {
    fun start()
}

object AnalyticsManager {
    // The annotation and the lookup share this constant so that they can't
    // refer to different classes.
    private const val VIDEO_TRACKER_CLASS =
        "com.example.analytics.video.VideoEventTracker"

    @UsesReflectionToConstruct(
        className = VIDEO_TRACKER_CLASS,
        parameterTypes = [],
    )
    fun initialize() {
        val tracker = try {
            Class.forName(VIDEO_TRACKER_CLASS)
                .getDeclaredConstructor()
                .newInstance() as AnalyticsPlugin
        } catch (e: ClassNotFoundException) {
            // Apps that don't play video leave out the add-on, so a missing
            // class isn't an error.
            return
        }
        // This is an ordinary interface call, not a reflective one, so R8
        // keeps start() without an annotation.
        tracker.start()
    }
}

La anotación sigue estos lineamientos:

  • Anota el código que realiza la reflexión. La anotación se encuentra en initialize, que llama a Class.forName, no en la clase del complemento.
  • Elige la anotación según lo que haga el código. La búsqueda crea una instancia de la clase, por lo que el código usa @UsesReflectionToConstruct.
  • Usa className para una clase a la que no puedas hacer referencia. La clase del complemento no está en la ruta de clase de compilación de la biblioteca principal, por lo que la anotación no puede usar classConstant.
  • Conserva solo el constructor al que llama el código. La búsqueda llama al constructor sin argumentos, por lo que parameterTypes es un array vacío.

Cuando compilas el AAR de la biblioteca principal, el complemento agrega una regla que conserva VideoEventTracker y su constructor sin argumentos cada vez que R8 conserva initialize. Las apps que usan el SDK aplican esta regla automáticamente, incluso si no usan Jetpack Keep, por lo que el complemento no necesita una regla propia. En comparación con la regla del complemento, este enfoque tiene las siguientes ventajas:

  • La regla se mantiene junto a la búsqueda. La biblioteca principal posee tanto la búsqueda como la regla, por lo que las actualizas juntas en lugar de coordinar un cambio en dos bibliotecas.
  • R8 aún puede quitar el código que no se use. Si una app incluye el complemento, pero nunca llama a initialize, la regla generada no se aplica y R8 puede quitar VideoEventTracker. La regla del complemento lo mantiene en todas las apps que lo incluyen.
  • Lint detecta búsquedas sin anotaciones. Si la biblioteca principal agrega más adelante otra búsqueda reflexiva sin una anotación, lint la marca para que puedas corregirla antes de lanzar la biblioteca.

Para confirmar el resultado, verifica que la regla generada aparezca en el archivo proguard.txt del AAR, como se describe en Bibliotecas de Android. Luego, prueba una compilación de lanzamiento de una app que incluya el complemento, ya que allí es donde una regla faltante interrumpiría las estadísticas de video.

Limitaciones

Jetpack Keep aún no detecta la reflexión que encuentra clases y miembros por sus anotaciones ni genera reglas de conservación para ella. Esta función está planificada.

Hasta entonces, usa reglas de conservación para este tipo de reflexión. Por ejemplo, la siguiente regla conserva todos los miembros anotados con @SomeAnnotation en las clases que conserva R8:

-keepclassmembers class * {
    @com.example.annotation.SomeAnnotation <methods>;
    @com.example.annotation.SomeAnnotation <fields>;
}

Si Jetpack Keep no funciona para tu caso de uso o si tienes otros comentarios, envía un informe de error.

Recursos adicionales