Gestire la memoria dell'app

In questa pagina viene spiegato come ridurre in modo proattivo l'utilizzo della memoria all'interno dell'app. Per informazioni su come il sistema operativo Android gestisce la memoria, consulta la Panoramica della gestione della memoria.

La memoria ad accesso casuale (RAM) è una risorsa preziosa per qualsiasi ambiente di sviluppo software ed è ancora più preziosa per un sistema operativo mobile in cui la memoria fisica è spesso limitata. Anche se sia Android Runtime (ART) che la macchina virtuale Dalvik eseguono la garbage collection di routine, questo non significa che tu possa ignorare quando e dove la tua app alloca e rilascia memoria. Devi comunque evitare di introdurre perdite di memoria, di solito causate dall'archiviazione a fini legali dei riferimenti agli oggetti nelle variabili dei membri statici, e rilasciare eventuali oggetti Reference al momento appropriato in base a quanto definito dai callback del ciclo di vita.

Monitorare l'utilizzo della memoria e della memoria disponibile

Devi individuare i problemi di utilizzo della memoria dell'app per poterli risolvere. Lo strumento Memory Profiler in Android Studio ti consente di trovare e diagnosticare i problemi di memoria nei seguenti modi:

  • Scopri come la tua app assegna la memoria nel tempo. Profiler di memoria mostra un grafico in tempo reale della quantità di memoria utilizzata dall'app, il numero di oggetti Java allocati e quando si verifica la garbage collection.
  • Avvia gli eventi di garbage collection e acquisisci uno snapshot dell'heap Java durante l'esecuzione dell'app.
  • Registra le allocazioni della memoria della tua app, esamina tutti gli oggetti allocati, visualizza l'analisi dello stack per ogni allocazione e passa al codice corrispondente nell'editor di Android Studio.

Rilascia memoria in risposta agli eventi

Android può recuperare memoria dalla tua app o interromperla completamente, se necessario, per liberare memoria per le attività critiche, come spiegato nella Panoramica della gestione della memoria. Per bilanciare ulteriormente la memoria di sistema ed evitare che il sistema debba arrestare il processo dell'app, puoi implementare l'interfaccia ComponentCallbacks2 nelle classi Activity. Il metodo di callback onTrimMemory() fornito consente alla tua app di ascoltare gli eventi relativi alla memoria quando è in primo piano o in background. Dopodiché consente all'app di rilasciare oggetti in risposta al ciclo di vita dell'app o a eventi di sistema che indicano che il sistema deve recuperare memoria.

Puoi implementare il callback onTrimMemory() per rispondere a diversi eventi relativi alla memoria, come mostrato nell'esempio seguente:

Kotlin

import android.content.ComponentCallbacks2
// Other import statements.

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

    // Other activity code.

    /**
     * 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) {

        // Determine which lifecycle or system event is raised.
        when (level) {

            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                /*
                   Release any UI objects that currently hold memory.

                   The user interface moves to the background.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                /*
                   Release any memory your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system
                   begins stopping background processes.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
            ComponentCallbacks2.TRIM_MEMORY_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process is one of the
                   first to be terminated.
                */
            }

            else -> {
                /*
                  Release any non-critical data structures.

                  The app receives an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
            }
        }
    }
}

Java

import android.content.ComponentCallbacks2;
// Other import statements.

public class MainActivity extends AppCompatActivity
    implements ComponentCallbacks2 {

    // Other activity code.

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    public void onTrimMemory(int level) {

        // Determine which lifecycle or system event is raised.
        switch (level) {

            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                /*
                   Release any UI objects that currently hold memory.

                   The user interface moves to the background.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:

                /*
                   Release any memory your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system
                   begins stopping background processes.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
            case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:

                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process is one of the
                   first to be terminated.
                */

                break;

            default:
                /*
                  Release any non-critical data structures.

                  The app receives an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
                break;
        }
    }
}

Controllare la quantità di memoria necessaria

Per consentire più processi in esecuzione, Android imposta un limite fisso per le dimensioni dello heap assegnate per ogni app. Il limite esatto per le dimensioni dell'heap varia tra i dispositivi in base alla quantità di RAM disponibile complessivamente. Se l'app raggiunge la capacità di heap e tenta di allocare più memoria, il sistema genera un OutOfMemoryError.

Per evitare di esaurire la memoria, puoi eseguire una query sul sistema per determinare la quantità di spazio heap disponibile sul dispositivo attuale. Puoi eseguire una query al sistema per questa figura chiamando getMemoryInfo(). Questa operazione restituisce un oggetto ActivityManager.MemoryInfo che fornisce informazioni sullo stato della memoria attuale del dispositivo, tra cui la memoria disponibile, la memoria totale e la soglia di memoria, il livello di memoria a cui il sistema inizia ad arrestare i processi. L'oggetto ActivityManager.MemoryInfo espone anche lowMemory, un semplice valore booleano che indica se la memoria del dispositivo scarseggia.

Il seguente esempio di snippet di codice mostra come utilizzare il metodo getMemoryInfo() nella tua app.

Kotlin

fun doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    if (!getAvailableMemory().lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private fun getAvailableMemory(): ActivityManager.MemoryInfo {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    return ActivityManager.MemoryInfo().also { memoryInfo ->
        activityManager.getMemoryInfo(memoryInfo)
    }
}

Java

public void doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();

    if (!memoryInfo.lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
    ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
    ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
    activityManager.getMemoryInfo(memoryInfo);
    return memoryInfo;
}

Utilizza costrutti di codice con maggiore efficienza in termini di memoria

Alcune funzionalità di Android, classi Java e costrutti di codice utilizzano più memoria di altre. Puoi ridurre al minimo la quantità di memoria utilizzata dall'app scegliendo alternative più efficienti nel tuo codice.

Utilizzare i servizi con parsimonia

Ti consigliamo vivamente di non lasciare i servizi in esecuzione quando non sono necessari. Lasciare in esecuzione servizi non necessari è uno dei peggiori errori di gestione della memoria che un'app Android possa commettere. Se la tua app ha bisogno di un servizio per funzionare in background, non lasciarlo in esecuzione a meno che non sia necessario eseguire un job. Interrompi il servizio quando completa l'attività. In caso contrario, potresti causare una perdita di memoria.

Quando avvii un servizio, il sistema preferisce mantenere il processo per quel servizio in esecuzione. Questo comportamento rende i processi dei servizi molto costosi perché la RAM utilizzata da un servizio non è disponibile per altri processi. In questo modo si riduce il numero di processi che il sistema può conservare nella cache LRU, rendendo meno efficiente il passaggio tra le app. Può anche portare a minacce al sistema quando la memoria è scarsa e il sistema non è in grado di gestire abbastanza processi per ospitare tutti i servizi attualmente in esecuzione.

In genere, evita di utilizzare servizi permanenti a causa delle continue richieste che richiedono alla memoria disponibile. Ti consigliamo invece di utilizzare un'implementazione alternativa, come WorkManager. Per ulteriori informazioni su come utilizzare WorkManager per pianificare processi in background, consulta Lavoro permanente.

Utilizzare i contenitori di dati ottimizzati

Alcune delle classi fornite dal linguaggio di programmazione non sono ottimizzate per l'utilizzo sui dispositivi mobili. Ad esempio, l'implementazione HashMap generica può essere inefficiente in termini di memoria perché richiede un oggetto di voce separato per ogni mappatura.

Il framework Android include diversi container di dati ottimizzati, tra cui SparseArray, SparseBooleanArray e LongSparseArray. Ad esempio, le classi SparseArray sono più efficienti perché evitano che il sistema sia necessario autobox per la chiave e, a volte, per il valore, il che crea altri oggetti ancora o due per voce.

Se necessario, puoi sempre passare a array non elaborati per una struttura di dati sottili.

Fai attenzione alle astrazioni del codice

Gli sviluppatori spesso usano le astrazioni come buona pratica di programmazione perché possono migliorare la flessibilità e la manutenzione del codice. Tuttavia, le astrazioni sono molto più costose perché di solito richiedono più codice da eseguire, il che richiede più tempo e RAM per mappare il codice in memoria. Se le tue astrazioni non sono significativamente vantaggiose, evitale.

Utilizzare i protobuf Lite per i dati serializzati

I buffer di protocollo (protobuf) sono un meccanismo estensibile e indipendente dal linguaggio e dalla piattaforma progettato da Google per la serializzazione dei dati strutturati. Simile al formato XML, ma di dimensioni inferiori, più veloci e più semplici. Se utilizzi protobuf per i tuoi dati, utilizza sempre protobufs Lite nel codice lato client. I protobuf regolari generano codice estremamente dettagliato, che può causare molti problemi nell'app, ad esempio utilizzo maggiore della RAM, notevole aumento delle dimensioni degli APK ed esecuzione più lenta.

Per ulteriori informazioni, consulta il readme protobuf.

Evitare l'abbandono della memoria

Gli eventi di garbage collection non influiscono sulle prestazioni dell'app. Tuttavia, molti eventi di garbage collection che si verificano in un breve periodo di tempo possono consumare rapidamente la batteria e aumentare marginalmente il tempo di configurazione dei frame a causa delle interazioni necessarie tra garbage collector e thread di app. Più tempo il sistema dedica alla raccolta dei rifiuti, più rapido è il consumo della batteria.

Spesso, il abbandono della memoria può causare il verificarsi di un numero elevato di eventi di garbage collection. In pratica, il tasso di abbandono della memoria descrive il numero di oggetti temporanei allocati che si verificano in un determinato periodo di tempo.

Ad esempio, potresti allocare più oggetti temporanei all'interno di un loop for. In alternativa, puoi creare nuovi oggetti Paint o Bitmap all'interno della funzione onDraw() di una vista. In entrambi i casi, l'app crea rapidamente molti oggetti a volume elevato. Questi elementi possono consumare rapidamente tutta la memoria disponibile nella giovane generazione, forzando la generazione di un evento di garbage collection.

Prima di risolvere il problema, utilizza Memory Profiler per individuare i punti del codice in cui il tasso di abbandono di memoria è elevato.

Dopo aver identificato le aree problematiche del codice, prova a ridurre il numero di allocazioni all'interno delle aree critiche per le prestazioni. Valuta la possibilità di spostare gli elementi fuori dai loop interni o di spostarli in una struttura di allocazione basata sulla fabbrica.

Puoi anche valutare se i pool di oggetti sono utili per il caso d'uso. Con un pool di oggetti, invece di rilasciare un'istanza di oggetto sul pavimento, puoi rilasciarla in un pool quando non è più necessario. La prossima volta che sarà necessaria un'istanza di questo tipo, potrai acquisirla dal pool anziché allocarla.

Valutare attentamente le prestazioni per determinare se un pool di oggetti è adatto in una determinata situazione. In alcuni casi, i pool di oggetti potrebbero peggiorare le prestazioni. Anche se i pool evitano le allocazioni, comportano altri costi generali. Ad esempio, la manutenzione del pool di solito comporta la sincronizzazione, con un overhead non trascurabile. Inoltre, la cancellazione dell'istanza dell'oggetto in pool per evitare perdite di memoria durante la release e poi l'inizializzazione durante l'acquisizione può avere un overhead diverso da zero.

Limitare il numero di istanze di oggetti nel pool rispetto a quelle necessarie crea un carico anche sulla garbage collection. Sebbene i pool di oggetti riducono il numero di chiamate di garbage collection, finiscono per aumentare la quantità di lavoro necessaria per ogni chiamata, in quanto è proporzionale al numero di byte attivi (raggiungibili).

Rimuovi le risorse e le librerie che utilizzano molta memoria

Alcune risorse e librerie all'interno del codice possono consumare memoria senza che tu te ne accorga. Le dimensioni complessive della tua app, comprese le librerie di terze parti o le risorse incorporate, possono influire sulla quantità di memoria consumata dall'app. Puoi migliorare il consumo di memoria dell'app rimuovendo componenti o risorse e librerie ridondanti, non necessari o con problemi di eccesso dal codice.

Ridurre le dimensioni complessive degli APK

Puoi ridurre notevolmente l'utilizzo della memoria della tua app riducendone le dimensioni complessive. Le dimensioni delle bitmap, le risorse, i frame di animazione e le librerie di terze parti possono contribuire alle dimensioni dell'app. Android Studio e l'SDK Android forniscono diversi strumenti per ridurre le dimensioni delle risorse e delle dipendenze esterne. Questi strumenti supportano i moderni metodi di riduzione del codice, come la compilation R8.

Per ulteriori informazioni sulla riduzione delle dimensioni complessive dell'app, vedi Ridurre le dimensioni dell'app.

Utilizza Hilt o Dagger 2 per l'inserimento delle dipendenze

I framework di inserimento delle dipendenze possono semplificare la scrittura del codice e fornire un ambiente adattivo utile per i test e altre modifiche alla configurazione.

Se intendi utilizzare un framework di inserimento delle dipendenze nella tua app, valuta la possibilità di utilizzare Hilt o Dagger. Hilt è una libreria di inserimento di dipendenze per Android su Dagger. Dagger non utilizza il riflesso per scansionare il codice dell'app. Puoi utilizzare l'implementazione statica in fase di compilazione di Dagger nelle app per Android senza costi di runtime o utilizzo della memoria inutili.

Altri framework di inserimento delle dipendenze che utilizzano la riflessione inizializzano i processi analizzando il codice alla ricerca di annotazioni. Questo processo può richiedere molti più cicli della CPU e della RAM e può causare un notevole ritardo all'avvio dell'app.

Fai attenzione quando utilizzi librerie esterne

Il codice della libreria esterna spesso non è scritto per gli ambienti mobili e può essere inefficiente per lavorare su un client mobile. Quando utilizzi una libreria esterna, potrebbe essere necessario ottimizzarla per i dispositivi mobili. Pianificare in anticipo questa operazione e analizzare la libreria in termini di dimensioni del codice e utilizzo della RAM prima di utilizzarla.

Anche alcune librerie ottimizzate per i dispositivi mobili possono causare problemi a causa di implementazioni diverse. Ad esempio, una libreria potrebbe utilizzare protobuf lite, mentre un'altra utilizza micro protobuf, con il risultato di due diverse implementazioni di protobuf nell'app. Questo può accadere con diverse implementazioni di logging, analisi, framework di caricamento delle immagini, memorizzazione nella cache e molte altre cose non previste.

Sebbene ProGuard possa aiutare a rimuovere le API e le risorse con i flag giusti, non può rimuovere le grandi dipendenze interne di una libreria. Le funzionalità desiderate in queste librerie potrebbero richiedere dipendenze di livello inferiore. Ciò diventa particolarmente problematico quando si utilizza una sottoclasse Activity da una libreria, che può avere un'ampia serie di dipendenze, quando le librerie usano la riflessione, cosa comune e richiede una modifica manuale di ProGuard affinché funzioni.

Evita di utilizzare una libreria condivisa solo per una o due funzionalità su decine. Non devi importare una grande quantità di codice e overhead che non utilizzi. Quando valuti l'uso o meno di una libreria, cerca un'implementazione che corrisponda perfettamente alle tue esigenze. In caso contrario, puoi decidere di creare una tua implementazione.