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.
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
pluginManagementno arquivosettings.gradle.ktsincluemgoogle().No arquivo
build.gradle.ktsraiz, 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 }No arquivo
build.gradle.ktsde 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) }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,getDeclaredMethodegetDeclaredField, 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 Gradleandroidx.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:
Execute o lint no módulo para encontrar chamadas reflexivas que precisam de anotações:
./gradlew :app:lintAplique as correções que o lint pode fazer automaticamente:
./gradlew :app:lintFixO Lint pode adicionar uma anotação automaticamente quando consegue determinar em qual classe e membros o código reflete.
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.
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
TODOpara 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
classConstantem vez declassName. No Kotlin, o código não é compilado até que você substituaTODO().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:releaseExtractKeepRulesO 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.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.
- Para uma regra que abrange todo o pacote, como
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
classConstantouclassName. Se a classe estiver disponível no tempo de compilação, useclassConstant. O compilador verifica a referência, e as refatorações do ambiente de desenvolvimento integrado, como a renomeação, a atualizam. UseclassNamesomente 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
parameterTypesouparameterTypeNames, 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 chamaClass.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
classNamepara 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 usarclassConstant. - 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á removerVideoEventTracker. 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
- Exemplo do Jetpack Keep: um projeto do Android Studio que usa o Jetpack Keep em um app, uma biblioteca do Android, uma biblioteca multiplataforma do Kotlin e uma biblioteca da JVM.
- Casos de uso e exemplos de regras de manutenção: regras de manutenção para cenários comuns de reflexão e JNI.
- Siga as práticas recomendadas: orientações sobre como escrever regras de manutenção com escopo restrito.