Riduci, offusca e ottimizza la tua app

Per rendere la tua app il più piccola e veloce possibile, devi ottimizzare e ridurre al minimo la compilazione della release con isMinifyEnabled = true.

In questo modo vengono attivate la riduzione, che rimuove il codice inutilizzato, l'offuscamento, che abbrevia i nomi delle classi e dei membri dell'app, e l'ottimizzazione, che applica strategie di ottimizzazione del codice migliorate per ridurre ulteriormente le dimensioni e migliorare le prestazioni dell'app. Questa pagina descrive in che modo R8 esegue queste attività di compilazione per il progetto e come puoi personalizzarle.

Quando crei il progetto utilizzando Android Gradle Plugin 3.4.0 o versioni successive, il plug-in non utilizza più ProGuard per eseguire l'ottimizzazione del codice in fase di compilazione. Il plug-in funziona invece con il compilatore R8 per gestire le seguenti attività di compilazione:

  • Riduzione del codice (o tree-shaking): rileva e rimuove in modo sicuro classi, campi, metodi e attributi inutilizzati dall'app e dalle dipendenze della libreria (il che lo rende uno strumento prezioso per aggirare il limite di 64.000 riferimenti). Ad esempio, se utilizzi solo alcune API di una dipendenza della libreria, la riduzione può identificare il codice della libreria che la tua app non utilizza e rimuoverlo solo da quest'ultima. Per scoprire di più, vai alla sezione su come ridurre il codice.
  • Riduzione delle risorse:rimuove le risorse inutilizzate dall'app pacchettizzata, incluse quelle nelle dipendenze delle librerie dell'app. Funziona insieme alla riduzione del codice, in modo che una volta rimosso il codice inutilizzato, sia possibile rimuovere in sicurezza anche tutte le risorse a cui non si fa più riferimento. Per saperne di più, vai alla sezione su come ridurre le risorse.
  • Ottimizzazione:ispeziona e riscrivi il codice per migliorare le prestazioni in fase di runtime e ridurre ulteriormente le dimensioni dei file DEX dell'app. Questo migliora le prestazioni di runtime del codice fino al 30%, migliorando drasticamente l'avvio e la tempistica dei frame. Ad esempio, se R8 rileva che il ramo else {} per una determinata istruzione if/else non viene mai eseguito, rimuove il codice per il branco else {}. Per saperne di più, vai alla sezione sull'ottimizzazione del codice.
  • Offuscamento (o minimizzazione degli identificatori): abbrevia il nome delle classi e dei membri, con conseguente riduzione delle dimensioni dei file DEX. Per saperne di più, consulta la sezione su come offuscare il codice.

Quando crei la versione di release dell'app, R8 può essere configurato in modo da eseguire le attività in fase di compilazione descritte sopra. Puoi anche disattivare determinate attività o personalizzare il comportamento di R8 tramite i file delle regole di ProGuard. Infatti, R8 funziona con tutti i file di regole di ProGuard esistenti, quindi l'aggiornamento del plug-in Android Gradle in modo che utilizzi R8 non dovrebbe richiedere la modifica delle regole esistenti.

Attivare la riduzione, l'offuscamento e l'ottimizzazione

Quando utilizzi Android Studio 3.4 o il plug-in Android Gradle 3.4.0 e versioni successive, R8 è il compilatore predefinito che converte il bytecode Java del progetto nel formato DEX che viene eseguito sulla piattaforma Android. Tuttavia, quando crei un nuovo progetto con Android Studio, la riduzione, l'oscuramento e l'ottimizzazione del codice non sono attivati per impostazione predefinita. Questo perché queste ottimizzazioni in fase di compilazione aumentano il tempo di compilazione del progetto e potrebbero introdurre bug se non personalizzi sufficientemente il codice da conservare.

Pertanto, è meglio attivare queste attività di compilazione quando crei la versione finale della tua app che testerai prima della pubblicazione. Per attivare la riduzione, l'oscuramento e l'ottimizzazione, includi quanto segue nello script di compilazione a livello di progetto.

Kotlin

android {
    buildTypes {
        getByName("release") {
            // Enables code shrinking, obfuscation, and optimization for only
            // your project's release build type. Make sure to use a build
            // variant with `isDebuggable=false`.
            isMinifyEnabled = true

            // Enables resource shrinking, which is performed by the
            // Android Gradle plugin.
            isShrinkResources = true

            proguardFiles(
                // Includes the default ProGuard rules files that are packaged with
                // the Android Gradle plugin. To learn more, go to the section about
                // R8 configuration files.
                getDefaultProguardFile("proguard-android-optimize.txt"),

                // Includes a local, custom Proguard rules file
                "proguard-rules.pro"
            )
        }
    }
    ...
}

Groovy

android {
    buildTypes {
        release {
            // Enables code shrinking, obfuscation, and optimization for only
            // your project's release build type. Make sure to use a build
            // variant with `debuggable false`.
            minifyEnabled true

            // Enables resource shrinking, which is performed by the
            // Android Gradle plugin.
            shrinkResources true

            // Includes the default ProGuard rules files that are packaged with
            // the Android Gradle plugin. To learn more, go to the section about
            // R8 configuration files.
            proguardFiles getDefaultProguardFile(
                    'proguard-android-optimize.txt'),
                    'proguard-rules.pro'
        }
    }
    ...
}

File di configurazione R8

R8 utilizza i file delle regole di ProGuard per modificare il comportamento predefinito e comprendere meglio la struttura dell'app, ad esempio le classi che fungono da punti di contatto nel codice dell'app. Sebbene tu possa modificare alcuni di questi file di regole, alcune regole potrebbero essere generate automaticamente dagli strumenti di compilazione, come AAPT2, o ereditate dalle dipendenze della libreria dell'app. La tabella seguente descrive le origini dei file delle regole di ProGuard utilizzati da R8.

Fonte Posizione Descrizione
Android Studio <module-dir>/proguard-rules.pro Quando crei un nuovo modulo utilizzando Android Studio, l'IDE crea un proguard-rules.pro file nella directory principale del modulo.

Per impostazione predefinita, questo file non applica alcuna regola. Pertanto, includi qui le tue regole ProGuard, come le regole di conservazione personalizzate.

Plug-in Android per Gradle Generato dal plug-in Android per Gradle in fase di compilazione. Il plug-in Android per Gradle genera proguard-android-optimize.txt, che include regole utili per la maggior parte dei progetti Android e attiva @Keep* annotazioni.

Per impostazione predefinita, quando crei un nuovo modulo utilizzando Android Studio, lo script di compilazione a livello di modulo include automaticamente questo file di regole nella build di release.

Nota:il plug-in Android Gradle include altri file di regole ProGuard predefiniti, ma è consigliabile utilizzare proguard-android-optimize.txt.

Dipendenze libreria

In una raccolta AAR:
proguard.txt

In una libreria JAR:
META-INF/proguard/<ProGuard-rules-file>

Oltre a queste posizioni, il plug-in Gradle per Android 3.6 o versioni successive supporta anche le regole di riduzione mirate.

Se una libreria AAR o JAR viene pubblicata con il proprio file di regole e includi questa libreria come dipendenza in fase di compilazione, R8 applica automaticamente queste regole al momento della compilazione del progetto.

Oltre alle regole ProGuard convenzionali, il plug-in Android Gradle 3.6 o versioni successive supporta anche regole di riduzione mirate. Queste sono regole che hanno come target specifici shrinker (R8 o ProGuard) e versioni specifiche di shrinker.

L'utilizzo di file di regole inclusi nelle librerie è utile se per il corretto funzionamento della libreria sono necessarie determinate regole; in altre parole, lo sviluppatore della libreria ha eseguito la procedura di risoluzione dei problemi per te.

Tuttavia, tieni presente che, poiché le regole sono additive, alcune regole incluse in una dipendenza della libreria non possono essere rimosse e potrebbero influire sulla compilazione di altre parti dell'app. Ad esempio, se una libreria include una regola per disattivare le ottimizzazioni del codice, questa regola disattiva le ottimizzazioni per l'intero progetto.

Android Asset Package Tool 2 (AAPT2) Dopo aver creato il progetto con minifyEnabled true: <module-dir>/build/intermediates/aapt_proguard_file/.../aapt_rules.txt AAPT2 genera regole di mantenimento in base ai riferimenti alle classi nel manifest, nei layout e in altre risorse dell'app. Ad esempio, AAPT2 include una regola di mantenimento per ogni attività registrata nel file manifest dell'app come punto di contatto.
File di configurazione personalizzati Per impostazione predefinita, quando crei un nuovo modulo utilizzando Android Studio, l'IDE crea <module-dir>/proguard-rules.pro per consentirti di aggiungere le tue regole. Puoi includere configurazioni aggiuntive, che R8 applica in fase di compilazione.

Quando imposti la proprietà minifyEnabled su true, R8 combina le regole di tutte le fonti disponibili elencate sopra. È importante ricordare questo aspetto quando risolvi i problemi relativi a R8, perché altre dipendenze di compilazione, come le dipendenze delle librerie, potrebbero introdurre modifiche al comportamento di R8 di cui non sei a conoscenza.

Per generare un report completo di tutte le regole applicate da R8 durante la compilazione del progetto, includi quanto segue nel file proguard-rules.pro del modulo:

// You can specify any path and filename.
-printconfiguration ~/tmp/full-r8-config.txt

Regole di riduzione target

Il plug-in Android Gradle 3.6 o versioni successive supporta le regole delle librerie che hanno come target rieseguitori specifici (R8 o ProGuard), nonché versioni specifiche di rieseguitori. In questo modo, gli sviluppatori di librerie possono personalizzare le proprie regole in modo che funzionino in modo ottimale nei progetti che utilizzano le nuove versioni degli strumenti di compressione, consentendo al contempo di continuare a utilizzare le regole esistenti nei progetti con versioni precedenti degli strumenti di compressione.

Per specificare le regole di riduzione mirate, gli sviluppatori delle librerie dovranno includerle in posizioni specifiche all'interno di una libreria AAR o JAR, come descritto di seguito.

In an AAR library:
    proguard.txt (legacy location)
    classes.jar
    └── META-INF
        └── com.android.tools (targeted shrink rules location)
            ├── r8-from-<X>-upto-<Y>/<R8-rules-file>
            └── proguard-from-<X>-upto-<Y>/<ProGuard-rules-file>

In a JAR library:
    META-INF
    ├── proguard/<ProGuard-rules-file> (legacy location)
    └── com.android.tools (targeted shrink rules location)
        ├── r8-from-<X>-upto-<Y>/<R8-rules-file>
        └── proguard-from-<X>-upto-<Y>/<ProGuard-rules-file>

Ciò significa che le regole di riduzione mirate vengono archiviate nella directory META-INF/com.android.tools di un file JAR o nella directory META-INF/com.android.tools all'interno di classes.jar di un file AAR.

In questa directory possono essere presenti più directory con nomi nel formato r8-from-<X>-upto-<Y> o proguard-from-<X>-upto-<Y> per indicare per quali versioni di quale compressore sono scritte le regole all'interno delle directory. Tieni presente che le parti -from-<X> e -upto-<Y> sono facoltative, la versione <Y> è esclusiva e gli intervalli di versione devono essere continui.

Ad esempio, r8-upto-8.0.0, r8-from-8.0.0-upto-8.2.0 e r8-from-8.2.0 formano un insieme valido di regole di riduzione scelte come target. Le regole nella directoryr8-from-8.0.0-upto-8.2.0 verranno utilizzate da R8 dalla versione 8.0.0 fino alla versione 8.2.0, esclusa.

In base a queste informazioni, il plug-in Android Gradle 3.6 o versioni successive selezionerà le regole dalle directory R8 corrispondenti. Se una libreria non specifica le regole di compressione mirate, il plug-in Gradle per Android seleziona le regole dalle posizioni precedenti (proguard.txt per un file AAR oMETA-INF/proguard/<ProGuard-rules-file> per un file JAR).

Gli sviluppatori di librerie possono scegliere di includere nelle loro librerie regole di riduzione mirate o regole ProGuard precedenti oppure entrambi i tipi se vogliono mantenere la compatibilità con il plug-in Gradle per Android precedente alla versione 3.6 o con altri strumenti.

Includi configurazioni aggiuntive

Quando crei un nuovo progetto o modulo utilizzando Android Studio, l'IDE crea un <module-dir>/proguard-rules.pro file in cui includere le tue regole. Puoi anche includere regole aggiuntive da altri file aggiungendole alla proprietà proguardFiles nello script di compilazione del modulo.

Ad esempio, puoi aggiungere regole specifiche per ogni variante di compilazione aggiungendo un'altra proprietà proguardFiles nel blocco productFlavor corrispondente. Il seguente file Gradle aggiunge flavor2-rules.pro al flavor del prodotto flavor2. Ora flavor2 utilizza tutte e tre le regole di ProGuard perché vengono applicate anche quelle del blocco release.

Inoltre, puoi aggiungere la proprietà testProguardFiles, che specifica un elenco di file ProGuard inclusi solo nell'APK di test:

Kotlin

android {
    ...
    buildTypes {
        getByName("release") {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                // List additional ProGuard rules for the given build type here. By default,
                // Android Studio creates and includes an empty rules file for you (located
                // at the root directory of each module).
                "proguard-rules.pro"
            )
            testProguardFiles(
                // The proguard files listed here are included in the
                // test APK only.
                "test-proguard-rules.pro"
            )
        }
    }
    flavorDimensions.add("version")
    productFlavors {
        create("flavor1") {
            ...
        }
        create("flavor2") {
            proguardFile("flavor2-rules.pro")
        }
    }
}

Groovy

android {
    ...
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles
                getDefaultProguardFile('proguard-android-optimize.txt'),
                // List additional ProGuard rules for the given build type here. By default,
                // Android Studio creates and includes an empty rules file for you (located
                // at the root directory of each module).
                'proguard-rules.pro'
            testProguardFiles
                // The proguard files listed here are included in the
                // test APK only.
                'test-proguard-rules.pro'
        }
    }
    flavorDimensions "version"
    productFlavors {
        flavor1 {
            ...
        }
        flavor2 {
            proguardFile 'flavor2-rules.pro'
        }
    }
}

Riduci il codice

Il codice ridotto con R8 è attivato per impostazione predefinita quando imposti la proprietà minifyEnabled su true.

La riduzione del codice (detta anche tree shaking) è il processo di rimozione del codice che R8 determina non essere necessario in fase di esecuzione. Questo processo può ridurre notevolmente le dimensioni della tua app se, ad esempio, l'app include molte dipendenze di libreria, ma utilizza solo una piccola parte delle sue funzionalità.

Per ridurre il codice della tua app, R8 determina innanzitutto tutti i punti di ingresso nel codice dell'app in base all'insieme combinato di file di configurazione. Questi punti di accesso includono tutti i corsi che la piattaforma Android potrebbe utilizzare per aprire le attività o i servizi della tua app. A partire da ogni punto di ingresso, R8 ispeziona il codice dell'app per creare un grafico di tutti i metodi, delle variabili dei membri e di altre classi a cui l'app potrebbe accedere in fase di runtime. Il codice non collegato al grafico è considerato non raggiungibile e potrebbe essere rimosso dall'app.

La figura 1 mostra un'app con una dipendenza dalla libreria di runtime. Durante l'ispezione del codice dell'app, R8 determina che i metodi foo(), faz() e bar() sono accessibili dal punto di contatto MainActivity.class. Tuttavia, la classeOkayApi.class o il relativo metodo baz() non viene mai utilizzata dall'app in fase di esecuzione e R8 rimuove questo codice durante il ridimensionamento dell'app.

Figura 1. Durante la compilazione, R8 crea un grafico basato sulle regole di conservazione combinate del tuo progetto per determinare il codice non raggiungibile.

R8 determina i punti di contatto tramite le regole -keep nei file di configurazione R8 del progetto. In altre parole, le regole keep specificano le classi che R8 non deve eliminare durante il ridimensionamento dell'app e R8 le considera come possibili punti di contatto dell'app. Il plug-in Gradle per Android e AAPT2 generano automaticamente le regole keep richieste dalla maggior parte dei progetti di app, ad esempio attività, visualizzazioni e servizi dell'app. Tuttavia, se devi personalizzare questo comportamento predefinito con regole di conservazione aggiuntive, leggi la sezione su come personalizzare il codice da conservare.

Se invece vuoi solo ridurre le dimensioni delle risorse della tua app, vai alla sezione su come condurre le risorse.

Tieni presente che se un progetto della libreria viene ridotto, un'app che dipende da quella libreria include le classi della libreria ridotte. Potrebbe essere necessario modificare le regole di conservazione della libreria se mancano classi nell'APK della libreria. Se stai creando e pubblicando una libreria in formato AAR, i file JAR locali di cui la libreria dipende non vengono ridotti nel file AAR.

Personalizzare il codice da conservare

Per la maggior parte delle situazioni, il file di regole di ProGuard predefinito (proguard-android-optimize.txt) è sufficiente per consentire a R8 di rimuovere solo il codice inutilizzato. Tuttavia, in alcune situazioni è difficile per R8 analizzare correttamente il codice e potrebbe rimuovere il codice di cui la tua app ha effettivamente bisogno. Ecco alcuni esempi di casi in cui il codice potrebbe essere rimosso erroneamente:

  • Quando l'app chiama un metodo dall'interfaccia nativa Java (JNI)
  • Quando l'app cerca il codice in fase di esecuzione (ad esempio con la riflessione)

Il test dell'app dovrebbe rilevare eventuali errori causati da codice rimosso in modo improprio, ma puoi anche controllare il codice rimosso generando un report sul codice rimosso.

Per correggere gli errori e forzare R8 a mantenere un determinato codice, aggiungi una riga -keep nel file delle regole di ProGuard. Ad esempio:

-keep public class MyClass

In alternativa, puoi aggiungere l'annotazione @Keep al codice che vuoi conservare. L'aggiunta di @Keep a una classe mantiene invariata l'intera classe. L'aggiunta a un metodo o a un campo manterrà invariati il metodo/campo (e il relativo nome) nonché il nome della classe. Tieni presente che questa annotazione è disponibile solo se utilizzi la libreria di annotazioni AndroidX e se includi il file delle regole di ProGuard pacchettizzato con il plug-in Android Gradle, come descritto nella sezione su come abilitare il ridimensionamento.

Quando utilizzi l'opzione -keep, ci sono molte considerazioni da fare; per ulteriori informazioni sulla personalizzazione del file delle regole, consulta il manuale di ProGuard. La sezione Risoluzione dei problemi illustra altri problemi comuni che potresti riscontrare quando il codice viene rimosso.

Stacca le librerie native

Per impostazione predefinita, le librerie di codice native vengono eliminate nelle build di release della tua app. Questa operazione consiste nella rimozione della tabella dei simboli e delle informazioni di debug contenute in eventuali librerie native utilizzate dalla tua app. Rimuovere le librerie di codice native permette di risparmiare notevolmente sulle dimensioni, ma non è possibile diagnosticare arresti anomali su Google Play Console a causa di informazioni mancanti (ad esempio nomi di classi e funzioni).

Assistenza per gli arresti anomali nativi

Google Play Console registra gli arresti anomali nativi in Android vitals. Con pochi passaggi, puoi generare e caricare un file di simboli di debug nativo per la tua app. Questo file attiva le tracce dello stack degli arresti anomali nativi simbolizzati (che includono i nomi di classi e funzioni) in Android Vitals per aiutarti a eseguire il debug dell'app in produzione. Questi passaggi variano a seconda della versione del plug-in Android per Gradle utilizzata nel progetto e dell'output della compilazione del progetto.

Plug-in Android per Gradle versione 4.1 o successive

Se il tuo progetto crea un Android App Bundle, puoi includere automaticamente il file dei simboli di debug nativo al suo interno. Per includere questo file nelle build di release, aggiungi quanto segue al file build.gradle.kts dell'app:

android.buildTypes.release.ndk.debugSymbolLevel = { SYMBOL_TABLE | FULL }

Seleziona il livello del simbolo di debug tra i seguenti:

  • Utilizza SYMBOL_TABLE per ottenere i nomi delle funzioni nelle analisi dello stack simboliche di Play Console. Questo livello supporta le tombstone.
  • Utilizza FULL per ottenere i nomi delle funzioni, i file e i numeri di riga nelle analisi dello stack simboliche di Play Console.

Se il tuo progetto crea un APK, utilizza l'impostazione di build build.gradle.kts mostrata in precedenza per generare il file dei simboli di debug nativi separatamente. Carica manualmente il file di simboli di debug nativi in Google Play Console. Durante il processo di compilazione, il plug-in Android per Gradle genera questo file nella seguente posizione del progetto:

app/build/outputs/native-debug-symbols/variant-name/native-debug-symbols.zip

Plug-in Android per Gradle versione 4.0 o precedenti (e altri sistemi di compilazione)

Nell'ambito del processo di compilazione, il plug-in Android per Gradle conserva una copia delle librerie senza dati in una directory di progetto. Questa struttura di directory è simile alla seguente:

app/build/intermediates/cmake/universal/release/obj/
├── armeabi-v7a/
│   ├── libgameengine.so
│   ├── libothercode.so
│   └── libvideocodec.so
├── arm64-v8a/
│   ├── libgameengine.so
│   ├── libothercode.so
│   └── libvideocodec.so
├── x86/
│   ├── libgameengine.so
│   ├── libothercode.so
│   └── libvideocodec.so
└── x86_64/
    ├── libgameengine.so
    ├── libothercode.so
    └── libvideocodec.so
  1. Comprimi i contenuti di questa directory:

    cd app/build/intermediates/cmake/universal/release/obj
    zip -r symbols.zip .
    
  2. Carica manualmente il file symbols.zip in Google Play Console.

Riduci le risorse

La riduzione delle risorse funziona solo insieme alla riduzione del codice. Dopo aver rimosso tutto il codice inutilizzato, lo shrinker di risorse può identificare le risorse ancora utilizzate dall'app. Questo è particolarmente vero quando aggiungi librerie di codice che includono risorse: devi rimuovere il codice della libreria inutilizzato in modo che le risorse della libreria non vengano referenziate e, di conseguenza, possano essere rimosse dallo Shrinker delle risorse.

Per abilitare la riduzione delle risorse, imposta la proprietà shrinkResources su true nello script di build (insieme a minifyEnabled per la riduzione del codice). Ad esempio:

Kotlin

android {
    ...
    buildTypes {
        getByName("release") {
            isShrinkResources = true
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

Groovy

android {
    ...
    buildTypes {
        release {
            shrinkResources true
            minifyEnabled true
            proguardFiles
                getDefaultProguardFile('proguard-android.txt'),
                'proguard-rules.pro'
        }
    }
}

Se non hai già creato l'app utilizzando minifyEnabled per la riduzione del codice, prova a farlo prima di abilitare shrinkResources, perché potresti dover modificare il file proguard-rules.pro per conservare le classi o i metodi che vengono creati o richiamati in modo dinamico prima di iniziare a rimuovere le risorse.

Personalizzare le risorse da conservare

Se ci sono risorse specifiche che vuoi conservare o eliminare, crea un file XML nel progetto con un tag <resources> e specifica ogni risorsa da conservare nell'attributo tools:keep e ogni risorsa da eliminare nell'attributo tools:discard. Entrambi gli attributi accettano un elenco di nomi di risorse separati da virgole. Puoi utilizzare il carattere asterisco come carattere jolly.

Ad esempio:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
    tools:discard="@layout/unused2" />

Salva questo file nelle risorse del progetto, ad esempio in res/raw/my.package.keep.xml. La compilazione non include questo file nella app.

Nota: assicurati di utilizzare un nome univoco per il file keep. Se vengono collegate librerie diverse, le regole di Keep entrano in conflitto altrimenti, causando potenziali problemi con regole ignorate o risorse conservate non necessarie.

Specificare le risorse da eliminare potrebbe sembrare inutile, quando invece potresti eliminarle, ma può essere utile quando utilizzi le varianti di build. Ad esempio, potresti inserire tutte le risorse nella directory del progetto comune, quindi creare un file my.package.build.variant.keep.xml diverso per ogni variante di compilazione quando sai che una determinata risorsa sembra essere utilizzata nel codice (e quindi non viene rimossa dallo shrinker), ma sai che in realtà non verrà utilizzata per la variante di compilazione in questione. È anche possibile che gli strumenti di compilazione abbiano identificato erroneamente una risorsa come necessaria, il che è possibile perché il compilatore aggiunge gli ID risorsa in linea e l'analizzatore delle risorse potrebbe non conoscere la differenza tra una risorsa a cui viene fatto effettivamente riferimento e un valore intero nel codice che ha lo stesso valore.

Attiva i controlli rigorosi dei riferimenti

In genere, lo strumento di riduzione delle risorse può determinare con precisione se una risorsa viene utilizzata. Tuttavia, se il codice effettua una chiamata a Resources.getIdentifier() (o se una delle tue librerie lo fa, la libreria AppCompat lo fa), significa che il tuo codice cerca i nomi delle risorse in base a stringhe generate dinamicamente. In questo modo, lo Shrinker delle risorse si comporta in modo difensivo per impostazione predefinita e contrassegna tutte le risorse con un formato del nome corrispondente come potenzialmente utilizzate e non disponibili per la rimozione.

Ad esempio, il seguente codice fa sì che tutte le risorse con il prefisso img_ vengano contrassegnate come utilizzate.

Kotlin

val name = String.format("img_%1d", angle + 1)
val res = resources.getIdentifier(name, "drawable", packageName)

Java

String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());

Lo shrinker delle risorse esamina anche tutte le costanti stringa nel codice e in varie risorse res/raw/, cercando URL delle risorse in un formato simile a file:///android_res/drawable//ic_plus_anim_016.png. Se trova stringhe come questa o altre che sembrano utilizzabili per creare URL di questo tipo, non le rimuove.

Questi sono esempi della modalità di riduzione sicura abilitata per impostazione predefinita. Tuttavia, puoi disattivare questa gestione "meglio prevenire che curare" e specificare che lo strumento di riduzione delle risorse mantenga solo le risorse di cui è certo che vengono utilizzate. Per farlo, imposta shrinkMode su strict nel file keep.xml, come segue:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:shrinkMode="strict" />

Se attivi la modalità di riduzione rigorosa e il codice fa riferimento anche a risorse con stringhe generate dinamicamente, come mostrato sopra, devi mantenere manualmente queste risorse utilizzando l'attributo tools:keep.

Rimuovi le risorse alternative inutilizzate

Lo strumento di riduzione delle risorse Gradle rimuove solo le risorse a cui non fa riferimento il codice dell'app, il che significa che non rimuoverà le risorse alternative per configurazioni di dispositivi diverse. Se necessario, puoi utilizzare la proprietà resConfigs del plug-in Android Gradle per rimuovere i file di risorse alternative di cui la tua app non ha bisogno.

Ad esempio, se utilizzi una libreria che include risorse linguistiche (come AppCompat o Google Play Services), la tua app include tutte le stringhe di lingua tradotte per i messaggi in queste librerie, indipendentemente dal fatto che il resto dell'app sia tradotto nelle stesse lingue o meno. Se vuoi mantenere solo le lingue supportate ufficialmente dalla tua app, puoi specificarle utilizzando la proprietà resConfig. Tutte le risorse per le lingue non specificate vengono rimosse.

Lo snippet seguente mostra come limitare le risorse linguistiche solo all'inglese e al francese:

Kotlin

android {
    defaultConfig {
        ...
        resourceConfigurations.addAll(listOf("en", "fr"))
    }
}

Groovy

android {
    defaultConfig {
        ...
        resConfigs "en", "fr"
    }
}

Quando rilasci un'app utilizzando il formato Android App Bundle, per impostazione predefinita vengono scaricate solo le lingue configurate sul dispositivo di un utente al momento dell'installazione dell'app. Allo stesso modo, nel download sono incluse solo le risorse che corrispondono alla densità dello schermo del dispositivo e le librerie native che corrispondono all'ABI del dispositivo. Per ulteriori informazioni, consulta la configurazione del bundle di app Android.

Per le app legacy rilasciate con APK (create prima di agosto 2021), puoi personalizzare le risorse ABI o di densità dello schermo da includere nell'APK creando più APK ciascuno con un target di configurazione del dispositivo diverso.

Unisci le risorse duplicate

Per impostazione predefinita, Gradle unisce anche le risorse con lo stesso nome, ad esempio gli elementi drawable con lo stesso nome che potrebbero trovarsi in cartelle di risorse diverse. Questo comportamento non è controllato dalla proprietà shrinkResources e non può essere disattivato perché è necessario evitare errori quando più risorse corrispondono al nome cercato dal codice.

L'unione delle risorse avviene solo quando due o più file condividono lo stesso nome, tipo e qualificatore di risorsa. Gradle seleziona il file che ritiene essere la scelta migliore tra i duplicati (in base a un ordine di priorità descritto di seguito) e passa solo quella risorsa ad AAPT per la distribuzione nell'elemento finale.

Gradle cerca risorse duplicate nelle seguenti posizioni:

  • Le risorse principali, associate all'insieme di origini principale, generalmente situate in src/main/res/.
  • Le sovrapposizioni delle varianti, dal tipo di build e dai relativi gusti.
  • Le dipendenze del progetto di libreria.

Gradle unisce le risorse duplicate nel seguente ordine di priorità a cascata:

Dipendenze → Principale → Gusto di compilazione → Tipo di compilazione

Ad esempio, se una risorsa duplicata viene visualizzata sia nelle risorse principali sia in un flavor di build, Gradle seleziona quella nel flavor di build.

Se nello stesso set di origine vengono visualizzate risorse identiche, Gradle non può unificarle e genera un errore di unione delle risorse. Questo può accadere se definisci più set di origini nella proprietà sourceSet del file build.gradle.kts, ad esempio se sia src/main/res/ sia src/main/res2/ contengono risorse identiche.

Offuscare il codice

Lo scopo dell'offuscamento è ridurre le dimensioni dell'app accorciando i nomi di classi, metodi e campi dell'app. Di seguito è riportato un esempio di offuscamento utilizzando R8:

androidx.appcompat.app.ActionBarDrawerToggle$DelegateProvider -> a.a.a.b:
androidx.appcompat.app.AlertController -> androidx.appcompat.app.AlertController:
    android.content.Context mContext -> a
    int mListItemLayout -> O
    int mViewSpacingRight -> l
    android.widget.Button mButtonNeutral -> w
    int mMultiChoiceItemLayout -> M
    boolean mShowTitle -> P
    int mViewSpacingLeft -> j
    int mButtonPanelSideLayout -> K

Anche se l'offuscamento non rimuove il codice dall'app, si possono verificare risparmi significativi per le dimensioni nelle app con file DEX che indicizzano molti metodi, classi e campi. Tuttavia, poiché l'offuscamento rinomina parti diverse del codice, alcune attività, come l'ispezione delle tracce dello stack, richiedono strumenti aggiuntivi. Per comprendere la traccia dello stack dopo l'offuscamento, leggi la sezione su come decodificare un'analisi dello stack offuscata.

Inoltre, se il codice si basa su nomi prevedibili per i metodi e le classi dell'app, ad esempio quando utilizzi la riflessione, devi trattare queste firme come punti di contatto e specificare regole di conservazione per loro, come descritto nella sezione su come personalizzare il codice da conservare. Queste regole di mantenimento indicano a R8 di non solo conservare il codice nel file DEX finale dell'app, ma anche di mantenere la denominazione originale.

Decodificare un'analisi dello stack offuscata

Dopo che R8 ha offuscato il codice, è difficile comprendere una traccia dello stack (se non impossibile) perché i nomi di classi e metodi potrebbero essere stati modificati. Per ottenere la traccia dello stack originale, devi ripercorrere la traccia dello stack.

Ottimizzazione del codice

Per ottimizzare ulteriormente l'app, R8 ispeziona il codice a un livello più approfondito per rimuovere altro codice inutilizzato o, se possibile, riscriverlo per renderlo meno prolisso. Di seguito sono riportati alcuni esempi di queste ottimizzazioni:

  • Se il codice non utilizza mai il ramo else {} per una determinata istruzione if/else, R8 potrebbe rimuovere il codice per il ramo else {}.
  • Se il codice chiama un metodo solo in alcuni punti, R8 potrebbe rimuoverlo e inserirlo in linea nei pochi siti di chiamata.
  • Se R8 determina che una classe ha una sola sottoclasse univoca e non viene creata un'istanza della classe stessa (ad esempio, una classe base astratta utilizzata solo da una classe di implementazione concreta), R8 può combinare le due classi e rimuovere una classe dall'app.
  • Per saperne di più, leggi i post del blog sull'ottimizzazione R8 di Jake Wharton.

R8 non consente di disattivare o attivare ottimizzazioni distinte né di modificare il comportamento di un'ottimizzazione. In effetti, R8 ignora le regole di ProGuard che tentano di modificare le ottimizzazioni predefinite, ad esempio -optimizations e -optimizationpasses. Questa limitazione è importante perché, man mano che R8 continua a migliorare, il mantenimento di un comportamento standard per le ottimizzazioni consente al team di Android Studio di risolvere facilmente eventuali problemi che potresti riscontrare.

Tieni presente che l'attivazione dell'ottimizzazione modificherà le tracce dello stack per la tua applicazione. Ad esempio, l'inserimento in linea rimuoverà i frame dello stack. Consulta la sezione sul recupero per scoprire come ottenere le analisi dello stack originali.

Impatto sulle prestazioni di runtime

Se la riduzione, l'oscuramento e l'ottimizzazione sono tutti attivati, R8 migliorerà le prestazioni di runtime del codice (incluse l'avvio e il tempo di frame nel thread dell'interfaccia utente) fino al 30%. La disattivazione di una di queste opzioni limita drasticamente l'insieme di ottimizzazioni utilizzate da R8.

Se R8 è abilitato, devi anche creare profili di avvio per prestazioni di avvio ancora migliori.

Attivare le ottimizzazioni avanzate

R8 include un insieme di ottimizzazioni aggiuntive (chiamate "modalità completa") che fanno sì che si comporti in modo diverso da ProGuard. Queste ottimizzazioni sono attivate per impostazione predefinita dalla versione 8.0.0 del plug-in Android per Gradle.

Puoi disattivare queste ottimizzazioni aggiuntive includendo quanto segue nel file gradle.properties del progetto:

android.enableR8.fullMode=false

Poiché le ottimizzazioni aggiuntive fanno sì che R8 si comporti in modo diverso da ProGuard, potrebbe essere necessario includere regole ProGuard aggiuntive per evitare problemi di runtime, se utilizzi regole progettate per ProGuard. Ad esempio, supponiamo che il tuo codice faccia riferimento a una classe tramite l'API Java Reflection. Quando non utilizzi la "modalità completa", R8 presume che tu intenda esaminare e manipolare gli oggetti di quella classe in fase di runtime, anche se il codice non lo fa, e conserva automaticamente la classe e il relativo inizializzante statico.

Tuttavia, quando utilizzi la "modalità completa", R8 non fa questa supposizione e, se afferma che il codice non utilizza mai la classe in fase di esecuzione, rimuove la classe dal file DEX finale dell'app. In altre parole, se vuoi conservare la classe e il suo inizializzatore statico, devi includere una regola keep nel file delle regole.

Se riscontri problemi durante l'utilizzo della "modalità completa" di R8, consulta la pagina delle domande frequenti su R8 per una possibile soluzione. Se non riesci a risolvere il problema, segnala un bug.

Ripercorrere le analisi dello stack

Il codice elaborato da R8 viene modificato in vari modi che possono rendere più difficili da comprendere le tracce dello stack perché non corrispondono esattamente al codice sorgente. Questo può accadere per le modifiche ai numeri di riga quando le informazioni di debug non vengono conservate. Ciò può essere dovuto a ottimizzazioni come l'inserimento in linea e la creazione di schemi. Il contributo più importante è l'offuscamento, in cui anche le classi e i metodi cambieranno nome.

Per recuperare la traccia dello stack originale, R8 fornisce lo strumento a riga di comando retrace, incluso nel pacchetto degli strumenti a riga di comando.

Per supportare il ritracciamento delle analisi dello stack dell'applicazione, devi assicurarti che la build conservi informazioni sufficienti per il ritracciamento aggiungendo le seguenti regole al file proguard-rules.pro del modulo:

-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile

L'attributo LineNumberTable conserva le informazioni sulle posizioni nei metodi in modo che queste posizioni vengano stampate nelle tracce dello stack. L'attributo SourceFile garantisce che tutte le potenziali istanze di runtime stampino effettivamente le informazioni sulla posizione. La direttiva -renamesourcefileattribute imposta il nome del file di origine nelle tracce di stack su SourceFile. Il nome effettivo del file di origine originale non è richiesto per il ritracciamento, perché il file di mappatura contiene il file di origine originale.

Ogni volta che viene eseguito, R8 crea un file mapping.txt, che contiene le informazioni necessarie per mappare le analisi dello stack alle analisi dello stack originali. Android Studio salva il file nella directory<module-name>/build/outputs/mapping/<build-type>/.

Quando pubblichi la tua app su Google Play, puoi caricare il file mapping.txt per ogni versione dell'app. Se la pubblichi usando gli Android App Bundle, questo file viene incluso automaticamente nei contenuti dell'app bundle. Google Play ripercorre le analisi dello stack in arrivo dai problemi segnalati dagli utenti per poter esaminare le analisi in Play Console. Per ulteriori informazioni, consulta l'articolo del Centro assistenza su come deobfuscare le tracce dello stack degli arresti anomali.

Risolvere i problemi relativi a R8

Questa sezione descrive alcune strategie per la risoluzione dei problemi relativi all'attivazione di riduzione, offuscamento e ottimizzazione utilizzando R8. Se di seguito non trovi una soluzione al tuo problema, leggi anche la pagina delle domande frequenti su R8 e la guida alla risoluzione dei problemi di ProGuard.

Generare un report sul codice rimosso (o mantenuto)

Per aiutarti a risolvere determinati problemi di R8, può essere utile visualizzare un report di tutto il codice rimosso da R8 dalla tua app. Per ogni modulo per cui vuoi generare questo report, aggiungi -printusage <output-dir>/usage.txt al file delle regole personalizzate. Quando attivi R8 e compili l'app, R8 genera un report con il percorso e il nome del file specificati. Il report del codice rimosso è simile al seguente:

androidx.drawerlayout.R$attr
androidx.vectordrawable.R
androidx.appcompat.app.AppCompatDelegateImpl
    public void setSupportActionBar(androidx.appcompat.widget.Toolbar)
    public boolean hasWindowFeature(int)
    public void setHandleNativeActionModesEnabled(boolean)
    android.view.ViewGroup getSubDecor()
    public void setLocalNightMode(int)
    final androidx.appcompat.app.AppCompatDelegateImpl$AutoNightModeManager getAutoNightModeManager()
    public final androidx.appcompat.app.ActionBarDrawerToggle$Delegate getDrawerToggleDelegate()
    private static final boolean DEBUG
    private static final java.lang.String KEY_LOCAL_NIGHT_MODE
    static final java.lang.String EXCEPTION_HANDLER_MESSAGE_SUFFIX
...

Se invece vuoi visualizzare un report dei punti di contatto determinati da R8 dalle regole di conservazione del progetto , includi -printseeds <output-dir>/seeds.txt nel file delle regole personalizzate. Quando attivi R8 e compili l'app, R8 genera un report con il percorso e il nome del file specificati. Il report dei punti di ingresso conservati è simile al seguente:

com.example.myapplication.MainActivity
androidx.appcompat.R$layout: int abc_action_menu_item_layout
androidx.appcompat.R$attr: int activityChooserViewStyle
androidx.appcompat.R$styleable: int MenuItem_android_id
androidx.appcompat.R$styleable: int[] CoordinatorLayout_Layout
androidx.lifecycle.FullLifecycleObserverAdapter
...

Risolvere i problemi relativi al ridimensionamento delle risorse

Quando riduci le risorse, la finestra Build mostra un riepilogo delle risorse che vengono rimosse dall'app. Per visualizzare l'output di testo dettagliato di Gradle, devi prima fare clic su Attiva/disattiva visualizzazione sul lato sinistro della finestra. Ad esempio:

:android:shrinkDebugResources
Removed unused resources: Resource data reduced from 2570KB to 1711KB: Removed 33%
:android:validateDebugSigning

Gradle crea anche un file di diagnostica denominato resources.txt in <module-name>/build/outputs/mapping/release/ (la stessa directory dei file di output di ProGuard). Questo file include dettagli come quali risorse fanno riferimento ad altre risorse e quali risorse vengono utilizzate o rimosse.

Ad esempio, per scoprire perché @drawable/ic_plus_anim_016 è ancora presente nella tua app, apri il file resources.txt e cerca il nome del file. Potresti scoprire che viene fatto riferimento a un'altra risorsa, come segue:

16:25:48.005 [QUIET] [system.out] @drawable/add_schedule_fab_icon_anim : reachable=true
16:25:48.009 [QUIET] [system.out]     @drawable/ic_plus_anim_016

Ora devi sapere perché @drawable/add_schedule_fab_icon_anim è raggiungibile e se cerchi verso l'alto vedrai che la risorsa è elencata in "Le risorse raggiungibili principali sono:". Ciò significa che esiste un riferimento a add_schedule_fab_icon_anim (ovvero il relativo ID R.drawable è stato trovato nel codice raggiungibile).

Se non utilizzi il controllo rigoroso, gli ID risorsa possono essere contrassegnati come raggiungibili se sono presenti costanti di stringa che sembrano poter essere utilizzate per creare i nomi delle risorse per le risorse caricate dinamicamente. In questo caso, se cerchi il nome della risorsa nell'output della compilazione, potresti trovare un messaggio simile al seguente:

10:32:50.590 [QUIET] [system.out] Marking drawable:ic_plus_anim_016:2130837506
    used because it format-string matches string pool constant ic_plus_anim_%1$d.

Se vedi una di queste stringhe e hai la certezza che la stringa non venga utilizzata per caricare dinamicamente la risorsa specificata, puoi utilizzare l'attributo tools:discard per indicare al sistema di build di rimuoverla, come descritto nella sezione su come personalizzare le risorse da conservare.