Utilizzare R8 in modalità completa

R8 offre due modalità: modalità di compatibilità e modalità completa. La modalità completa offre ottimizzazioni avanzate che migliorano le prestazioni dell'app.

Questa guida è rivolta agli sviluppatori Android che vogliono utilizzare le ottimizzazioni più potenti di R8. Esplora le differenze principali tra la modalità di 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

Mantenere le classi associate agli attributi

Gli attributi sono metadati archiviati all'interno dei 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 l'eliminazione 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 della classe compilata conterrebbe 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 mantenuti 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. Oltre a mantenere le classi, i campi o i metodi su cui si riflette, devi anche mantenere esplicitamente gli attributi da cui dipendono.

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 non sono definite esplicitamente regole di conservazione specifiche. 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.

Se utilizzi una versione di Gson precedente alla 2.11.0, 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 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. Senza mantenere 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 riuscita della deserializzazione.

È importante capire che le regole condivise in precedenza risolvono solo il problema di rilevamento del tipo generico (ad esempio, List<User>). R8 rinomina anche i campi delle classi. Se non utilizzi le annotazioni @SerializedName nei 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.

Mantenere il costruttore predefinito

Nella modalità completa di 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à mantiene 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 è attivata 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 maggior numero di incorporamenti.

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, devi aggiungere regole -keep specifiche per conservare i membri, che manterranno 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.

Metadati specifici di Kotlin

Durante la compilazione del codice Kotlin, il compilatore Kotlin archivia i metadati specifici della lingua (come supporto di valori null, funzioni di estensione e firme di coroutine) in un'annotazione @kotlin.Metadata su ogni file di classe.

Se la tua app o le sue dipendenze utilizzano la reflection Kotlin (kotlin.reflect), la libreria di reflection analizza questi metadati in fase di runtime per esaminare la struttura della classe. Nella modalità completa R8, R8 rimuove le annotazioni per impostazione predefinita se non vengono mantenute in modo esplicito. Inoltre, se R8 riduce o comprime le classi senza conservare e aggiornare i metadati, la reflection Kotlin non riuscirà in fase di runtime, causando un comportamento imprevedibile o arresti anomali (ad esempio KotlinReflectionInternalError).

Per evitare comportamenti imprevedibili e garantire che le funzioni di reflection Kotlin funzionino correttamente dopo la minificazione, devi conservare le annotazioni visibili in fase di runtime e preservare esplicitamente la 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 { *; }