Novità sul prodotto

Configurare e risolvere i problemi relativi alle regole Keep di R8

Lettura di 7 minuti

Nello sviluppo moderno di Android, la distribuzione di un'applicazione piccola, veloce e sicura è un'aspettativa fondamentale degli utenti. Lo strumento principale del sistema di compilazione Android per raggiungere questo obiettivo è l'ottimizzatore R8 , il compilatore che gestisce la rimozione di codice e risorse inutilizzati per la riduzione, la ridenominazione o la minimizzazione del codice e l'ottimizzazione delle app.

L'attivazione di R8 è un passaggio fondamentale per preparare un'app per la release, ma richiede agli sviluppatori di fornire indicazioni sotto forma di "regole di conservazione".

Dopo aver letto questo articolo, guarda il video della settimana di Performance Spotlight su come attivare, eseguire il debug e risolvere i problemi relativi all'ottimizzatore R8 su YouTube.

 

 

Perché sono necessarie le regole di conservazione

La necessità di scrivere regole di conservazione deriva da un conflitto fondamentale: R8 è uno strumento di analisi statica, ma le app per Android spesso si basano su pattern di esecuzione dinamica come la reflection o le chiamate in entrata e in uscita dal codice nativo utilizzando JNI (Java Native Interface).

R8 crea un grafico del codice utilizzato analizzando le chiamate dirette. Quando si accede al codice in modo dinamico, l'analisi statica di R8 non può prevederlo e lo identifica come non utilizzato e lo rimuove, causando arresti anomali in fase di runtime.

Una regola di conservazione è un'istruzione esplicita per il compilatore R8, che indica: "Questa classe, questo metodo o questo campo specifico è un punto di ingresso a cui verrà eseguito l'accesso in modo dinamico in fase di runtime. Devi conservarlo, anche se non riesci a trovare un riferimento diretto."

Per ulteriori dettagli sulle regole di conservazione, consulta la guida ufficiale.

Dove scrivere le regole di conservazione

Le regole di conservazione personalizzate per un'applicazione vengono scritte in un file di testo. Per convenzione, questo file si chiama proguard-rules.pro e si trova nella radice del modulo dell'app o della libreria. Questo file viene quindi specificato nel tipo di compilazione release del file build.gradle.kts del modulo.

  release {

    isShrinkResources = true

    isMinifyEnabled = true

    proguardFiles(

        getDefaultProguardFile("proguard-android-optimize.txt"),

        "proguard-rules.pro",

    )

}

Utilizzare il file predefinito corretto

Il metodo getDefaultProguardFile importa un insieme predefinito di regole fornite dall'SDK Android. Se utilizzi il file sbagliato, l'app potrebbe non essere ottimizzata. Assicurati di utilizzare proguard-android-optimize.txt. Questo file fornisce le regole di conservazione predefinite per i componenti Android standard e consente le ottimizzazioni del codice di R8. La versione obsoleta di proguard-android.txt fornisce solo le regole di conservazione, ma non attiva le ottimizzazioni di R8.

progaurd.png

Poiché si tratta di un grave problema di prestazioni, a partire dal Feature Drop 3 di Android Studio Narwhal, inizieremo ad avvisare gli sviluppatori dell'utilizzo del file errato. A partire dal plug-in Android per Gradle versione 9.0, non supportiamo più il file proguard-android.txtobsoleto. Assicurati quindi di eseguire l'upgrade alla versione ottimizzata.

Come scrivere le regole di conservazione

Una regola di conservazione è composta da tre parti principali:

  1. Un'opzione come -keep-keepclassmembers
  2. Modificatori facoltativi come allowshrinking
  3. Una specifica di classe che definisce il codice da abbinare

Per la sintassi completa e gli esempi, consulta le indicazioni per aggiungere regole di conservazione.

Anti-pattern della regola Keep

È importante conoscere le best practice, ma anche gli anti-pattern. Questi anti-pattern spesso derivano da incomprensioni o scorciatoie per la risoluzione dei problemi e possono essere catastrofici per le prestazioni di una build di produzione.

Opzioni globali

Questi flag sono attivazioni/disattivazioni globali che non devono mai essere utilizzate in una build di rilascio. Servono solo per il debug temporaneo per isolare un problema.

L'utilizzo di -dontotptimize disattiva efficacemente le ottimizzazioni delle prestazioni di R8, con conseguente rallentamento dell'app.

Quando utilizzi -dontobfuscate disabiliti tutta la ridenominazione e l'utilizzo di -dontshrink disattiva la rimozione del codice inutilizzato. Entrambe queste regole globali aumentano le dimensioni dell'app.

Evita di utilizzare questi flag globali in un ambiente di produzione, se possibile, per un'esperienza utente dell'app più performante.

Regole di conservazione eccessivamente generiche

Il modo più semplice per annullare i vantaggi di R8 è scrivere regole di conservazione troppo ampie. Regole come quella riportata di seguito indicano all'ottimizzatore R8 di non ridurre, non offuscare e non ottimizzare nessuna classe in questo pacchetto o nessuno dei relativi sottopacchetti. In questo modo, i vantaggi di R8 vengono completamente rimossi per l'intero pacchetto. Prova invece a scrivere regole di conservazione specifiche e ristrette.
 

  -keep class com.example.package.** { *;} // WIDE KEEP RULES CAUSE PROBLEMS

L'operatore di inversione (!)

L'operatore di inversione (!) sembra un modo efficace per escludere un pacchetto da una regola. Ma non è così semplice. Considera questo esempio:

  -keep class !com.example.my_package.** { *; } // USE WITH CAUTION

Potresti pensare che questa regola significhi "non conservare le classi incom.example.package", ma in realtà significa "conserva ogni classe, metodo e proprietà nell'intera applicazione che non si trova in com.example.package". Se questa informazione ti ha sorpreso, ti consigliamo di verificare la presenza di negazioni nella configurazione di R8.

Regole ridondanti per i componenti Android

Un altro errore comune è l'aggiunta manuale di regole di conservazione per Activities, Services o BroadcastReceivers della tua app. Questa operazione è inutile. Il file proguard-android-optimize.txt predefinito include già le regole pertinenti per il funzionamento immediato di questi componenti Android standard.

Inoltre, molte librerie hanno le proprie regole di conservazione. Pertanto, non dovresti dover scrivere regole personalizzate per questi casi. In caso di problemi con le regole Keep di una libreria che stai utilizzando, è meglio contattare l'autore della libreria per capire qual è il problema.

Best practice per le regole

Ora che sai cosa non fare, parliamo delle best practice.

Scrivere regole di conservazione specifiche

Le regole di conservazione efficaci devono essere il più ristrette e specifiche possibile. Devono conservare solo ciò che è necessario, consentendo a R8 di ottimizzare tutto il resto.
 

RegolaQualità

 

-keep class com.example.** { ; }

 

Basso:mantiene un intero pacchetto e i relativi pacchetti secondari

 

-keep class com.example.MyClass { ; }

 

Basso: mantiene un'intera classe, che probabilmente è ancora troppo ampia
-keepclassmembers class com.example.MyClass {

    private java.lang.String secretMessage;

    public void onNativeEvent(java.lang.String);

}
Alto:vengono mantenuti solo i metodi e le proprietà pertinenti di una classe specifica

Utilizzare antenati comuni

Invece di scrivere regole di conservazione separate per più modelli di dati diversi, scrivi una regola che abbia come target una classe base o un'interfaccia comune. La regola riportata di seguito indica a R8 di conservare tutti i membri delle classi che implementano questa interfaccia ed è altamente scalabile.

  # Keep all fields of any class that implements SerializableModel

-keepclassmembers class * implements com.example.models.SerializableModel {

    <fields>;

}

Utilizzare le annotazioni per scegliere come target più classi

Crea un'annotazione personalizzata (ad es. @Serialize) e utilizzala per "taggare" le classi di cui è necessario conservare i campi. Si tratta di un altro pattern pulito, dichiarativo e altamente scalabile. Puoi anche creare regole di conservazione per le annotazioni già esistenti dai framework che utilizzi.

  # Keep all fields of any class annotated with @Serialize

-keepclassmembers class * {

    @com.example.annotations.Serialize <fields>;

}

Scegliere l'opzione di Keep giusta

L'opzione di conservazione è la parte più importante della regola. La scelta di un'opzione errata può disattivare inutilmente l'ottimizzazione.

Opzione KeepCosa fa
-keepImpedisce la rimozione o la ridenominazione della classe e dei membri menzionati nella dichiarazione .
-keepclassmembersImpedisce la rimozione o la ridenominazione dei membri specificati, ma consente la rimozione del corso stesso solo se non è già stato rimosso.
-keepclasseswithmembersUna combinazione: mantiene il corso e i suoi membri solo se sono presenti tutti i membri specificati.

Per saperne di più sull'opzione di conservazione, consulta la nostra documentazione sulle opzioni di conservazione.

Consenti l'ottimizzazione con i modificatori

Modificatori come allowshrinkingallowobfuscation rilassano una regola -keep generale, restituendo il potere di ottimizzazione a R8. Ad esempio, se una libreria legacy ti costringe a utilizzare -keep su un'intera classe, potresti recuperare parte dell'ottimizzazione consentendo la riduzione e l'offuscamento:

  # Keep this class, but allow R8 to remove it if it's unused and allow R8 to rename it.

-keep,allowshrinking,allowobfuscation class com.example.LegacyClass

Aggiungere opzioni globali per un'ulteriore ottimizzazione

Oltre alle regole di conservazione, puoi aggiungere flag globali al file di configurazione R8 per favorire un'ulteriore ottimizzazione.

-repackageclasses è un'opzione potente che indica a R8 di spostare tutte le classi offuscate in un unico pacchetto. In questo modo si risparmia spazio significativo nel file DEX rimuovendo le stringhe ridondanti del nome del pacchetto.

-allowaccessmodification consente a R8 di ampliare l'accesso (ad es. da privatepublic) per consentire l'incorporamento più aggressivo. Questa opzione è ora abilitata per impostazione predefinita quando si utilizza proguard-android-optimize.txt.

Avviso: gli autori delle librerie non devono mai aggiungere questi flag di ottimizzazione globali alle regole dei consumatori, in quanto verrebbero applicati forzatamente all'intera app.

Per rendere il tutto ancora più chiaro, nella versione 9.0 del plug-in Android per Gradle inizieremo a ignorare completamente i flag di ottimizzazione globali delle librerie. 

Best practice per le librerie

Ogni app per Android si basa sulle librerie in un modo o nell'altro. Vediamo quindi le best practice per le librerie.

Per gli sviluppatori di librerie

Se la tua libreria utilizza la reflection o JNI, è tua responsabilità fornire le regole di conservazione necessarie ai suoi consumatori. Queste regole vengono inserite in un file consumer-rules.pro, che viene poi raggruppato automaticamente all'interno del file AAR della libreria.

  android {

    defaultConfig {

        consumerProguardFiles("consumer-rules.pro")

    }

    ...

}

Per gli utenti della raccolta

Filtrare le regole di conservazione problematiche

Se devi utilizzare una libreria che include regole Keep problematiche, puoi filtrarle nel file build.gradle.kts a partire da AGP 9.0. In questo modo, R8 ignora le regole provenienti da una dipendenza specifica.

  release {

    optimization.keepRules {

        // Ignore all consumer rules from this specific library

        it.ignoreFrom("com.somelibrary:somelibrary")

    }

}

La migliore regola di conservazione è nessuna regola di conservazione

La strategia di configurazione R8 definitiva consiste nel rimuovere completamente la necessità di scrivere regole Keep. Per molte app, questo risultato può essere ottenuto scegliendo librerie moderne che favoriscono la generazione di codice rispetto alla reflection.Con la generazione di codice, l'ottimizzatore può determinare più facilmente quale codice viene effettivamente utilizzato in fase di runtime e quale può essere rimosso. Inoltre, se non utilizzi alcuna reflection dinamica, non ci sono punti di ingresso "nascosti" e, di conseguenza, non sono necessarie regole di conservazione. Quando scegli una nuova libreria, preferisci sempre una soluzione che utilizzi la generazione di codice rispetto alla reflection.

Per saperne di più su come scegliere le librerie, consulta Scegliere la libreria giusta.

Debug e risoluzione dei problemi della configurazione di R8

Quando R8 rimuove codice che avrebbe dovuto conservare o il tuo APK è più grande del previsto, utilizza questi strumenti per diagnosticare il problema.

Trovare regole di conservazione duplicate e globali

Poiché R8 unisce le regole di decine di origini, può essere difficile sapere qual è il set di regole "finale". L'aggiunta di questo flag al file proguard-rules.pro genera un report completo:

  # Outputs the final, merged set of rules to the specified file

-printconfiguration build/outputs/logs/configuration.txt

Puoi cercare in questo file regole ridondanti o risalire a una regola problematica (ad esempio -dontoptimize) fino alla libreria specifica che la include.

Chiedi a R8: perché lo conservi?

Se un corso che ti aspettavi di rimuovere è ancora presente nell'app, R8 può dirti il motivo. Aggiungi questa regola:

  # Asks R8 to explain why it's keeping a specific class

class com.example.MyUnusedClass

-whyareyoukeeping 

Durante la compilazione, R8 stampa la catena esatta di riferimenti che ha causato il mantenimento della classe, consentendoti di tracciare il riferimento e modificare le regole.

Per una guida completa, consulta la sezione Risoluzione dei problemi di R8.

Passaggi successivi

R8 è un potente strumento per migliorare le prestazioni delle app per Android. La sua efficacia dipende da una corretta comprensione del suo funzionamento come motore di analisi statica.

Scrivendo regole specifiche a livello di membro, sfruttando gli antenati e le annotazioni e scegliendo con attenzione le opzioni di conservazione giuste, puoi conservare esattamente ciò che è necessario. La pratica più avanzata consiste nell'eliminare completamente la necessità di regole scegliendo librerie moderne basate sulla generazione di codice rispetto alle loro versioni precedenti basate sulla reflection.

Mentre segui la settimana di Performance Spotlight, assicurati di guardare il video di oggi su YouTube e continua con la nostra sfida R8. Utilizza #optimizationEnabled per qualsiasi domanda sull'attivazione o la risoluzione dei problemi di R8. Siamo a tua disposizione.

È ora di scoprire i vantaggi per te.

Ti invitiamo ad attivare la modalità completa R8 per la tua app oggi stesso.

  1. Per iniziare, segui le nostre guide per gli sviluppatori: Attivare l'ottimizzazione delle app.
  2. Controlla se utilizzi ancora proguard-android.txt e sostituiscilo con proguard-android-optimize.txt.
  3. Poi, misura l'impatto. Non limitarti a sentire la differenza, verificala. Misura i miglioramenti delle prestazioni adattando il codice della nostra  app di esempio Macrobenchmark su GitHub per misurare i tempi di avvio prima e dopo.

Siamo certi che noterai un miglioramento significativo delle prestazioni della tua app.

Già che ci sei, usa l'hashtag #AskAndroid per porre le tue domande. Durante la settimana, i nostri esperti monitorano e rispondono alle tue domande.

Non perderti l'appuntamento di domani, in cui parleremo dell'ottimizzazione guidata dal profilo con i profili di baseline e di avvio, condivideremo i miglioramenti del rendimento del rendering di Compose nelle versioni precedenti e prenderemo in esame le considerazioni sul rendimento per il lavoro in background.

Scritto da:

Continua a leggere