Priorità all'efficienza della memoria: passaggi essenziali per Android 17
Lettura di 10 minuti
Sebbene le prestazioni delle app siano spesso associate a un'interfaccia utente fluida e a tempi di avvio rapidi, la memoria funge da base silenziosa su cui si basano queste metriche visibili. Non è un segreto che stiamo assistendo a un cambiamento in cui la memoria del dispositivo è più importante che mai. Non solo abbiamo fatto passi avanti nell'ottimizzazione della memoria di Android con Android 17, ma forniamo anche gli strumenti e il supporto API per aiutarti a rispettare i requisiti di memoria più rigorosi entro la fine dell'anno.
Per garantire la stabilità del dispositivo, a partire da Android 17, il sistema inizierà a applicare limiti di memoria alle app in base alla RAM totale del dispositivo. Se un'app supera questi limiti, Android interromperà il processo senza alcuna analisi dello stack associata.
Oltre a queste chiusure forzate, l'utilizzo non ottimizzato della memoria utilizzata peggiora inevitabilmente l'esperienza utente. Quando l'app si avvicina ai limiti di memoria heap, attiva una garbage collection frequente, che porta a notevoli interruzioni dell'interfaccia utente. Inoltre, quando un dispositivo esaurisce la memoria disponibile, il sistema si affretta a recuperare le pagine, causando un carico della CPU, una latenza dell'interfaccia utente e un consumo eccessivo della batteria. Se la carenza di memoria è troppo grave, può causare eventi di interruzione per memoria insufficiente (LMK) che terminano bruscamente i processi in background e forzano le app ad avere avvii a freddo lenti e a perdere lo stato dell'utente.
Per creare app ad alto rendimento ed evitare questi arresti forzati, ti consigliamo di adottare le seguenti strategie di ottimizzazione della memoria:
- Massimizzare l'ottimizzazione del bytecode con R8
- Ottimizzare il caricamento delle immagini
- Rilevare e correggere le perdite di memoria con Android Studio
- Taglia la memoria quando l'app esce dallo stato visibile
- Osservabilità avanzata della memoria con ProfilingManager
Una versione ridotta di questo post del blog è disponibile anche in formato video. Dai un'occhiata.
Informazioni sui limiti di memoria delle app per Android 17
In Android 17 vengono introdotti limiti di memoria per le app per impedire che un malintenzionato comprometta l'esperienza di multitasking e la stabilità dell'intero dispositivo dell'utente.
Di seguito è riportata una suddivisione dei motivi che hanno portato a questa modifica dell'architettura:
- Prevenzione di interruzioni a cascata: quando un'app diventa troppo grande o perde memoria mentre mantiene uno stato privilegiato (ad es. esegue un servizio in primo piano), inizialmente è protetta da Low Memory Killer (LMK) del sistema. Man mano che questa singola app cresce senza controllo e accumula RAM, LMK è costretto a compensare chiudendo decine di app più piccole e ben funzionanti memorizzate nella cache e job in background per recuperare spazio per l'app che consuma molta memoria.
- Preservare il multitasking e lo stato utente:quando il sistema è costretto a eliminare le app memorizzate nella cache per ospitare un singolo processo con perdite, l'esperienza di multitasking viene notevolmente compromessa. Gli utenti che tornano alle applicazioni memorizzate nella cache precedenti riscontrano avvii completi lenti anziché riprese rapide quasi istantanee. Questa inefficienza genera un maggiore carico della CPU e accelera l'esaurimento della batteria. Può anche distruggere il contesto dell'utente nelle app utilizzate di recente, ad esempio le posizioni di scorrimento, gli stack di navigazione e i progressi di gioco.
Per determinare se la sessione dell'app è stata interessata da questi vincoli sul campo, puoi chiamare getDescription() all'interno di ApplicationExitInfo. Se il sistema ha applicato un limite, il motivo dell'uscita viene segnalato come REASON_OTHER e la stringa di descrizione conterrà "MemoryLimiter:AnonSwap". Puoi anche sfruttare la profilazione basata su trigger utilizzando TRIGGER_TYPE_ANOMALY per acquisire automaticamente i dump dell'heap quando viene raggiunto il limite di memoria. Inoltre, Android sta lavorando attivamente per mostrare agli sviluppatori altre metriche di memoria in campo all'interno di Google Play Console.
Abbiamo anche ampliato la nostra documentazione sui limiti di memoria per includere i comandi di debug locali, consentendoti di simulare i vincoli di memoria nel tuo ambiente locale e di convalidare il comportamento della tua applicazione in caso di applicazione di limiti di memoria.
Massimizzare l'ottimizzazione del bytecode con R8
Un modo molto efficace per ridurre il footprint della memoria dell'app è attivare l'ottimizzatore R8. Riducendo le classi, i metodi e i campi in nomi più brevi ed eliminando le risorse e il codice inutilizzati, R8 riduce significativamente il footprint della memoria dell'app riducendo al minimo la quantità di codice residente richiesta durante l'esecuzione.
R8 riduce al minimo il codice residente, riducendo il footprint della memoria e abbassando il rischio di interruzione di LMK. Ciò comporta avvii tiepidi più frequenti rispetto agli avvii a freddo lenti. Inoltre, il bytecode semplificato riduce l'overhead della CPU del thread principale, riducendo direttamente i tassi di ANR per un'esperienza utente più fluida. Ad esempio, la banca digitale Monzo ha abilitato l'ottimizzazione R8 completa e ha registrato una riduzione del 35% della percentuale di ANR, un miglioramento del 30% del tasso di avvio a freddo e una riduzione del 9% delle dimensioni complessive dell'app.
Per configurare correttamente R8 nel file build.gradle:
- Imposta
isShrinkResources = trueeisMinifyEnabled = true. - Utilizza
proguard-android-optimize.txtanziché il vecchioproguard-android.txt, che impedisce le ottimizzazioni e non è più supportato nel plug-in Android per Gradle 9. - Rimuovi
android.enableR8.fullMode = falsedagradle.properties.
Se utilizzi la reflection nella tua codebase, aggiungi regole Keep per impedire a R8 di ottimizzare queste parti del codice. Assicurati di definire l'ambito delle regole di conservazione in modo restrittivo per ottenere la massima ottimizzazione.
Per ottenere la massima ottimizzazione, assicurati di seguire queste best practice nel file delle regole di conservazione.
- Rimuovi le opzioni globali come
-dontoptimize,-dontshrinke-dontobfuscateche impediscono a R8 di ottimizzare l'intero codebase - Rimuovi le regole di conservazione che impediscono l'ottimizzazione dei componenti Android come attività, servizi, visualizzazioni o ricevitori di trasmissione.
- Per scegliere come target solo classi o metodi specifici, perfeziona le regole di conservazione generali a livello di pacchetto.
Per ulteriori best practice, consulta la nostra documentazione sulle regole di conservazione.
Best practice per gli sviluppatori di librerie R8
Se sei uno sviluppatore di librerie, inserisci rigorosamente le regole di cui i tuoi consumatori hanno bisogno nel file consumer-rules file e mantieni le regole di protezione interne della libreria nel file proguard-rules.pro. Per ulteriori informazioni su come ottimizzare le librerie, consulta la sezione Ottimizzazione per gli autori di librerie.
R8 Configuration Analyzer
Per controllare l'ottimizzazione R8, utilizza lo Strumento di analisi della configurazione. Configuration Analyzer mostra lo stato attuale dell'ottimizzazione con i punteggi di offuscamento, ottimizzazione e riduzione. Con l'analizzatore della configurazione, puoi anche capire quante classi, metodi o campi vengono esclusi dall'ottimizzazione da ogni regola di conservazione. Per ottenere la massima ottimizzazione, perfeziona queste regole di conservazione generali a livello di pacchetto.
Utilizzando l'analizzatore della configurazione, puoi anche identificare le regole di conservazione che includono altre regole di conservazione, le regole di conservazione ridondanti e le regole di conservazione inutilizzate.
Skill dell'agente R8
Puoi anche utilizzare la skill dell'agente R8 con l'agente Android Studio o altri strumenti di AI per risolvere errori di configurazione e perfezionare le regole, migliorando così le prestazioni dell'app. (gli approfondimenti delle competenze basate sull'AI richiedono una verifica tecnica)
Ottimizzare il caricamento delle immagini
Le bitmap sono in genere gli oggetti comuni più grandi che risiedono nella memoria dell'app. Rappresentano la fase finale del processo di caricamento delle immagini, in cui i file compressi, come JPEG o PNG, vengono decodificati in dati pixel non elaborati per la visualizzazione. Ciò significa che una piccola immagine compressa da 100 KB può occupare diversi megabyte di RAM perché il consumo di memoria è determinato dalle dimensioni in pixel e dalla profondità del colore dell'immagine. Poiché le operazioni bitmap si trovano spesso nel percorso critico per il disegno dei frame, le immagini non ottimizzate causano un grave aumento della memoria e un'interruzione dell'interfaccia utente.
Google consiglia di utilizzare le librerie di caricamento delle immagini Coil per i progetti Kotlin-first, in particolare quando si sviluppa con Jetpack Compose e Glide per le applicazioni basate su Java.
Adotta queste cinque best practice
- Ridurre la risoluzione delle immagini:se carichi bitmap manualmente, evita di caricare un'immagine enorme in una visualizzazione miniatura minuscola; utilizza inSampleSize per caricare una versione più piccola. Glide e Coil eseguono il downsampling delle immagini per impostazione predefinita e puoi configurare questa strategia di downsampling utilizzando rispettivamente DownsampleStrategy e ImageLoader.
- Ritaglio : evita di incorporare il padding direttamente in un file immagine per scopi di letterbox (ad esempio, la creazione di un bordo trasparente per espandere le dimensioni di un'immagine). Anziché incorporare questi bordi, utilizza InsetDrawable o applica il padding direttamente all'interno della visualizzazione o del composable contenente la bitmap.
- Configurazione:bilancia memoria e qualità scegliendo il formato pixel giusto. Utilizza
RGB_565quando non è necessaria la trasparenza, che utilizza la metà della memoria del formatoARGB_8888predefinito. In Glide puoi configurare questa impostazione utilizzando DecodeFormat e in Coil puoi utilizzare la proprietà bitmapConfig. - Dai la priorità ai drawables vettoriali:per gli asset geometrici di base, utilizza ShapeDrawable come alternativa leggera alla decodifica delle bitmap rasterizzate. Definendo questi asset una sola volta tramite XML, ti assicuri che vengano scalati senza problemi in tutte le densità di visualizzazione, eliminando efficacemente l'aumento della memoria basato sulle risorse.
- Riutilizzo:se la tua applicazione gestisce le bitmap manualmente, per ridurre al minimo la saturazione della memoria, quando una bitmap non è più necessaria, l'app deve chiamare
bitmap.recycle()ed eliminare immediatamente il riferimentoBitmap. Se utilizzi una libreria di caricamento delle immagini come Glide o Coil, restituisci la bitmap al pool gestito della libreria. Fornendo un buffer esistente per le esigenze di memoria future, il pool evita efficacemente l'overhead delle nuove allocazioni.
Per saperne di più, consulta la nostra documentazione sull'ottimizzazione delle prestazioni per le immagini.
Strumenti di Android Studio
Puoi anche eliminare le bitmap ridondanti utilizzando Android Studio Narwhal 4. Ecco come trovarli in cinque semplici passaggi:
- Apri la scheda Profiler in Android Studio
- Fai clic su Dump dell'heap (o "Analizza utilizzo memoria") e premi Registra per acquisire un'istantanea dello stato attuale della memoria dell'app.
- Scansiona i risultati dell'analisi per individuare il triangolo giallo di avviso ⚠️, che Android Studio utilizza per segnalare le bitmap duplicate memorizzate più volte. In alternativa, vai all'intestazione del Profiler, scegli "Filtra per" e seleziona l'impostazione "Bitmap duplicati".
- Fai clic su una voce segnalata per aprire il riquadro Anteprima bitmap, che ti consente di vedere esattamente quale immagine è la recidiva.
- Utilizza questa conferma visiva per individuare la logica di caricamento ridondante nel codice e implementare una strategia di memorizzazione nella cache migliore.
Rilevare e correggere le perdite di memoria con Android Studio
Le perdite di memoria in Android si verificano quando il codice mantiene il riferimento di un oggetto molto tempo dopo la fine del suo ciclo di vita. In questo modo, il Garbage Collector (GC) non può recuperare la memoria, il che alla fine porta a prestazioni lente o a un errore OutOfMemoryError (OOM).
Android Studio Panda 3 include un'attività di profilazione LeakCanary dedicata, che consente agli sviluppatori di analizzare le perdite di memoria in tempo reale e mappare le tracce direttamente all'interno dell'IDE.
L'attività Profiler LeakCanary in Android Studio sposta attivamente l'analisi della perdita di memoria dal dispositivo alla macchina di sviluppo, con un conseguente aumento significativo delle prestazioni durante la fase di analisi della perdita rispetto all'analisi della perdita sul dispositivo.
Inoltre, l'analisi delle perdite è ora contestualizzata all'interno dell'IDE e completamente integrata con il codice sorgente, fornendo funzionalità come Vai alla dichiarazione e altre connessioni di codice utili che riducono drasticamente l'attrito e il tempo necessario per esaminare e correggere le perdite di memoria.
Esempi di perdite di memoria comuni
Le perdite di memoria si verificano quando un oggetto persiste in memoria oltre la sua durata prevista. Ciò si verifica in genere a causa di:
- Mantenere i riferimenti a Fragment, Activity o View che non sono più in uso.
- Gestione errata dei riferimenti al contesto.
- Mancata annullamento della registrazione di osservatori, listener e ricevitori.
- Creazione di riferimenti statici a oggetti associati a componenti con cicli di vita più brevi.
Ecco alcuni scenari di esempio:
| Scenario | Esempio basato su Compose | Esempio basato sulle visualizzazioni |
| Perdita di contesto | Esempio: Correzione: | Esempio: Correzione: |
| Leaking Listeners | Esempio: Correzione: | Esempio: Correzione: |
| Visualizzazioni trapelate | Esempio:
| Esempio: Correzione: |
Taglia la memoria quando l'app esce dallo stato visibile
Android può recuperare memoria dalla tua app o arrestarla completamente, se necessario, per liberare memoria per le attività critiche, come spiegato in Panoramica della gestione della memoria. Android in genere recupera la memoria dalla tua app quando non è visibile all'utente, ad esempio eliminando alcune pagine di codice e dati dell'app in memoria o comprimendo le allocazioni dell'heap. Quando l'utente riprende l'app e questa tenta di accedere a una memoria recuperata, il sistema operativo la ripristina su richiesta. Questo comportamento di swapping può essere lento e causare jank o interruzioni impreviste nell'app.
Se lasci che sia il sistema operativo a decidere quale memoria recuperare dalla tua app, potresti scoprire che il sistema operativo ha recuperato la memoria di cui avrai bisogno poco dopo aver ripreso l'app. In alternativa, la tua app può eliminare volontariamente le allocazioni di memoria che può rigenerare in un secondo momento, su richiesta e a un costo ridotto. Per farlo, puoi implementare l'interfaccia ComponentCallbacks2. Puoi implementare onTrimMemory in Activity, Fragment, Service o anche nella tua classe Application personalizzata. L'utilizzo nella classe Application è molto efficace per la gestione della cache globale.
Il metodo di callback onTrimMemory() fornito notifica alla tua app gli eventi relativi al ciclo di vita o alla memoria che rappresentano una buona opportunità per la tua app di ridurre volontariamente l'utilizzo della memoria.
In termini di gestione del ciclo di vita della memoria, l'implementazione deve concentrarsi esclusivamente su TRIM_MEMORY_UI_HIDDEN e TRIM_MEMORY_BACKGROUND. A partire da Android 14, il sistema ha smesso di inviare notifiche per altre costanti legacy, che sono state ritirate ufficialmente in Android 15.
TRIM_MEMORY_UI_HIDDEN: questo indicatore segnala che l'interfaccia utente della tua applicazione non è più visibile all'utente. Ciò offre l'opportunità di rilasciare allocazioni di memoria sostanziali legate strettamente all'interfaccia, come bitmap, buffer di riproduzione video o risorse di animazione complesse.
TRIM_MEMORY_BACKGROUND: a questo livello, il processo risiede in background ed è ora un candidato per la terminazione per soddisfare le esigenze di memoria globali del sistema. Per estendere la durata dello stato memorizzato nella cache del processo e ridurre il numero di avvii a freddo dell'app, devi rilasciare in modo aggressivo tutte le risorse che possono essere facilmente ricostruite una volta che l'utente riprende la sessione.
import android.content.ComponentCallbacks2 // Other import statements. class MainActivity : AppCompatActivity(), ComponentCallbacks2 { /** * Release memory when the UI becomes hidden or when system resources become low. * @param level the memory-related event that is raised. */ override fun onTrimMemory(level: Int) { if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { // Release memory related to UI elements, such as bitmap caches. } if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { // Release memory related to background processing, such as by // closing a database connection. } } }
Nota: l'integrazione di onTrimMemory potrebbe dipendere dal supporto dell'SDK. Ad esempio, alcuni giochi si basano sul motore grafico per attivare questa funzionalità. Consulta i documenti sull'ottimizzazione della memoria di gioco.
Osservabilità avanzata della memoria con ProfilingManager
Per rilevare e diagnosticare problemi di memoria sul campo che non possono essere riprodotti localmente, devi utilizzare l'API ProfilingManager. Introdotta in Android 15, questa API di osservabilità avanzata ti consente di raccogliere in modo programmatico i profili Perfetto degli utenti reali.
Per i team che non dispongono di un'infrastruttura dedicata per gestire e ospitare gli artefatti delle prestazioni, Crashlytics sta esplorando una soluzione specializzata per semplificare questo flusso di lavoro. Invitano gli sviluppatori a fornire feedback.
Android 17 introduce nuovi trigger basati su eventi, in particolare TRIGGER_TYPE_OOM e TRIGGER_TYPE_ANOMALY:
- Il trigger OOM raccoglie automaticamente un dump dell'heap Java nel momento esatto in cui si verifica un arresto anomalo OutOfMemoryError, fornendo stati di allocazione precisi. Un profilo OOM raccolto viene fornito al successivo avvio dell'app e alla registrazione del callback
registerForAllProfilingResults. - Il trigger anomalia rileva gravi problemi di prestazioni, come spam eccessivo del raccoglitore o superamento delle soglie di memoria. L'anomalia di memoria genera un dump dell'heap poco prima che il sistema termini l'app.
val profilingManager = applicationContext.getSystemService(ProfilingManager::class.java) val triggers = ArrayList<ProfilingTrigger>() triggers.add(ProfilingTrigger.Builder( ProfilingTrigger.TRIGGER_TYPE_ANOMALY)) val mainExecutor: Executor = Executors.newSingleThreadExecutor() val resultCallback = Consumer<ProfilingResult> { profilingResult -> if (profilingResult.errorCode != ProfilingResult.ERROR_NONE) { // upload profile result to server for further analysis setupProfileUploadWorker(profilingResult.resultFilePath) } profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback) profilingManager.addProfilingTriggers(triggers)
Una volta raccolto il dump dell'heap, puoi scaricare il profilo dal server o localmente tramite ADB pull e trascinare il file nell'interfaccia utente di Perfetto. Per semplificare il flusso di lavoro di debug della memoria, utilizza Heap Dump Explorer, la nuova visualizzazione predefinita per i dump dell'heap nell'interfaccia utente di Perfetto. Questo strumento fornisce un'interfaccia intuitiva per l'ispezione dei dump dell'heap Java, consentendoti di visualizzare le gerarchie di allocazione degli oggetti, calcolare le dimensioni della memoria mantenuta e identificare il percorso più breve dalla radice della garbage collection. Sfruttando Heap Dump Explorer, puoi individuare rapidamente perdite di memoria, oggetti mantenuti gonfi come allocazioni bitmap eccessive e analizzare le allocazioni di oggetti heap in un unico posto.
Conclusione
L'ottimizzazione del bytecode con R8, l'adozione delle best practice per il caricamento delle immagini e la risoluzione delle perdite di memoria sono passaggi fondamentali per offrire un'esperienza utente di alta qualità gestendo in modo efficace le risorse in condizioni di stress. L'adozione di queste misure proattive contribuisce a mantenere la stabilità e il rendimento dell'app, evitando chiusure impreviste e salvaguardando il contesto dell'utente. Per approfondire le tue competenze in materia di prestazioni, consulta la nostra guida alla memoria rivista.
-
IstruzioniConsapevole del fatto che il consumo eccessivo della batteria è top of mind per gli utenti Android, Google ha adottato misure significative per aiutare gli sviluppatori a creare app più efficienti dal punto di vista energetico.
Alice Yuan • Lettura di 8 minuti -
IstruzioniLa guida al livellamento delle prestazioni è suddivisa in 5 livelli. Inizieremo con il livello 1, che introduce strumenti di misurazione delle prestazioni con uno sforzo di adozione minimo, e arriveremo al livello 5, ideale per le app che dispongono delle risorse per mantenere un framework di prestazioni personalizzato.
Alice Yuan • Lettura di 9 minuti -
IstruzioniQuando si lavora a nuove funzionalità, le prestazioni dell'app spesso passano in secondo piano. Tuttavia, anche se non è sempre la priorità degli sviluppatori, gli utenti possono vedere esattamente dove le prestazioni della tua app sono inferiori.
Ben Weiss • Lettura di 3 minuti
Ricevi gli ultimi approfondimenti sullo sviluppo per Android direttamente nella tua casella di posta ogni settimana.