Gerar regras de manutenção com base em anotações

O Jetpack Keep permite declarar o uso de reflexão e da Java Native Interface (JNI) com anotações diretamente no código-fonte Kotlin ou Java, em vez de em arquivos de regras de manutenção separados. O plug-in do Gradle Jetpack Keep lê essas anotações no momento do build e gera as regras de manutenção necessárias para o R8.

O Jetpack Keep ajuda a manter a eficácia da otimização do R8 conforme sua base de código muda:

  • As regras de manutenção ficam ao lado do código que precisa delas. Ao renomear ou excluir um código que usa reflexão, você atualiza a anotação no mesmo lugar. Não há um arquivo de regras separado para manter em sincronia.

  • As regras geradas são condicionais. O R8 mantém o destino de uma chamada reflexiva apenas se também mantiver o código que faz a chamada. Se o código de chamada não for usado e o R8 o remover (geralmente o caso em recursos de biblioteca opcionais), o R8 também poderá remover o destino, para que as regras de recursos não utilizados não adicionem excesso de código.

  • O Lint encontra a reflexão para você. A biblioteca inclui verificações de lint que sinalizam chamadas reflexivas que não estão anotadas e que podem adicionar muitas das anotações automaticamente.

Como o Jetpack Keep funciona

Considere uma função que instancia uma classe por nome. Se o R8 não conseguir identificar que seu código usa a classe, ele vai remover o construtor da classe, e o app vai falhar no momento da execução. Para evitar a falha, adicione uma regra como esta ao arquivo de regras de manutenção:

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

Com o Jetpack Keep, você anota a função que executa a reflexão:

// 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
}

No momento do build, o plug-in do Gradle gera uma regra de manutenção para o construtor. Ao contrário da regra escrita manualmente, a regra gerada é condicional: o R8 mantém o construtor apenas se loadResourceLocator permanecer no app otimizado.

Requisitos

Para usar o Jetpack Keep, seu projeto precisa do Plug-in do Android para Gradle (AGP) 8.7 ou mais recente.

O plug-in do Gradle é compatível com os seguintes tipos de módulo:

Tipo de módulo Plug-in que o módulo aplica Configuração complementar
App Android com.android.application Nenhum
Biblioteca do Android com.android.library Nenhum
Biblioteca Kotlin Multiplatform com um destino do Android com.android.kotlin.multiplatform.library Nenhum
Biblioteca JVM (Java ou Kotlin) java-library ou org.jetbrains.kotlin.jvm Registrar uma transformação JAR

Configurar o Jetpack Keep

Para usar o Jetpack Keep, adicione a biblioteca e o plug-in do Gradle a cada módulo que usa reflexão ou JNI. A biblioteca fornece as anotações e as verificações de lint, e o plug-in transforma as anotações em regras de manutenção.

  1. No arquivo libs.versions.toml, adicione a biblioteca e o plug-in. A biblioteca e o plug-in compartilham a mesma versão:

    [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" }
    

    O Gradle faz o download do plug-in no repositório Maven do Google. Portanto, verifique se os repositórios pluginManagement no arquivo settings.gradle.kts incluem google().

  2. No arquivo build.gradle.kts raiz, declare o plug-in com o AGP, mas sem aplicá-lo:

    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. No arquivo build.gradle.kts de cada módulo que usa reflexão ou JNI, aplique o plug-in e adicione a dependência da 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. Sincronize o projeto com arquivos do Gradle.

Para cada variante de build de um módulo de app Android, o plug-in registra uma tarefa <variant>ExtractKeepRules, como releaseExtractKeepRules, e transmite as regras geradas para o R8 dessa variante. Para módulos de biblioteca, o plug-in empacota as regras geradas como regras de manutenção do consumidor na saída da biblioteca. Os módulos de biblioteca da JVM precisam de uma configuração extra.

Encontrar reflexão com o lint

O Jetpack Keep inclui as seguintes verificações de lint:

ReflectionWithoutKeepAnnotations
Sinaliza chamadas reflexivas, como Class.forName, getDeclaredConstructor, getDeclaredMethod e getDeclaredField, que não têm uma anotação de manutenção correspondente.
MissingKeepAnnotationPlugin
Sinaliza módulos que dependem de androidx.annotation.keep:keep, mas não aplicam o plug-in do Gradle androidx.annotation.keep. Sem o plug-in, o build não gera regras de manutenção com base nas anotações.

Para usar o lint para anotar um módulo do Android, siga estas etapas:

  1. Execute o lint no módulo para encontrar chamadas reflexivas que precisam de anotações:

    ./gradlew :app:lint
    
  2. Aplique as correções que o lint pode fazer automaticamente:

    ./gradlew :app:lintFix
    

    O Lint pode adicionar uma anotação automaticamente quando consegue determinar em qual classe e membros o código reflete.

  3. Revise cada anotação adicionada para confirmar se ela corresponde ao que o código faz. Por exemplo, verifique se os tipos de parâmetro correspondem ao construtor ou método que seu código chama.

  4. Resolva as descobertas restantes no Android Studio. Quando o lint não consegue determinar a classe de destino, a correção rápida insere a anotação com um marcador de posição TODO para você preencher. Também é possível adicionar a anotação manualmente.

    Por exemplo, a função a seguir recebe o nome da classe como um parâmetro, de modo que o lint não pode determinar qual classe ela cria. A correção rápida adiciona uma anotação com um marcador de posição:

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

    Substitua o marcador pela classe que seu código cria. Se a classe estiver disponível no tempo de compilação, use classConstant em vez de className. No Kotlin, o código não é compilado até que você substitua TODO().

  5. Verifique quais regras de manutenção o plug-in gera com base nas suas anotações antes de remover as regras atuais. Para um módulo de app, execute a tarefa de extração para a variante de lançamento:

    ./gradlew :app:releaseExtractKeepRules
    

    O plug-in grava as regras em app/build/generated/release/androidx.annotation.keep.rules.pro. Para módulos de biblioteca, consulte Verificar as regras geradas.

  6. Remova as regras de manutenção que as novas anotações substituem dos arquivos de regras de manutenção. Uma regra também pode abranger reflexões que o lint não sinalizou, como reflexões em outro módulo ou em uma biblioteca. Portanto, verifique cada regra antes de removê-la:

    • Para uma regra que abrange todo o pacote, como -keep class com.example.** { *; }, verifique se as anotações ou outras regras cobrem todas as reflexões nas classes do pacote.
    • Para uma regra que mantém classes ou membros específicos, verifique se ela corresponde a uma das regras geradas ou se nenhuma outra reflexão depende dela.
  7. Teste o build de lançamento.

Adicionar anotações ao código

Adicione uma anotação Keep do Jetpack ao código que executa a reflexão: a função, o construtor, o campo ou a classe que contém a chamada reflexiva. A anotação descreve o destino da reflexão, e a regra gerada mantém esse destino somente se o R8 mantiver o código anotado.

Escolha uma anotação com base no que seu código faz:

Nota Use quando o código
@UsesReflectionToConstruct Chama um construtor usando reflexão, por exemplo, com getDeclaredConstructor().newInstance().
@UsesReflectionToAccessMethod Chama um método por reflexão, por exemplo, com getMethod ou getDeclaredMethod.
@UsesReflectionToAccessField Lê ou grava um campo por reflexão, por exemplo, com getField ou getDeclaredField.
@UnconditionallyKeep É chamado de fora do código Kotlin ou Java do app, por exemplo, do código nativo pela JNI. Use essa anotação apenas quando não for possível usar as anotações @UsesReflection*, que geram regras condicionais.

Todas as anotações @UsesReflection* seguem estas convenções:

  • Identifique a classe de destino com classConstant ou className. Se a classe estiver disponível no tempo de compilação, use classConstant. O compilador verifica a referência, e as refatorações do ambiente de desenvolvimento integrado, como a renomeação, a atualizam. Use className somente quando a classe não estiver disponível no tempo de compilação, como uma classe em uma dependência opcional. Esses dois parâmetros são mutuamente exclusivos.
  • Segmente a classe mais específica. As regras geradas mantêm os membros correspondentes na classe especificada e nas subclasses ou implementações de interface dela. Quanto mais geral for a classe, mais código o R8 vai manter.
  • Especifique os tipos de parâmetro para manter uma única sobrecarga. Se você não definir parameterTypes ou parameterTypeNames, as regras vão manter todos os construtores ou todas as sobrecargas do método. Para manter apenas o construtor ou método sem argumentos, transmita uma matriz vazia.
  • Repita a anotação para vários destinos. Se uma função refletir em mais de uma classe ou membro, adicione uma anotação para cada destino.

Criar objetos com reflexão

Use @UsesReflectionToConstruct quando o código instanciar uma classe por reflexão. No exemplo a seguir, um executor de tarefas instancia implementações de uma interface StartupTask por nome. Como a anotação especifica a interface, as regras geradas mantêm o construtor sem argumentos de cada implementação de StartupTask, desde que o R8 mantenha 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()
    }
}

Chamar métodos com reflexão

Use @UsesReflectionToAccessMethod quando o código chamar um método por reflexão. Defina methodName como o nome do método. No exemplo a seguir, o código chama um método estático em uma dependência opcional. A classe não está disponível no tempo de compilação, então a anotação usa className em vez 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)
    }
}

Acessar campos com reflexão

Use @UsesReflectionToAccessField quando o código ler ou gravar um campo por reflexão. Defina fieldName como o nome do 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)
    }
}

Manter o código chamado de fora do app

Use @UnconditionallyKeep para códigos chamados de fora do código Kotlin ou Java do app, como um método que o código nativo chama pela JNI. Anote o elemento chamado, não o código que o chama:

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()
}

A regra gerada mantém o método anotado e a classe que o contém, com os nomes originais. Ele não mantém outras classes que a assinatura do método referencia. Se o código nativo depender dessas classes, adicione anotações a elas também ou adicione regras de manutenção para elas. Para mais informações sobre JNI e R8, consulte Interação com código nativo usando JNI.

Por padrão, @UnconditionallyKeep preserva o nome do elemento. Se o autor da chamada externa não depender do nome, defina shouldPreserveName = false para que o R8 possa renomear o elemento.

Verificar as regras geradas

Nesta seção, explicamos como verificar as regras geradas para cada tipo de módulo. Para um módulo de app, o plug-in transmite as regras diretamente para o R8. Para um módulo de biblioteca, o plug-in empacota as regras como regras de retenção do consumidor na saída da biblioteca. Os apps que dependem da sua biblioteca aplicam essas regras automaticamente quando executam o R8, mesmo que não usem o Jetpack Keep. Para mais informações sobre as regras de manutenção do consumidor, consulte Otimização para autores de bibliotecas.

Apps Android

Para verificar quais regras de manutenção o plug-in gera para um módulo de app, execute a tarefa de extração para uma variante de build:

./gradlew :app:releaseExtractKeepRules

O plug-in grava as regras de cada variante em app/build/generated/<variant>/androidx.annotation.keep.rules.pro. Um comentário # context: antes de cada regra identifica o código anotado que a produziu.

Para verificar como as regras geradas afetam a otimização junto com as outras regras de manutenção, use o Analisador de configuração do R8.

Bibliotecas do Android

Os módulos de biblioteca do Android não precisam de nenhuma configuração adicional. Ao criar o AAR, o plug-in adiciona as regras geradas ao arquivo proguard.txt do AAR, depois de todas as regras de manutenção do consumidor que o AAR já contém. Para isso, o plug-in registra uma tarefa <variant>KeepRulesTransformAar para cada variante de build.

Para verificar as regras, crie o AAR, por exemplo, com ./gradlew :mylibrary:assembleRelease, e abra proguard.txt no AAR gerado. As regras geradas aparecem em uma seção com o seguinte cabeçalho:

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

No macOS e no Linux, também é possível imprimir as regras na linha de comando:

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

Bibliotecas do Kotlin Multiplatform

Os módulos de biblioteca do Kotlin Multiplatform com um destino Android também não precisam de configuração extra. Ao criar o AAR para o destino do Android, o plug-in adiciona as regras geradas ao arquivo proguard.txt do AAR da mesma maneira que para as bibliotecas do Android. O plug-in registra uma tarefa androidMainKeepRulesTransformAar para fazer isso.

Para verificar as regras, execute essa tarefa e abra proguard.txt no AAR que ela criar. No macOS e no Linux, é possível imprimir as regras na linha de comando:

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

Bibliotecas da JVM

Antes de verificar as regras de um módulo de biblioteca da JVM, configure o módulo para que o plug-in adicione as regras geradas a uma cópia do JAR. Em seguida, execute a tarefa de transformação que você registrou e verifique os arquivos de regras no JAR que ela cria. Por exemplo, se você nomeou a tarefa como keepRulesJar, é possível imprimir as regras no macOS e no Linux com os seguintes comandos:

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

Configurar bibliotecas da JVM

Para módulos de biblioteca da JVM que produzem um JAR, o plug-in não modifica o JAR automaticamente. Em vez disso, use a função annotationKeep.registerJavaArchiveTransform para registrar uma tarefa que cria uma cópia do JAR com as regras geradas e, em seguida, publique esse JAR. Adicione o seguinte ao arquivo build.gradle.kts do módulo da 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)
}

A transformação adiciona as regras geradas ao local das regras do R8 de destino do JAR, META-INF/com.android.tools/r8/proguard.txt. Se o JAR já tiver regras em META-INF/com.android.tools/ ou META-INF/proguard/, a transformação vai anexar as regras geradas ao arquivo de regras atual. Para mais informações sobre esses locais, consulte Suporte a diferentes versões do R8.

Depois de registrar a transformação, verifique as regras no JAR.

Exemplo: carregar uma biblioteca de complementos opcional

As bibliotecas costumam usar reflexão para carregar complementos opcionais. Por exemplo, um SDK do Analytics pode enviar uma biblioteca principal e um complemento de vídeo. O complemento depende da biblioteca principal, então ela não pode se referir diretamente às classes do complemento. Em vez disso, a biblioteca principal procura a classe do complemento pelo nome no momento da execução e ativa a análise de vídeo somente se encontrar a classe.

Como a biblioteca principal usa a classe do complemento apenas por reflexão, o R8 pode remover a classe ou o construtor dela de um build de lançamento do app, o que faz com que a pesquisa falhe. A falha ocorre apenas em builds de lançamento de apps que incluem o complemento, por isso é difícil de detectar nos testes.

Sem o Jetpack Keep, o complemento envia uma regra de retenção do consumidor para a classe, conforme mostrado em Reflexão para oferecer suporte a dependências opcionais:

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

Essa regra está em uma biblioteca diferente da pesquisa que precisa dela, então as duas podem ficar dessincronizadas. Com o Jetpack Keep, você anota a pesquisa na 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()
    }
}

A anotação segue estas diretrizes:

  • Anote o código que executa a reflexão. A anotação está em initialize, que chama Class.forName, não na classe do complemento.
  • Escolha a anotação com base no que o código faz. A pesquisa cria uma instância da classe, então o código usa @UsesReflectionToConstruct.
  • Use className para uma classe que não pode ser referenciada. A classe do complemento não está no caminho de classe de compilação da biblioteca principal, então a anotação não pode usar classConstant.
  • Mantenha apenas o construtor que o código chama. A pesquisa chama o construtor sem argumentos, então parameterTypes é uma matriz vazia.

Quando você cria o AAR da biblioteca principal, o plug-in adiciona uma regra que mantém VideoEventTracker e o construtor sem argumentos sempre que o R8 mantém initialize. Os apps que usam o SDK aplicam essa regra automaticamente, mesmo que não usem o Jetpack Keep. Portanto, o complemento não precisa de uma regra própria. Em comparação com a regra do complemento, essa abordagem tem as seguintes vantagens:

  • A regra fica ao lado da consulta. A biblioteca principal é proprietária da pesquisa e da regra. Portanto, você as atualiza juntas, em vez de coordenar uma mudança em duas bibliotecas.
  • O R8 ainda pode remover códigos não utilizados. Se um app incluir o complemento, mas nunca chamar initialize, a regra gerada não será aplicada, e o R8 poderá remover VideoEventTracker. A regra do complemento o mantém em todos os apps que o incluem.
  • O lint detecta pesquisas não anotadas. Se a biblioteca principal adicionar outra pesquisa reflexiva sem uma anotação, o lint vai sinalizar isso para que você possa corrigir o problema antes de lançar a biblioteca.

Para confirmar o resultado, verifique se a regra gerada aparece no arquivo proguard.txt do AAR, conforme descrito em Bibliotecas do Android. Em seguida, teste um build de lançamento de um app que inclua o complemento, porque é aí que uma regra ausente interromperia a análise de dados de vídeo.

Limitações

O Jetpack Keep ainda não detecta reflexões que encontram classes e membros pelas anotações deles nem gera regras de manutenção para isso. Esse recurso está planejado.

Até lá, use as regras de manutenção para esse tipo de reflexão. Por exemplo, a regra abaixo mantém todos os membros anotados com @SomeAnnotation nas classes que o R8 mantém:

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

Se o Jetpack Keep não funcionar para seu caso de uso ou se você tiver outro feedback, registre um problema.

Outros recursos