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 l'applicazione 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 applicazione di Android si concentra rigorosamente sull'RSS anonimo:

  • Che 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):

  • Che 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 tiene traccia di 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.

  • Che 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 gioco è in esecuzione con uno stato di processo sensibile se uno degli SDK di terze parti attiva involontariamente un'attività in background. Monitora 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 di 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 le acquisizioni manualmente in scene specifiche o registrare trigger automatici come TRIGGER_TYPE_ANOMALY per attivare automaticamente un'acquisizione quando il processo del gioco viola le soglie di Memory Limiter. Tuttavia, gli sviluppatori di giochi devono tenere conto delle limitazioni critiche nei motori di gioco moderni:

Nota:i motori di gioco moderni (ad esempio 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 della 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 di 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 da Memory Limiter 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 questa quantità diventa disponibile per il sistema quando il processo viene terminato.

Tracepoint di memoria

I tracepoint di memoria tengono traccia della quantità di memoria RSS utilizzata dal gioco. Il calcolo della memoria utilizzata RSS è molto più veloce del calcolo della memoria utilizzata PSS. Poiché è più veloce da calcolare, l'RSS mostra una granularità più precisa delle modifiche alla dimensione 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 Tracciamento del sistema. Assicurati che la categoria memory: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 le risorse in background.

2. Eseguire query SQL (Trace Processor) 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. picchi.

  • Trova l'allocazione RSS anonima massima:

    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 si è arrestato in modo anomalo perché ha esaurito la memoria. L'output dello strumento è molto più dettagliato rispetto all'utilizzo di logcat. È utile per il debug della memoria perché mostra se il gioco si è arrestato in modo anomalo perché ha esaurito la memoria o se è stato terminato da LMK.

Per maggiori 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 le regole di configurazione e gli script di runtime, consulta Come controllare la memoria con gli strumenti di Unity.

  • API del 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 della 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.