Sfruttare tutto il potenziale dell'ottimizzatore R8

R8 offre due modalità: modalità di compatibilità e modalità completa. La modalità completa offre ottimizzazioni efficaci che migliorano il rendimento dell'app.

Questa guida è rivolta agli sviluppatori Android che vogliono utilizzare le ottimizzazioni più potenti di R8. Esplora le principali differenze tra la modalità compatibilità e quella completa e fornisce le configurazioni esplicite necessarie per eseguire la migrazione del progetto in modo sicuro ed evitare arresti anomali comuni in fase di runtime.

Attivare la modalità completa

Per attivare la modalità completa, rimuovi la seguente riga dal file gradle.properties:

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

Mantieni i corsi associati agli attributi

Gli attributi sono metadati archiviati all'interno di file di classe compilati che non fanno parte del codice eseguibile. Tuttavia, possono essere necessari per determinati tipi di riflessione. Esempi comuni includono Signature (che conserva le informazioni sul tipo generico dopo la cancellazione del tipo), InnerClasses e EnclosingMethod (per riflettere sulla struttura della classe) e le annotazioni visibili in fase di runtime.

Il seguente codice mostra l'aspetto di un attributo Signature per un campo in bytecode. Per un campo:

List<User> users;

Il file di classe compilato conterrà il seguente 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

Le librerie che utilizzano molto la reflection (come Gson) spesso si basano su questi attributi per ispezionare e comprendere dinamicamente la struttura del codice. Per impostazione predefinita, nella modalità completa di R8, gli attributi vengono conservati solo se la classe, il campo o il metodo associato viene mantenuto in modo esplicito.

L'esempio seguente mostra perché gli attributi sono necessari e quali regole keep devi aggiungere quando esegui la migrazione dalla modalità di compatibilità alla modalità completa.

Considera il seguente esempio in cui deserializziamo un elenco di utenti utilizzando la libreria 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 la compilazione, l'eliminazione dei tipi di Java rimuove gli argomenti dei tipi generici. Ciò significa che in fase di runtime sia List<String> che List<User> vengono visualizzati come List non elaborato. Pertanto, librerie come Gson, che si basano sulla reflection, non possono determinare i tipi di oggetti specifici che List è stato dichiarato contenere durante la deserializzazione di un elenco JSON, il che può causare problemi di runtime.

Per conservare le informazioni sul tipo, Gson utilizza TypeToken. Il wrapping TypeToken conserva le informazioni di deserializzazione necessarie.

L'espressione Kotlin object:TypeToken<List<User>>() {}.type crea una classe interna anonima che estende TypeToken e acquisisce le informazioni sul tipo generico. In questo esempio, la classe anonima si chiama $GsonRemoteJsonListExample$listType$1.

Il linguaggio di programmazione Java salva la firma generica di una superclasse come metadati, noti come attributo Signature, all'interno del file della classe compilata. TypeToken utilizza quindi questi metadati Signature per recuperare il tipo in fase di runtime. In questo modo, Gson può utilizzare la reflection per leggere Signature e rilevare correttamente il tipo List<User> completo necessario per la deserializzazione.

Quando R8 è attivato in modalità di compatibilità, conserva l'attributo Signature per le classi, incluse le classi interne anonime come $GsonRemoteJsonListExample$listType$1, anche se le regole di conservazione specifiche non sono definite in modo esplicito. Di conseguenza, la modalità di compatibilità R8 non richiede ulteriori regole di conservazione esplicite per il corretto funzionamento di questo esempio.

// keep rule for compatibility mode
-keepattributes Signature

Quando R8 è abilitato in modalità completa, l'attributo Signature della classe interna anonima $GsonRemoteJsonListExample$listType$1 viene rimosso. Senza queste informazioni sul tipo nel Signature, Gson non riesce a trovare il tipo di applicazione corretto, il che comporta un IllegalStateException. Le regole di conservazione necessarie per evitare questo problema sono:

// 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: Questa regola indica a R8 di conservare l'attributo che Gson deve leggere. In modalità completa, R8 conserva l'attributo Signature solo per classi, campi o metodi che corrispondono esplicitamente a una regola keep.

  • -keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken: questa regola è necessaria perché TypeToken racchiude il tipo di oggetto deserializzato. Dopo l'eliminazione del tipo, viene creata una classe interna anonima per conservare le informazioni sul tipo generico. Se non viene mantenuto esplicitamente com.google.gson.reflect.TypeToken, R8 in modalità completa non includerà questo tipo di classe nell'attributo Signature necessario per la deserializzazione.

  • -keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken: questa regola conserva le informazioni sul tipo delle classi anonime che estendono TypeToken, ad esempio $GsonRemoteJsonListExample$listType$1 in questo esempio. Senza questa regola, R8 in modalità completa rimuove le informazioni sul tipo necessarie, causando la mancata deserializzazione.

A partire dalla versione 2.11.0, la libreria Gson raggruppa le regole keep necessarie per la deserializzazione in modalità completa. Quando crei la tua app con R8 abilitato, R8 trova e applica automaticamente queste regole dalla libreria. In questo modo, la tua app riceve la protezione necessaria senza che tu debba aggiungere o gestire manualmente queste regole specifiche nel tuo progetto.

È importante capire che le regole condivise in precedenza risolvono solo il problema di scoprire il tipo generico (ad es. List<User>). R8 rinomina anche i campi delle classi. Se non utilizzi le annotazioni @SerializedName nei tuoi modelli di dati, Gson non riuscirà a deserializzare JSON perché i nomi dei campi non corrisponderanno più alle chiavi JSON.

Tuttavia, se utilizzi una versione di Gson precedente alla 2.11 o se i tuoi modelli non utilizzano l'annotazione @SerializedName, devi aggiungere regole di conservazione esplicite per questi modelli.

Mantieni il costruttore predefinito

Nella modalità completa R8, il costruttore predefinito/senza argomenti non viene mantenuto implicitamente, anche quando la classe stessa viene conservata. Se crei un'istanza di una classe utilizzando class.getDeclaredConstructor().newInstance() o class.newInstance(), devi conservare esplicitamente il costruttore senza argomenti in modalità completa. Al contrario, la modalità di compatibilità conserva sempre il costruttore senza argomenti.

Considera un esempio in cui viene creata un'istanza di PrecacheTask utilizzando la reflection per chiamare dinamicamente il relativo metodo run. Anche se questo scenario non richiede regole aggiuntive in modalità di compatibilità, in modalità completa, il costruttore predefinito di PrecacheTask verrà rimosso. Pertanto, è necessaria una regola di conservazione specifica.

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

La modifica dell'accesso è abilitata per impostazione predefinita

In modalità compatibilità, R8 non altera la visibilità di metodi e campi all'interno di una classe. Tuttavia, in modalità completa, R8 migliora l'ottimizzazione modificando la visibilità di metodi e campi, ad esempio da privati a pubblici. Ciò consente un maggiore inserimento in linea.

Questa ottimizzazione può causare problemi se il codice utilizza la reflection che si basa specificamente sulla visibilità di determinati membri. R8 non riconoscerà questo utilizzo indiretto, il che potrebbe causare arresti anomali dell'app. Per evitare questo problema, devi aggiungere regole -keep specifiche per conservare i membri, che preserveranno anche la loro visibilità originale.

Per saperne di più, consulta questo esempio per capire perché l'accesso ai membri privati tramite reflection non è consigliato e le regole di conservazione per mantenere questi campi/metodi.