Ottimizzazione per gli autori delle librerie

In qualità di autore di una libreria, devi assicurarti che gli sviluppatori di app possano incorporare facilmente la tua libreria nella loro app mantenendo un'esperienza utente finale di alta qualità. Ciò significa che la tua libreria deve essere compatibile con l'ottimizzazione di Android (R8) senza richiedere una configurazione aggiuntiva da parte dello sviluppatore oppure devi documentare che la libreria potrebbe non essere adatta all'uso su Android. È fondamentale che le librerie destinate all'uso su Android non impediscano importanti ottimizzazioni delle app e rispettino i requisiti di ottimizzazione aggiuntivi.

Questa documentazione è rivolta agli sviluppatori di librerie pubblicate, ma potrebbe essere utile anche per gli sviluppatori di moduli di libreria interni in un'app di grandi dimensioni e modularizzata.

Se sei uno sviluppatore di app e vuoi scoprire come ottimizzare la tua app per Android, consulta Attivare l'ottimizzazione delle app. Per scoprire quali librerie sono adatte all'uso, consulta Scegliere le librerie con attenzione.

Comprendere i tipi di regole di conservazione

Esistono due tipi distinti di regole di conservazione che puoi avere nelle librerie:

  • Le regole di conservazione del consumatore devono specificare le regole che conservano ciò che la libreria riflette. Se una libreria utilizza la reflection o JNI per chiamare il proprio codice o il codice definito da un'app client, queste regole devono descrivere il codice da conservare. Le librerie devono pacchettizzare le regole di conservazione del consumatore, che utilizzano lo stesso formato delle regole di conservazione delle app. Queste regole vengono raggruppate negli artefatti della libreria (AAR o JAR) e vengono utilizzate automaticamente durante l'ottimizzazione delle app per Android quando viene utilizzata la libreria. Queste regole vengono mantenute nel file specificato con la consumerProguardFiles proprietà nel tuo build.gradle.kts (o build.gradle) file. Per saperne di più, consulta Scrivere regole di conservazione del consumatore.
  • Le regole di conservazione della build della libreria vengono applicate quando viene creata la libreria. Sono necessarie solo se decidi di ottimizzare parzialmente la libreria in fase di build. Devono impedire la rimozione dell'API pubblica della libreria, altrimenti l'API pubblica non sarà presente nella distribuzione della libreria, il che significa che gli sviluppatori di app non potranno utilizzare la libreria. Queste regole vengono mantenute nel file specificato con la proguardFiles proprietà nel tuo build.gradle.kts (o build.gradle) file. Per saperne di più, consulta Ottimizzare la build della libreria AAR.

Requisiti e linee guida per l'ottimizzazione

La configurazione di R8 nelle librerie ha un impatto globale sulle dimensioni e sulle prestazioni del file binario finale dell'app che la utilizza. Oltre alle best practice generali per le regole di conservazione, gli autori delle librerie devono rispettare requisiti specifici e tenere in considerazione linee guida aggiuntive.

Rispettare i requisiti di ottimizzazione

L'inefficienza delle librerie contribuisce in modo significativo all'aumento delle dimensioni delle app, allo spreco di memoria, all'avvio lento e agli errori L'applicazione non risponde (ANR). Le librerie devono evitare di violare i seguenti requisiti per non ridurre in modo significativo la qualità delle app e l'esperienza utente.

  • Nessuna regola di conservazione ampia o a livello di pacchetto: la libreria non deve includere regole di conservazione ampie che conservano la maggior parte del codice nella libreria o in un'altra libreria. Le regole di conservazione ampie potrebbero risolvere i blocchi a breve termine, ma aumentano le dimensioni dell'app di tutte le app che utilizzano la tua libreria.

    Non includere regole di conservazione a livello di pacchetto (ad esempio -keep class com.mylibrary.** {*; }) per i pacchetti nella libreria o in altre librerie a cui viene fatto riferimento. Queste regole limitano l'ottimizzazione di questi pacchetti in tutte le app che utilizzano la tua libreria.

  • Nessuna regola globale inappropriata: non utilizzare mai opzioni globali come -dontobfuscate o -allowaccessmodification.

  • Utilizzare la generazione di codice anziché la reflection, quando possibile: quando possibile, utilizza la generazione di codice (codegen) anziché la reflection. La generazione di codice e la reflection sono entrambi approcci comuni per evitare il codice boilerplate durante la programmazione, ma la generazione di codice è più compatibile con un ottimizzatore di app come R8.

    Con la generazione di codice, il codice viene analizzato e modificato durante il processo di compilazione. Poiché non vengono apportate modifiche importanti dopo il tempo di compilazione, l'ottimizzatore sa quale codice è necessario e quale può essere rimosso in sicurezza.

    Con la reflection, il codice viene analizzato e manipolato in fase di runtime. Poiché il codice non viene finalizzato fino all'esecuzione, l'ottimizzatore non sa quale codice può essere rimosso in sicurezza. È probabile che rimuova il codice utilizzato dinamicamente tramite reflection durante il runtime, causando blocchi delle app per gli utenti.

    Molte librerie moderne utilizzano la generazione di codice anziché la reflection. Consulta KSP per un punto di ingresso comune, utilizzato da Room, Dagger2 e molti altri.

  • Supportare la modalità completa di R8: la libreria non deve bloccarsi quando è attivata la modalità completa di R8. La modalità completa di R8 è la modalità consigliata per utilizzare R8 ed è la modalità predefinita a partire da AGP 8.0, che è diventato stabile nel 2023. Se la libreria si blocca in R8, la soluzione consiste nell'identificare il punto di ingresso specifico della reflection o di JNI e aggiungere una regola mirata, anziché conservare l'intero pacchetto.

Altri consigli

Oltre ai requisiti di ottimizzazione, di seguito sono riportati altri consigli.

  • Non utilizzare -repackageclasses nel file delle regole di conservazione del consumatore della libreria. Tuttavia, per ottimizzare la build della libreria, puoi utilizzare -repackageclasses con un nome di pacchetto interno, ad esempio <your.library.package>.internal, in nel file delle regole di conservazione della build della libreria. In questo modo puoi migliorare l'efficienza della libreria nelle app non ottimizzate. Tuttavia, in genere non è necessario, perché anche le app devono essere ottimizzate.
  • Dichiara tutti gli attributi necessari per il funzionamento della libreria nei file delle regole di conservazione della libreria, anche se potrebbe esserci una sovrapposizione con gli attributi definiti in proguard-android-optimize.txt.
  • Se nella distribuzione della libreria sono necessari i seguenti attributi, mantienili nel file delle regole di conservazione della build della libreria e non nel file delle regole di conservazione del consumatore della libreria:
    • AnnotationDefault
    • EnclosingMethod
    • Exceptions
    • InnerClasses
    • RuntimeInvisibleAnnotations
    • RuntimeInvisibleParameterAnnotations
    • RuntimeInvisibleTypeAnnotations
    • RuntimeVisibleAnnotations
    • RuntimeVisibleParameterAnnotations
    • RuntimeVisibleTypeAnnotations
    • Signature
  • Gli autori delle librerie devono mantenere l'attributo RuntimeVisibleAnnotations nelle regole di conservazione del consumatore se le annotazioni vengono utilizzate in fase di runtime.
  • Gli autori delle librerie non devono utilizzare le seguenti opzioni globali nelle regole di conservazione del consumatore:
    • -include
    • -basedirectory
    • -injars
    • -outjars
    • -libraryjars
    • -repackageclasses
    • -flattenpackagehierarchy
    • -allowaccessmodification
    • -renamesourcefileattribute
    • -ignorewarnings
    • -addconfigurationdebugging
    • -printconfiguration
    • -printmapping
    • -printusage
    • -printseeds
    • -applymapping
    • -obfuscationdictionary
    • -classobfuscationdictionary
    • -packageobfuscationdictionary

Quando la reflection è accettabile

Se devi utilizzare la reflection, devi riflettere solo su una delle seguenti opzioni:

  • Tipi mirati specifici (implementatori di interfacce o sottoclassi specifici)
  • Codice che utilizza un'annotazione di runtime specifica

L'utilizzo della reflection in questo modo limita il costo di runtime e consente di scrivere regole di conservazione del consumatore mirate.

Questa forma specifica e mirata di reflection è un pattern che puoi vedere in entrambi i framework Android (ad esempio, quando espandi attività, visualizzazioni e drawables) e nelle librerie AndroidX (ad esempio, quando crei WorkManager ListenableWorkers, o RoomDatabases). Al contrario, la reflection aperta di Gson non è adatta all'uso nelle app Android.

Errori comuni

Alcuni errori comuni potrebbero portarti a configurare R8 in modo errato. Questi includono:

  • Comprensione errata delle ottimizzazioni di R8: contrariamente a quanto si pensa, le ottimizzazioni di R8 non si limitano alla sola offuscamento, ma includono anche la riduzione del codice e le ottimizzazioni logiche con tecniche di inlining dei metodi e unione delle classi. Per saperne di più, consulta Panoramica dell'ottimizzazione di R8.

  • Ignorare l'ottimizzazione delle librerie offuscate: un errore comune è omettere una libreria dall'ottimizzazione, perché la libreria è stata ottimizzata o offuscata durante la compilazione in un AAR (Android Archive) o JAR (Java Archive). Le ottimizzazioni durante il tempo di compilazione della libreria sono limitate e l'app non deve disattivare l'ottimizzazione della libreria includendola in una regola di conservazione. Per saperne di più, consulta Ottimizzare la build della libreria AAR.

  • Comprensione errata dell'opzione -keep La regola -keep impedisce a R8 di eseguire uno qualsiasi dei passaggi di ottimizzazione. Per saperne di più, consulta Scegliere l'opzione di conservazione corretta.

Configurare il packaging delle regole

Per assicurarti che le regole di conservazione del consumatore vengano applicate correttamente, devi pacchettizzarle in modo appropriato a seconda del formato della libreria.

Librerie AAR

Per aggiungere regole del consumatore per una libreria AAR, utilizza l'opzione consumerProguardFiles nello script di build del modulo della libreria Android. Per saperne di più, consulta le nostre indicazioni sulla creazione di moduli di libreria.

Kotlin

android {
    defaultConfig {
        consumerProguardFiles("consumer-proguard-rules.pro")
    }
    ...
}

Alla moda

android {
    defaultConfig {
        consumerProguardFiles 'consumer-proguard-rules.pro'
    }
    ...
}

Librerie JAR

Per raggruppare le regole con la libreria Kotlin o Java fornita come JAR, inserisci il file delle regole nella directory META-INF/proguard/ del JAR finale, con qualsiasi nome file. Ad esempio, se il codice si trova in <libraryroot>/src/main/kotlin, inserisci un file di regole del consumatore in <libraryroot>/src/main/resources/META-INF/proguard/consumer-proguard-rules.pro e le regole verranno raggruppate nella posizione corretta nel JAR di output.

Verifica che il JAR finale raggruppi correttamente le regole controllando che si trovino nella directory META-INF/proguard.

Ottimizzare la build della libreria AAR (avanzato)

In genere, non è necessario ottimizzare direttamente la build di una libreria perché le possibili ottimizzazioni in fase di tempo di compilazione della libreria sono molto limitate. In qualità di sviluppatore di librerie, devi ragionare su più fasi di ottimizzazione e mantenere il comportamento, sia in fase di tempo di compilazione della libreria che dell'app, prima di ottimizzare la libreria.

Se vuoi comunque ottimizzare la libreria in fase di compilazione, questa operazione è supportata dal plug-in Android per Gradle.

Kotlin

android {
    buildTypes {
        release {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
        configureEach {
            consumerProguardFiles("consumer-rules.pro")
        }
    }
}

Alla moda

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles
                getDefaultProguardFile('proguard-android-optimize.txt'),
                'proguard-rules.pro'
        }
        configureEach {
            consumerProguardFiles "consumer-rules.pro"
        }
    }
}

Tieni presente che il comportamento di proguardFiles è molto diverso da consumerProguardFiles:

  • proguardFiles vengono utilizzati in tempo di compilazione, spesso insieme a getDefaultProguardFile("proguard-android-optimize.txt"), per definire quale parte della libreria deve essere conservata durante la build della libreria. Come minimo, questa è la tua API pubblica.
  • consumerProguardFiles , al contrario, vengono pacchettizzati nella libreria per influenzare le ottimizzazioni che avvengono in un secondo momento, durante la build di un'app che utilizza la tua libreria.

Ad esempio, se la libreria utilizza la reflection per creare classi interne, potrebbe essere necessario definire le regole di conservazione sia in proguardFiles sia in consumerProguardFiles.

Se utilizzi -repackageclasses nella build della libreria, ricrea i pacchetti delle classi in un sottopacchetto all'interno del pacchetto della libreria. Ad esempio, utilizza -repackageclasses 'com.example.mylibrary.internal' anziché -repackageclasses 'internal'.

Supportare versioni diverse di R8 (avanzato)

Puoi personalizzare le regole in modo che abbiano come target versioni specifiche di R8. In questo modo, la libreria funziona in modo ottimale nei progetti che utilizzano versioni più recenti di R8, consentendo al contempo di continuare a utilizzare le regole esistenti nei progetti con versioni precedenti di R8.

Per specificare le regole R8 mirate, devi includerle nella directory META-INF/com.android.tools all'interno di classes.jar di un AAR o nella directory META-INF/com.android.tools di un JAR.

In an AAR library:
    proguard.txt (legacy location, the file name must be "proguard.txt")
    classes.jar
    └── META-INF
        └── com.android.tools (location of targeted R8 rules)
            ├── r8-from-<X>-upto-<Y>/<R8-rule-files>
            └── ... (more directories with the same name format)

In a JAR library:
    META-INF
    ├── proguard/<ProGuard-rule-files> (legacy location)
    └── com.android.tools (location of targeted R8 rules)
        ├── r8-from-<X>-upto-<Y>/<R8-rule-files>
        └── ... (more directories with the same name format)

Nella directory META-INF/com.android.tools possono essere presenti più sottodirectory con nomi nel formato r8-from-<X>-upto-<Y> per indicare le versioni di R8 per cui sono scritte le regole. Ogni sottodirectory può contenere uno o più file contenenti le regole R8, con qualsiasi nome file ed estensione.

Tieni presente che le parti -from-<X> e -upto-<Y> sono facoltative, la versione <Y> è esclusiva e gli intervalli di versioni sono in genere continui, ma possono anche sovrapporsi.

Ad esempio, r8, r8-upto-8.0.0, r8-from-8.0.0-upto-8.2.0 e r8-from-8.2.0 sono nomi di directory che rappresentano un insieme di regole R8 mirate. Le regole nella directory r8 possono essere utilizzate da qualsiasi versione di R8. Le regole nella directory r8-from-8.0.0-upto-8.2.0 possono essere utilizzate da R8 dalla versione 8.0.0 fino alla versione 8.2.0 esclusa.

Il plug-in Android per Gradle utilizza queste informazioni per selezionare tutte le regole che possono essere utilizzate dalla versione corrente di R8. Se una libreria non specifica regole R8 mirate, il plug-in Android per Gradle selezionerà le regole dalle posizioni legacy (proguard.txt per un AAR o META-INF/proguard/<ProGuard-rule-files> per un JAR).