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 hacerlo en archivos de reglas de conservación separados. El complemento de Jetpack Keep para Gradle 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 permanecen 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 ningún archivo de reglas separado para 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 el código de llamada no se usa y R8 lo quita (lo que suele ocurrir en las funciones de biblioteca opcionales), R8 también puede quitar el destino, de modo que las reglas de las funciones no utilizadas no agreguen expansión.

  • Lint encuentra la reflexión por ti. La biblioteca incluye comprobaciones 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 en 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 en su lugar:

// 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 manualmente 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 Plugin that the module applies 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 la reflexión o JNI. La biblioteca proporciona las anotaciones y las comprobaciones 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, así que asegúrate de que los repositorios pluginManagement de tu archivo settings.gradle.kts incluyan google().

  2. En tu archivo build.gradle.kts raíz, declara el complemento junto con 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 usa la 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 Gradle.

Para cada variante de compilación de un módulo de app para Android, el complemento registra una <variant>ExtractKeepRules tarea, como releaseExtractKeepRules, y pasa las reglas generadas a R8 para esa variante. Para 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 configuración adicional.

Encuentra la reflexión con lint

Jetpack Keep incluye las siguientes comprobaciones 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 para 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 qué clase y miembros 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 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 la completes. También puedes agregar la anotación de forma manual.

    Por ejemplo, la siguiente función recibe el nombre de la clase como 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 compila hasta que reemplazas TODO().

  5. Verifica qué reglas de conservación genera el complemento a partir de tus anotaciones antes de quitar las reglas existentes. Para 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 de todo el paquete, como -keep class com.example.** { *; }, asegúrate de que las anotaciones o las otras reglas abarquen toda la reflexión sobre las clases del paquete.
    • Para una regla que conserva clases o miembros específicos, asegúrate de que coincida con una de las reglas generadas o que ninguna otra reflexión dependa de ella.
  7. Prueba tu compilación de lanzamiento.

Anota tu código

Agrega una anotación de Jetpack Keep 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 hace tu código:

Anotación Uso 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 código nativo a través de JNI. Usa esta anotación solo cuando no puedas usar las @UsesReflection* anotaciones, que generan reglas condicionales.

Todas las anotaciones @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 la clase más específica. Las reglas generadas conservan 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 conservar una sola sobrecarga. Si no estableces parameterTypes o parameterTypeNames, las reglas conservan todos los constructores o todas las sobrecargas del método. Para conservar solo el constructor o el método sin argumentos, pasa un array vacío.
  • Repite la anotación para varios destinos. Si una función refleja 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 instancias de 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 en 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 en 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)
    }
}

Conserva el código llamado desde fuera de tu app

Usa @UnconditionallyKeep para el código que se llama desde fuera del código Kotlin o Java de tu app, como un método 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 su clase contenedora, 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, anótalas también o agrega reglas de conservación para ellas. 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 llamador externo no depende del nombre, establece 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. Para un módulo de app, el complemento pasa las reglas directamente a R8. Para 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 del consumidor, 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 para 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 afectan las reglas generadas a 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 cualquier regla de conservación del consumidor que ya contenga el AAR. El complemento registra una tarea <variant>KeepRulesTransformAar para cada variante de compilación para hacerlo.

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 que puedas 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 nombraste la tarea keepRulesJar, 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

Para 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 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 segmentadas 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 puede enviar una biblioteca principal y un complemento de video. El complemento depende de la biblioteca principal, por lo que la biblioteca principal no puede hacer referencia directamente a las clases del complemento. En su lugar, la biblioteca principal busca la clase del complemento por nombre en el tiempo 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, R8 puede quitar 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 en 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 está en una biblioteca diferente de la búsqueda que la necesita, por lo que las dos pueden dejar de estar sincronizadas. Con Jetpack Keep, anotas la búsqueda en la biblioteca principal en su lugar:

// 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 está en initialize, que llama a Class.forName, no en la clase del complemento.
  • Elige la anotación según lo que hace 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 puedes 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 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 permanece junto a la búsqueda. La biblioteca principal posee la búsqueda y 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 usa. 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 conserva en cada app que lo incluye.
  • Lint detecta búsquedas sin anotaciones. Si la biblioteca principal agrega más tarde otra búsqueda reflexiva sin una anotación, lint la marca, por lo que puedes 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 es allí donde una regla faltante interrumpiría las estadísticas de video.

Recursos adicionales