Ativar todo o potencial do otimizador R8

O R8 oferece dois modos: de compatibilidade e completo. O modo completo oferece otimizações eficientes que melhoram a performance 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

Reter classes associadas a atributos

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 eliminaçã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.

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, no tempo de 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 de 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. As regras de preservaçã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 o apagamento 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 inclui 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.

A partir da versão 2.11.0 do Gson, a biblioteca agrupa as regras de manutenção necessárias para a desserialização no modo completo. Quando você cria o app com o R8 ativado, ele encontra e aplica automaticamente essas regras da biblioteca. Isso oferece a proteção de que seu app precisa sem que você precise adicionar ou manter manualmente essas regras específicas no projeto.

É 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 seus 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 preservaçã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 particular 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 e entenda por que não é recomendável acessar membros particulares usando reflexão e as regras de manutenção para reter esses campos/métodos.