Usar o R8 no modo completo

O R8 oferece dois modos: de compatibilidade e completo. O modo completo oferece otimizações eficientes que melhoram o desempenho do app.

Este guia é destinado a desenvolvedores Android que querem usar as otimizações mais eficientes do R8. Ele explora as principais diferenças entre o modo de compatibilidade e o modo completo e fornece as configurações explícitas necessárias para migrar seu projeto com segurança e evitar falhas comuns de tempo de execução.

Ativar o modo completo

Para ativar o modo completo, remova a seguinte linha do arquivo gradle.properties:

android.enableR8.fullMode=false // Remove this line to enable full mode

Manter classes associadas a atributos

Os atributos são metadados armazenados em arquivos de classe compilados que não fazem parte do código executável. No entanto, eles podem ser necessários para determinados tipos de reflexão. Exemplos comuns incluem Signature (que preserva informações de tipo genérico após a exclusão de tipo), InnerClasses e EnclosingMethod (para refletir sobre a estrutura de classe) e anotações visíveis em tempo de execução.

O código a seguir mostra como um atributo Signature aparece para um campo em bytecode. Para um campo:

List<User> users;

O arquivo de classe compilado teria o seguinte bytecode:

.field public static final users:Ljava/util/List;
    .annotation system Ldalvik/annotation/Signature;
        value = {
            "Ljava/util/List<",
            "Lcom/example/package/User;",
            ">;"
        }
    .end annotation
.end field

Bibliotecas que usam muito a reflexão (como Gson) geralmente dependem desses atributos para inspecionar e entender dinamicamente a estrutura do seu código. Por padrão, no modo completo do R8, os atributos só são mantidos se a classe, o campo ou o método associado for mantido explicitamente.

O exemplo a seguir demonstra por que os atributos são necessários e quais regras de preservação você precisa adicionar ao migrar do modo de compatibilidade para o modo completo. Além de manter as classes, os campos ou os métodos que estão sendo refletidos, também é necessário manter explicitamente os atributos em que eles se baseiam.

Considere o exemplo a seguir, em que desserializamos uma lista de usuários usando a biblioteca Gson.


import com.google.gson.Gson
import com.google.gson.reflect.TypeToken

data class User(
    @SerializedName("username")
    var username: String? = null,
    @SerializedName("age")
    var age: Int = 0
)

fun GsonRemoteJsonListExample() {
    val gson = Gson()

    // 1. The JSON string for a list of users returned from remote
    val jsonOutput = """[{"username":"alice","age":30}, {"username":"bob","age":25}]"""

    // 2. Deserialize the JSON string into a List<User>
    // We must use TypeToken for generic types like List
    val listType = object : TypeToken<List<User>>() {}.type
    val deserializedList: List<User> = gson.fromJson(jsonOutput, listType)

    // Print the list
    println("First user from list: ${deserializedList}")
}

Durante a compilação, a eliminação de tipos do Java remove argumentos de tipo genérico. Isso significa que, durante a execução, List<String> e List<User> aparecem como um List bruto. Portanto, bibliotecas como o Gson, que dependem de reflexão, não podem determinar os tipos de objetos específicos que o List foi declarado para conter ao desserializar uma lista JSON, o que pode levar a problemas de tempo de execução.

Para preservar informações de tipo, o Gson usa TypeToken. O encapsulamento TypeToken mantém as informações de desserialização necessárias.

A expressão Kotlin object:TypeToken<List<User>>() {}.type cria uma classe interna anônima que estende TypeToken e captura as informações de tipo genérico. Neste exemplo, a classe anônima é chamada de $GsonRemoteJsonListExample$listType$1.

A linguagem de programação Java salva a assinatura genérica de uma superclasse como metadados, conhecida como atributo Signature, no arquivo de classe compilado. Em seguida, o TypeToken usa esses metadados Signature para recuperar o tipo durante a execução. Isso permite que o Gson use a reflexão para ler o Signature e descobrir o tipo List<User> completo necessário para a desserialização.

Quando o R8 está ativado no modo de compatibilidade, ele retém o atributo Signature para classes, incluindo classes internas anônimas como $GsonRemoteJsonListExample$listType$1, mesmo que regras de preservação específicas não sejam definidas explicitamente. Como resultado, o modo de compatibilidade do R8 não exige mais regras de manutenção explícitas para que este exemplo funcione como esperado.

// keep rule for compatibility mode
-keepattributes Signature

Quando o R8 está ativado no modo completo, o atributo Signature da classe interna anônima $GsonRemoteJsonListExample$listType$1 é removido. Sem essas informações de tipo no Signature, o Gson não consegue encontrar o tipo de aplicativo correto, o que resulta em um IllegalStateException.

Se você estiver usando uma versão do Gson anterior à 2.11.0, as regras de manutenção necessárias para evitar isso são:

// keep rule required for full mode
-keepattributes Signature
-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken { *; }
-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken
  • -keepattributes Signature: essa regra instrui o R8 a manter o atributo que o Gson precisa ler. No modo completo, o R8 retém apenas o atributo Signature para classes, campos ou métodos que são correspondidos explicitamente por uma regra keep.

  • -keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken: essa regra é necessária porque TypeToken encapsula o tipo do objeto que está sendo desserializado. Após a eliminação de tipo, uma classe interna anônima é criada para reter as informações de tipo genérico. Sem manter explicitamente com.google.gson.reflect.TypeToken, o R8 no modo completo não incluirá esse tipo de classe no atributo Signature necessário para a desserialização.

  • -keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken: essa regra retém as informações de tipo de classes anônimas que estendem TypeToken, como $GsonRemoteJsonListExample$listType$1 neste exemplo. Sem essa regra, o R8 no modo completo remove as informações de tipo necessárias, fazendo com que a desserialização falhe.

É importante entender que as regras compartilhadas anteriormente resolvem apenas o problema de descobrir o tipo genérico (por exemplo, List<User>). O R8 também renomeia os campos das classes. Se você não usar anotações @SerializedName nos modelos de dados, o Gson não vai conseguir desserializar o JSON porque os nomes dos campos não vão mais corresponder às chaves JSON.

No entanto, se você estiver usando uma versão do Gson anterior à 2.11 ou se os modelos não usarem a anotação @SerializedName, adicione regras de manutenção explícitas para esses modelos.

Manter o construtor padrão

No modo completo do R8, o construtor sem argumentos/padrão não é mantido implicitamente, mesmo quando a própria classe é mantida. Se você estiver criando uma instância de uma classe usando class.getDeclaredConstructor().newInstance() ou class.newInstance(), mantenha explicitamente o construtor sem argumentos no modo completo. Em contraste, o modo de compatibilidade sempre retém o construtor sem argumentos.

Considere um exemplo em que uma instância de PrecacheTask é criada usando reflexão para chamar dinamicamente o método run. Embora esse cenário não exija regras adicionais no modo de compatibilidade, no modo completo, o construtor padrão de PrecacheTask seria removido. Portanto, é necessária uma regra de retenção específica.

// In library
interface StartupTask {
    fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
    fun execute(taskClass: Class<out StartupTask>) {
        // The class isn't removed, but its constructor might be.
        val task = taskClass.getDeclaredConstructor().newInstance()
        task.run()
    }
}

// In app
class PreCacheTask : StartupTask {
    override fun run() {
        Log.d("Pre cache task", "Warming up the cache...")
    }
}

fun runTaskRunner() {
    // The library is given a direct reference to the app's task class.
    TaskRunner.execute(PreCacheTask::class.java)
}
# Full mode keep rule
# default constructor needs to be specified

-keep class com.example.fullmoder8.PreCacheTask {
    <init>();
}

A modificação de acesso é ativada por padrão

No modo de compatibilidade, o R8 não altera a visibilidade de métodos e campos em uma classe. No entanto, no modo completo, o R8 melhora a otimização mudando a visibilidade dos seus métodos e campos, por exemplo, de privado para público. Isso permite mais inlining.

Essa otimização pode causar problemas se o código usar reflexão que depende especificamente de membros com visibilidade específica. O R8 não reconhece esse uso indireto, o que pode causar falhas no app. Para evitar isso, adicione regras específicas de -keep para preservar os membros e a visibilidade original deles.

Para mais informações, consulte este exemplo para entender por que não é recomendável acessar membros particulares usando reflexão e as regras de manutenção para reter esses campos/métodos.

Metadados específicos do Kotlin

Ao compilar o código Kotlin, o compilador Kotlin armazena metadados específicos da linguagem (como capacidade de anulação, funções de extensão e assinaturas de corrotinas) em uma anotação @kotlin.Metadata em cada arquivo de classe.

Se o app ou as dependências dele usarem a reflexão do Kotlin (kotlin.reflect), a biblioteca de reflexão vai analisar esses metadados no tempo de execução para inspecionar a estrutura da classe. No modo completo do R8, as anotações são removidas por padrão se não forem mantidas explicitamente. Além disso, se o R8 reduzir ou minimizar suas classes sem preservar e atualizar os metadados, a reflexão do Kotlin vai falhar no tempo de execução, resultando em comportamento imprevisível ou falhas (como KotlinReflectionInternalError).

Para evitar comportamentos imprevisíveis e garantir que as funções de reflexão do Kotlin funcionem corretamente após a minificação, mantenha as anotações visíveis em tempo de execução e preserve explicitamente a classe kotlin.Metadata:

# Preserve runtime-visible annotations required for inspecting metadata
-keepattributes RuntimeVisibleAnnotations

# Keep Kotlin metadata to ensure kotlin.reflect functions correctly
-keep class kotlin.Metadata { *; }