Monitorare l'utilizzo della memoria

Per ottimizzare in modo efficace il footprint della memoria del gioco, devi prima capire in che modo la piattaforma Android misura la memoria e come utilizzare la telemetria di sistema, le API di diagnostica e gli strumenti di profilazione. Questa guida descrive in dettaglio come monitorare, acquisire e analizzare le allocazioni di memoria del gioco in base alle nuove linee guida della piattaforma.

Informazioni sulle metriche RSS e swap

Per analizzare ed eseguire il debug in modo efficace del comportamento della memoria del gioco, devi comprendere le metriche tecniche esatte utilizzate dalla piattaforma Android per la gestione della memoria. Per informazioni di base dettagliate su come questo parametro di telemetria viene elaborato e monitorato in produzione, consulta la documentazione di Android Vitals - Memoria utilizzata (RSS anonimo + swap).

1. RSS anonimo (RssAnon)

La dimensione del set residente (RSS) misura la porzione di memoria occupata da un processo che viene mantenuta nella RAM fisica del dispositivo. L'RSS è suddiviso in memoria supportata da file e memoria anonima. La metrica di memoria di Android si concentra rigorosamente sull'RSS anonimo:

  • Cosa include: pagine di memoria allocate direttamente dal processo del gioco che non sono collegate a un file fisico nello spazio di archiviazione. Queste pagine includono heap Java o Kotlin, stack di esecuzione dei thread e, soprattutto, allocazioni di memoria nativa (ad esempio allocatori di motori C++ personalizzati o blocchi di memoria richiesti utilizzando malloc o new nativi e modificati dalla logica di gioco). Scopri di più su questa metrica nel dizionario Memoria di processo (RSS).
  • Perché è importante: i motori di gioco utilizzano enormi pool di memoria nativa per gestire la fisica, il rendering e la logica. Poiché questi pool non sono supportati da file, risiedono interamente nell'RSS anonimo e costituiscono la maggior parte del footprint fisico del gioco.

2. Swap non compresso (VmSwap)

Android non supporta uno spazio di swap tradizionale basato su disco a causa dei vincoli di usura e latenza dello spazio di archiviazione flash. Utilizza invece zRAM (swap non compresso):

  • Cosa include: quando la pressione della RAM fisica aumenta, il daemon di gestione della memoria del kernel comprime le pagine anonime inattive e le sposta in una porzione dedicata e non compressa della RAM fisica (zRAM).
  • Calcolo della metrica: il sistema monitora questo valore in base alla dimensione non compressa (VmSwap) per valutare la domanda effettiva di memoria fisica del gioco. Se il gioco alloca memoria e il sistema la scambia con zRAM, viene comunque conteggiata nel footprint totale della memoria del gioco.

3. Stati dei processi

La memoria utilizzata è suddivisa per stati dei processi in Android Vitals. Per gli sviluppatori di giochi, gli SDK o i giochi di terze parti possono anche attivare in modo imprevisto servizi percepiti dall'utente o servizi in background.

  • Cosa include: primo piano, servizi percepibili, background e memorizzati nella cache.
  • Perché è importante: i diversi stati dei processi hanno un impatto diverso sulla gestione della memoria del sistema operativo Android. Potresti non sapere che il tuo gioco è in esecuzione con uno stato di processo sensibile se uno degli SDK di terze parti attiva involontariamente un'attività in background. Verifica se il gioco è in esecuzione in background utilizzando RunningAppProcessInfo.

Application Programming Interface (API)

Android fornisce API di sistema che consentono al gioco di rispondere in modo dinamico alla pressione della memoria e di acquisire diagnostiche dettagliate della memoria in fase di runtime.

Rispondere agli eventi di riduzione della memoria

Il sistema utilizza onTrimMemory per notificare alla tua app gli eventi del ciclo di vita che rappresentano una buona opportunità per ridurre volontariamente la memoria utilizzata ed evitare di essere terminata dal killer per memoria insufficiente (LMK) per liberare memoria per altre app.

Se il sistema termina l'app in background, l'utente riscontra un avvio a freddo lento alla ripresa. La riduzione della memoria utilizzata in background contribuisce a prevenire queste terminazioni in background.

Quando rispondi agli eventi di riduzione, rilascia le allocazioni di memoria di grandi dimensioni e ricostruibili che non sono necessarie immediatamente:

  • Esempio: riduci o elimina le bitmap memorizzate nella cache (decodificate dallo spazio di archiviazione locale) in risposta a TRIM_MEMORY_UI_HIDDEN.

Kotlin

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {
    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.
        }
    }
}

Java

public class MainActivity extends AppCompatActivity implements ComponentCallbacks2 {
    public void onTrimMemory(int level) {
        switch (level) {
            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.
            }
        }
    }
}

ProfilingManager

Introdotta in Android 15 (livello API 35), l'API ProfilingManager consente alle applicazioni di acquisire snapshot definiti a livello di programmazione (ad esempio profili heap , tracce di sistema e dump heap Java) direttamente in fase di runtime.

Gli sviluppatori possono attivare manualmente le acquisizioni in scene specifiche o registrare trigger automatici come TRIGGER_TYPE_ANOMALY per attivare automaticamente un'acquisizione quando il processo del gioco viola le soglie del limitatore di memoria. Tuttavia, gli sviluppatori di giochi devono tenere conto delle limitazioni critiche nei moderni motori di gioco:

Nota:i motori di gioco moderni (come Unity o Unreal) gestiscono le prestazioni di esecuzione preallocando enormi blocchi di memoria virtuale dal kernel utilizzando mmap con il flag MAP_ANONYMOUS. I motori utilizzano quindi suballocatori personalizzati (ad esempio, il gestore di memoria nativa di Unity o BinnedAllocators di Unreal) per suddividere e allocare internamente i blocchi di memoria.

ApplicationExitInfo

Se il gioco viene terminato in background o interrotto perché ha violato i limiti di memoria dei singoli processi, i meccanismi standard di dump di arresto anomalo Java o nativi (ad esempio Firebase Crashlytics) non registrano l'evento. Per eseguire query e registrare queste terminazioni a livello di programmazione, gli sviluppatori devono utilizzare l' ApplicationExitInfo API all'avvio del gioco.

  • Implementazione: all'avvio, chiama ActivityManager.getHistoricalProcessExitReasons() per recuperare i motivi di uscita delle sessioni recenti.
  • Motivi di uscita dalla memoria principali:
    • REASON_LOW_MEMORY: indica che il processo è stato terminato dal killer per memoria insufficiente (LMK) del sistema. Questa terminazione si verifica quando la pressione della memoria a livello di dispositivo è elevata e il sistema operativo deve recuperare la RAM. Questo motivo di uscita indica che il footprint in background del gioco è troppo grande per coesistere con altre applicazioni.
    • REASON_MEMORY_LIMITER (Android 17 (livello API 37) e versioni successive): indica che il processo è stato interrotto in modo specifico perché ha superato il limite di memoria cgroup (RssAnon + VmSwap) assegnato dal limitatore di memoria della piattaforma. Questa terminazione può verificarsi anche se sul dispositivo è rimasta molta memoria fisica, il che indica una violazione diretta dei limiti dei singoli processi.

Utilizzare gli strumenti disponibili

Utilizza i seguenti strumenti della piattaforma durante lo sviluppo e il controllo qualità per misurare con precisione la memoria utilizzata del gioco.

meminfo

Questo strumento raccoglie statistiche sulla memoria per mostrare la quantità di memoria PSS allocata e le categorie per cui è stata utilizzata.

Stampa le meminfo statistiche in uno dei seguenti modi:

  • Utilizza il comando adb shell dumpsys meminfo package-name.
  • Utilizza la MemoryInfo chiamata dall'API di debug di Android.

La statistica PrivateDirty mostra la quantità di RAM all'interno del processo che non può essere paginata su disco e non è condivisa con altri processi. La maggior parte di questo importo diventa disponibile per il sistema quando il processo viene terminato.

Punti di traccia della memoria

I punti di traccia della memoria monitorano la quantità di memoria RSS utilizzata dal gioco. Il calcolo della memoria utilizzata da RSS è molto più veloce del calcolo dell'utilizzo della PSS. Poiché è più veloce da calcolare, l'RSS mostra una granularità più precisa delle modifiche delle dimensioni della memoria per misurazioni più accurate della memoria utilizzata. Di conseguenza, è più facile notare i picchi che potrebbero causare l'esaurimento della memoria del gioco.

Perfetto

Perfetto è una suite di strumenti per la raccolta di informazioni su prestazioni e memoria su un dispositivo e la visualizzazione in un'interfaccia utente basata sul web. Supporta tracce di lunghezza arbitraria, in modo da poter visualizzare l'evoluzione dell'RSS nel tempo. Puoi anche eseguire query SQL sui dati prodotti per l'elaborazione offline. Attiva le tracce lunghe dall'app Traccia di sistema. Assicurati che la categoriamemory:Memory sia attivata per la traccia. Per la strumentazione della memoria personalizzata in fase di sviluppo e test, puoi anche utilizzare l'API (beta) heapprofd.

Ispezionare RssAnon e swap in Perfetto

Per verificare l'impatto della memoria anonima e dello swap zRAM del gioco, carica il file di traccia nell'interfaccia utente basata sul web all'indirizzo ui.perfetto.dev e segui queste tecniche di analisi , progettate per studi di casi di memoria approfonditi (per maggiori dettagli, consulta Studi di casi di analisi della memoria di Perfetto):

1. Visualizzare i contatori di memoria nella sequenza temporale

  • Individua il processo: nell'elenco di navigazione, cerca il nome del pacchetto o del processo del gioco.
  • Espandi il gruppo di tracce: fai clic sulla riga del processo per espandere le tracce dei thread e individua il sottogruppo denominato Memoria.
  • Analizza le tracce:
    • mem.rss.anon (RSS anonimo): questo grafico a linee mostra la RAM fisica in tempo reale occupata dai pool di memoria non gestiti del gioco. Monitora questa sequenza temporale durante i caricamenti delle scene, i popup dell'interfaccia utente o le transizioni di gioco per verificare la presenza di picchi di allocazione elevati.
    • mem.swap (swap compresso o VmSwap): questo grafico traccia la dimensione precompressa dei blocchi di memoria spostati in zRAM. Un'attività di swap elevata in concomitanza con il gameplay indica che il gioco è in esecuzione su un dispositivo con memoria limitata e che il sistema sta comprimendo attivamente gli asset in background.

2. Esecuzione di query SQL (elaboratore di tracce) Per un'analisi offline dettagliata, puoi eseguire query SQL direttamente nella console dell'interfaccia utente di Perfetto o utilizzare la libreria Python Trace Processor autonoma per calcolare i picchi statistici.

  • Trova l'allocazione massima di RSS anonimo:

    SELECT
      max(value) / 1024 / 1024 AS max_rss_anon_mb
    FROM counter
    JOIN counter_track ON counter.track_id = counter_track.id
    WHERE counter_track.name = 'mem.rss.anon'
      AND counter_track.upid IN (
        SELECT upid FROM process WHERE name = 'your.game.package.name'
      );
    
  • Correlare RssAnon e VmSwap in un determinato timestamp:

    SELECT
      ts,
      track.name AS metric_type,
      value / 1024 / 1024 AS size_mb
    FROM counter
    JOIN counter_track track ON counter.track_id = track.id
    WHERE (track.name = 'mem.rss.anon' OR track.name = 'mem.swap')
      AND track.upid IN (
        SELECT upid FROM process WHERE name = 'your.game.package.name'
      )
    ORDER BY ts ASC;
    

Per maggiori dettagli sull'ispezione dei file di traccia utilizzando Android Studio, consulta Ispezionare le tracce di sistema: memoria di processo (RSS). Per dettagli sulla creazione di script per i profili di memoria, consulta Registrare le allocazioni native.

heapprofd

heapprofd è uno strumento di monitoraggio della memoria che fa parte di Perfetto. Questo strumento può aiutarti a trovare perdite di memoria mostrando dove è stata allocata la memoria utilizzando malloc. heapprofd può essere avviato utilizzando uno script Python e, poiché lo strumento ha un overhead basso, non influisce sulle prestazioni come altri strumenti come Malloc Debug.

bugreport

bugreport è uno strumento di logging per scoprire se il gioco ha subito un arresto anomalo a causa dell'esaurimento della memoria. L'output dello strumento è molto più dettagliato rispetto all'utilizzo di logcat. È utile per il debug della memoria perché mostra se il gioco ha subito un arresto anomalo a causa di out of memory o se è stato terminato dal LMK.

Per ulteriori informazioni, consulta Acquisire e leggere i report sui bug.

Strumenti del motore grafico

Sebbene i log a livello di piattaforma e la telemetria di sistema siano fondamentali per monitorare le soglie e la conformità del sistema operativo, gli strumenti specifici del motore di gioco ti aiutano ad attribuire le allocazioni direttamente agli oggetti di gioco, ai comportamenti degli script e alle gerarchie delle scene attive.

Unità

In un ambiente Unity Engine, puoi stimare con precisione il footprint della memoria RSS anonima + swap di Android in fase di runtime con un'affidabilità elevata (in genere con una varianza inferiore al 10% rispetto ai valori effettivi a livello di sistema operativo) utilizzando le classi e gli strumenti di profilazione nativi di Unity.

Per un tutorial passo passo completo, incluse regole di configurazione e script di runtime, consulta Come controllare la memoria con gli strumenti di Unity.

  • API Profiler di Unity: puoi approssimare a livello di programmazione il footprint della memoria non gestita del gioco in fase di runtime eseguendo query sulle metriche principali del motore:
    • Utilizzo della classe Profiler: monitora le allocazioni di memoria totali sommando i valori di Profiler.GetTotalReservedMemoryLong() e Profiler.GetMonoHeapSizeLong().
    • Utilizzo della classe ProfilerRecorder: monitora le categorie di memoria in modo dinamico. Per stabilire un'approssimazione di base affidabile, recupera la memoria riservata totale (nelle build di release) o sottrai la memoria riservata Gfx (nelle build di sviluppo) per rimuovere i componenti di memoria grafica supportati da file.
  • Profiler di memoria di Unity: per identificare ed eseguire il debug delle perdite di memoria offline, acquisisci uno snapshot della memoria e ispeziona il grafico Memoria residente sul dispositivo nella sezione Tutta la memoria. Per calcolare il footprint approssimativo, somma i totali delle seguenti categorie: Non monitorata, Android Runtime, Nativa e Gestita.
    • Limitazione zRAM: in condizioni di memoria limitata, il kernel Android può comprimere le pagine di memoria inattive nello spazio di swap (zRAM). Poiché il Memory Profiler di Unity non è in grado di rilevare i parametri di swap a livello di sistema operativo, potresti notare piccole discrepanze nel footprint durante le scene con un utilizzo elevato della memoria. Esegui un controllo incrociato delle stime con Perfetto per confermare i valori esatti.

Unreal

In un ambiente Unreal Engine, puoi valutare il footprint della memoria del gioco combinando la diagnostica del motore con la telemetria della piattaforma. Per istruzioni passo passo e flussi di lavoro di profilazione, consulta Controllare la memoria utilizzata con Unreal Engine.

Gli strumenti e le interfacce di diagnostica principali includono:

  • API di diagnostica C++: utilizza GetMemoryUsedFast per query di memoria leggere e l'interfaccia GetStats per statistiche di memoria a livello di hardware.
  • Comandi della console: monitora le tendenze di allocazione della memoria in tempo reale sull'hardware del dispositivo utilizzando i comandi del motore stat unit e stat unitmax.
  • Unreal Insights: ispeziona le acquisizioni della sequenza temporale con precisione dei frame per analizzare le metriche a livello di piattaforma e i contatori di memoria personalizzati.