Mantieni attivo il lavoro in background con un processo snello

Molte app eseguono operazioni legittime quando la loro UI non è più visibile, ad esempio il download di pacchetti di asset di grandi dimensioni, la sincronizzazione di database locali, lo streaming audio, la registrazione di allenamenti o il mantenimento di una sessione di un dispositivo connesso. Quando questo lavoro in background viene eseguito all'interno dello stesso processo del sistema operativo della UI in primo piano e del motore grafico, l'intero processo rimane a un livello di importanza elevato (oom_score_adj tra 0 e 250) insieme a tutta la memoria allocata dalla UI.

Questa pagina spiega perché questo danneggia la tua app e il resto del dispositivo, come spostare il lavoro in background in un processo leggero dedicato utilizzando android:process e come verificare il risparmio di memoria utilizzando adb shell e Perfetto.

Perché il lavoro su un singolo processo conserva la memoria della UI

Android gestisce la memoria e la terminazione dei processi con la granularità di un processo del sistema operativo (pid), non dei singoli componenti (Activity, Service o BroadcastReceiver). Activity Manager (OomAdjuster) assegna a ogni processo un singolo valore oom_score_adj che riflette il componente attivo o l'associazione in entrata più importante in quel processo:

  • Quando l'utente interagisce con la tua Activity, il processo viene eseguito nello stato TOP (oom_score_adj = 0).
  • Quando l'utente esce dall'app e questa non ha componenti in background attivi, il processo passa al bucket PREV (oom_score_adj = 700) o CACHED (oom_score_adj >= 900). Nel bucket memorizzato nella cache, la piattaforma blocca il processo con il freezer cgroup v2, compatta la memoria con CachedAppOptimizer e consente a lmkd di terminare il processo per primo in caso di pressione della memoria.
  • Tuttavia, se lo stesso processo esegue anche un servizio in primo piano (ad esempio i tipi dataSync, mediaPlayback, connectedDevice o location in oom_score_adj = 200) o ospita un servizio a cui si associa il sistema o un altro client importante, l'intero processo rimane con quella priorità elevata anche dopo l'arresto di Activity.
Confronto tra la co-location di un singolo processo che mantiene la memoria della UI bloccata a oom_score_adj 200 e l'isolamento dei processi suddivisi che mantiene solo il worker a oom_score_adj 200 mentre il processo della UI scende alla cache.
Figura 1. Co-location a processo singolo rispetto all'isolamento a processo suddiviso durante il lavoro in background.

Quando un processo UI pesante rimane a oom_score_adj = 200, diverse categorie di memoria di grandi dimensioni rimangono residenti:

  • Activity interrotta e gerarchia di oggetti View: a meno che l'Activity non venga terminata esplicitamente, il relativo Window, l'albero delle visualizzazioni, le strutture di layout e lo stato della visualizzazione rimangono raggiungibili nell'heap Java mentre l'utente si trova in un'altra app.
  • Contesti grafici nativi e allocazioni GPU: l'HardwareRenderer standard mantiene il contesto e le cache della GPU, mentre un Activity interrotto si trova in un processo non memorizzato nella cache. Anche le app e i motori grafici che creano i propri contesti Vulkan (VkDevice, VkSwapchainKHR, pool di descrittori, cache della pipeline) o OpenGL ES (EGLContext, texture, buffer dei vertici) mantengono le strutture dei driver, i programmi shader e i buffer grafici, a meno che il motore non elimini esplicitamente il dispositivo grafico quando viene arrestato.
  • Frammentazione dell'heap e overhead di runtime: anche quando un'app rilascia le cache in onTrimMemory, le pagine dell'allocatore nativo parzialmente popolate, le pagine di copia su scrittura Zygote dirty, le librerie condivise caricate (.so) e i metadati di runtime rimangono residenti nella memoria privata del processo. Poiché il processo supera la soglia di memorizzazione nella cache (oom_score_adj < 900), la piattaforma non lo blocca né esegue la compattazione delle app memorizzate nella cache. Per un'analisi più approfondita di questo effetto con i servizi associati, consulta Collocazione di un servizio associato con un'interfaccia utente che richiede molta memoria.

In che modo un processo in background snello protegge la tua app

Mantenere in background una memoria di dimensioni adatte al primo piano mette a rischio il lavoro in background:

  • I limiti di memoria in background sono più rigidi di quelli in primo piano. In Android 17 e versioni successive, il limitatore di memoria applica alla tua app un limite di memoria non visibile più piccolo, mentre nessuno dei suoi processi mostra una UI visibile, anche durante l'esecuzione di un servizio in primo piano. Il limite copre tutti i processi della tua app. Se il processo che esegue il servizio in primo piano conserva ancora la memoria della UI e della grafica, questa memoria viene conteggiata ai fini del limite inferiore. Quando l'app supera il limite, il kernel la limita durante il recupero delle pagine, il che rallenta il download, la sincronizzazione o la riproduzione in esecuzione. Se l'app continua ad allocare memoria dopo l'esaurimento dello swap, il sistema la termina. Per verificare se la tua app è stata interessata, consulta Limiti di memoria dell'app.
  • I processi più grandi vengono terminati per primi in caso di pressione sulla memoria. Quando lmkd deve liberare memoria ed è configurato per scegliere il processo più pesante in un bucket di importanza (ro.lmk.kill_heaviest_task), un processo di servizio in primo piano che contiene anche centinaia di MB di memoria UI viene terminato prima di processi più piccoli con la stessa importanza e il tuo lavoro in background si interrompe con questo. Consulta Interazione tra kswapd e lmkd.
  • Lo stato della UI memorizzato nella cache può essere bloccato, compattato e riscritto nella memoria flash. Consentire allo stato della UI in primo piano di passare a un processo memorizzato nella cache consente al sistema operativo di bloccare il processo, comprimere la memoria in zRAM e riscrivere le pagine cold nello spazio di archiviazione flash. Il footprint della memoria residente si riduce mentre lo stato della UI rimane invariato, offrendo sia al lavoro in background sia alla UI memorizzata nella cache la migliore possibilità di sopravvivere fino al ritorno dell'utente.
  • I budget di memoria per processo diventano pratici. Un processo che esegue solo lavoro in background ha un working set ridotto e prevedibile, quindi puoi assegnargli un budget di memoria limitato senza vincolare la UI.

L'esecuzione di un secondo processo potrebbe sembrare un aumento del footprint della memoria totale dell'app, perché un secondo runtime ha un costo di base ridotto. Questo confronto è valido solo se un singolo processo può liberare completamente tutto lo stato della UI e della grafica ogni volta che passa al lavoro in background, cosa che le app e i toolkit della UI non possono fare in modo affidabile nella pratica. Consentire al sistema operativo di bloccare, compattare e riscrivere un processo UI memorizzato nella cache separato utilizza molta meno memoria fisica nel caso comune, mantiene l'app entro il limite di non visibilità e riduce le terminazioni per memoria insufficiente per l'app e per l'intero dispositivo.

Isola il lavoro in background in un processo dedicato

Per eliminare la residenza di UI e grafica durante il lavoro in background, esegui componenti in background di lunga durata in un processo leggero dedicato, mantenendo l'UI e il motore grafico nel processo principale:

Processo Dichiarazione del manifest Componenti in hosting Stato quando l'utente esce dalla UI durante il lavoro in background
Processo UI principale Valore predefinito (com.example.app) Activity, visualizzazioni UI, cache delle immagini, renderer Vulkan o OpenGL ES, motore audio e di scena Scende a PREV e poi a CACHED (oom_score_adj >= 900). Bloccato, compattato e riscritto nella memoria flash.
Processo worker in background android:process=":bg" Service in primo piano, downloader di asset in background, worker di sincronizzazione o gestore di dispositivi connessi Viene eseguito a FGS (oom_score_adj = 200) solo con l'heap e il codice del worker, senza visualizzazioni UI o contesti GPU.

Dichiara i budget di processo e di memoria

Assegna il tuo background Service a un sottoprocesso privato impostando android:process=":bg" in AndroidManifest.xml. I due punti iniziali (:) creano un processo privato con ambito limitato all'UID della tua app.

In Android 17 QPR2 (livello API 37.2) e versioni successive, combina la suddivisione dei processi con i budget di memoria dell'app per processo all'interno del blocco <processes> in modo che il sistema operativo imponga un limite massimo di working set per il worker in background senza limitare il processo della UI in primo piano:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:name=".HeavyApplication"
        android:label="@string/app_name">

        <!-- App-wide budgets that cover every process, including :bg -->
        <memory-budget android:maxMb="512" />
        <memory-budget
            android:maxMb="128"
            android:state="background" />

        <processes>
            <!-- Tight budget for the headless background worker process -->
            <process android:process=":bg">
                <memory-budget android:maxMb="32" />
            </process>
        </processes>

        <activity
            android:name=".MainActivity"
            android:exported="true" />

        <service
            android:name=".AssetDownloadService"
            android:process=":bg"
            android:foregroundServiceType="dataSync"
            android:exported="false" />

    </application>
</manifest>

La memoria utilizzata in :bg viene conteggiata sia nel budget di sia nel budget dell'app. Sui dispositivi con versioni precedenti ad Android 17 QPR2, il sistema ignora <memory-budget>, mentre android:process=":bg" funziona su tutti i livelli API.

Inizializzazione del gate in Application.onCreate

Android crea un'istanza della classe Application e chiama Application.onCreate in ogni processo generato per il tuo pacchetto. Un errore comune quando si adotta android:process=":bg" è lasciare toolkit UI pesanti, pool di caricamento delle immagini, SDK Analytics o l'inizializzazione del motore nativo (System.loadLibrary) all'interno di un metodo Application.onCreate non protetto. In questo modo, il processo :bg viene gonfiato con lo stesso footprint di base che stai cercando di evitare.

Controlla Application.getProcessName, disponibile in Android 9 (livello API 28) e versioni successive, all'interno di onCreate e inizializza solo le dipendenze richieste dal processo corrente:

Kotlin

class HeavyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        val processName = Application.getProcessName()
        if (processName != null && processName.endsWith(BG_PROCESS_SUFFIX)) {
            initBackgroundWorkerProcess()
            return
        }
        initMainUiProcess()
    }

    private fun initBackgroundWorkerProcess() {
        // Initialize only lightweight HTTP, storage, and sync primitives.
        // Avoid loading native rendering libraries or allocating UI caches.
    }

    private fun initMainUiProcess() {
        // Initialize UI frameworks, image caches, and native engines.
        System.loadLibrary("heavy_engine_jni")
    }

    companion object {
        private const val BG_PROCESS_SUFFIX = ":bg"
    }
}

Java

public final class HeavyApplication extends Application {
    private static final String BG_PROCESS_SUFFIX = ":bg";

    @Override
    public void onCreate() {
        super.onCreate();
        String processName = Application.getProcessName();
        if (processName != null && processName.endsWith(BG_PROCESS_SUFFIX)) {
            initBackgroundWorkerProcess();
            return;
        }
        initMainUiProcess();
    }

    private void initBackgroundWorkerProcess() {
        // Initialize only lightweight HTTP, storage, and sync primitives.
        // Avoid loading native rendering libraries or allocating UI caches.
    }

    private void initMainUiProcess() {
        // Initialize UI frameworks, image caches, and native engines.
        System.loadLibrary("heavy_engine_jni");
    }
}

Le librerie che vengono inizializzate tramite un provider di contenuti, come Jetpack App Startup, vengono inizializzate solo nel processo principale, perché un provider di contenuti dichiarato senza android:process viene eseguito nel processo principale. Se il codice in :bg dipende da una libreria di questo tipo, inizializzala esplicitamente in initBackgroundWorkerProcess.

Se utilizzi un motore grafico come Unity, implementa il servizio in background in una libreria o un plug-in Android semplice scritto in Java o Kotlin che non faccia riferimento a classi del motore come UnityPlayer. Il processo :bg non carica mai le librerie native del motore. Per confermare, verifica che adb shell cat /proc/<pid>/maps per il processo :bg non elenchi libunity.so o libil2cpp.so.

Comunicare senza bloccare il processo della UI

Quando l'utente ha MainActivity aperto mentre AssetDownloadService è in esecuzione in :bg, la UI spesso deve mostrare l'avanzamento del download in tempo reale. L'importanza del processo passa da un client al processo che ospita il servizio o il provider di contenuti che utilizza, quindi la direzione di ogni dipendenza è importante:

  • L'associazione della UI al worker è sicura. Quando MainActivity si associa a AssetDownloadService, il sistema aumenta l'importanza di :bg in modo che corrisponda a quella della UI visibile. Quando l'utente esce, il processo della UI viene memorizzato nella cache e l'associazione non eleva più nulla.
  • Il worker che dipende dal processo della UI non è sicuro. Se il codice in :bg si associa a un servizio o esegue query su un provider di contenuti eseguito nel processo principale, il sistema eleva il processo principale all'importanza elevata del worker e la memoria della UI viene nuovamente bloccata. Fai in modo che la UI registri un callback con il worker, ad esempio un Messenger o un'interfaccia di callback AIDL. Il passaggio di un oggetto binder come callback non modifica l'importanza di nessuno dei due processi.

Annulla la registrazione del callback prima di annullare l'associazione, in modo che il worker non chiami mai il processo della UI dopo che il sistema lo ha bloccato. L'esempio seguente esegue l'associazione al worker solo quando la UI è visibile:

Kotlin

class MainActivity : Activity() {
    private var downloadService: IBinder? = null

    private val downloadConnection = object : ServiceConnection {
        override fun onServiceConnected(
                name: ComponentName,
                service: IBinder
        ) {
            downloadService = service
            // Register a Messenger or AIDL callback for progress updates.
        }

        override fun onServiceDisconnected(name: ComponentName) {
            // The binding stays active and reconnects if the worker restarts.
            downloadService = null
        }
    }

    override fun onStart() {
        super.onStart()
        val intent = Intent(this, AssetDownloadService::class.java)
        bindService(intent, downloadConnection, Context.BIND_AUTO_CREATE)
    }

    override fun onStop() {
        super.onStop()
        if (downloadService != null) {
            // Unregister the progress callback here, before unbinding.
            downloadService = null
        }
        // Unbind the ServiceConnection even if onServiceConnected hasn't run
        // yet.
        unbindService(downloadConnection)
    }
}

Java

public final class MainActivity extends Activity {
    private IBinder downloadService;

    private final ServiceConnection downloadConnection =
            new ServiceConnection() {
                @Override
                public void onServiceConnected(
                        ComponentName name, IBinder service) {
                    downloadService = service;
                    // Register a Messenger or AIDL callback for progress
                    // updates.
                }

                @Override
                public void onServiceDisconnected(ComponentName name) {
                    // The binding stays active and reconnects if the worker
                    // restarts.
                    downloadService = null;
                }
            };

    @Override
    protected void onStart() {
        super.onStart();
        Intent intent = new Intent(this, AssetDownloadService.class);
        bindService(intent, downloadConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (downloadService != null) {
            // Unregister the progress callback here, before unbinding.
            downloadService = null;
        }
        // Unbind the ServiceConnection even if onServiceConnected hasn't run
        // yet.
        unbindService(downloadConnection);
    }
}

Per saperne di più su come i flag di associazione influiscono su oom_score_adj tra i processi, consulta Associazioni di servizi e stati dei processi.

Coordinare lo stato condiviso su disco in modo sicuro

Poiché il processo principale e :bg vengono eseguiti in spazi di indirizzi separati con runtime e descrittori dei file separati:

  • SQLite e Room: utilizza il logging write-ahead (la modalità di journal predefinita in Room sulla maggior parte dei dispositivi) in modo che entrambi i processi possano leggere e scrivere nel database contemporaneamente. Non chiamare enableMultiInstanceInvalidation da :bg. Room ospita il proprio servizio di invalidazione nel processo principale, quindi il worker si associa al processo principale e mantiene la memoria della UI con l'importanza elevata del worker. Invece, fai in modo che il worker segnali le modifiche tramite il callback registrato dalla UI e che la UI esegua nuovamente una query sul database all'avvio.
  • Preferenze: non condividere SharedPreferences tra i processi. Utilizza DataStore nel codice multiprocesso o passa lo stato esplicitamente tramite IPC.
  • File degli asset: scrivi i pacchetti di asset scaricati in un file di gestione temporanea in :bg, chiama fdatasync e poi posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED) in :bg dopo la scrittura (come mostrato in Restituisci le pagine di memoria al sistema operativo) e rinomina il file in modo atomico. Lascia che l'interfaccia utente acquisisca il risultato al successivo avvio e associazione al worker, anziché chiamare un processo dell'interfaccia utente che potrebbe essere bloccato.

Osserva l'effetto nella shell adb

Puoi verificare che la suddivisione dei processi in primo piano e in background isoli l'utilizzo elevato della memoria esaminando gli stati dei processi di Activity Manager e dumpsys meminfo dopo aver eseguito la navigazione alla schermata Home. L'output di esempio in questa sezione proviene dall'app di esempio MemoryLab, che esegue la stessa allocazione nativa di 64 MB nel processo UI in entrambe le configurazioni e ospita il servizio in primo piano diviso in un processo :remote.

1. Confronta gli stati dei processi e i punteggi OOM

Avvia il workload in background, premi il pulsante Home in modo che Activity si interrompa ed esegui una query su Activity Manager:

adb shell dumpsys activity processes com.example.app | grep -E "Proc #|oom:|state:"

In un'architettura a processo singolo, il processo principale rimane elevato anche se il suo Activity viene interrotto. Per circa 15 secondi dopo che l'utente esce, Activity Manager mantiene un processo che esegue un servizio in primo piano su oom_score_adj = 50 (fg-service-act) e poi lo abbassa su 200 (fg-service). Il seguente output è stato acquisito 20 secondi dopo aver premuto Home:

    Proc #1: prcp     F/S/FGS  ---NFUATI  t: 0 13794:com.android.memorylab/u0a284 (fg-service)
        oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200
        state: cur=FGS  set=FGS   lastRss=0.00 lastCachedRss=0.00

In un'architettura a processi separati, solo il processo worker rimane in oom_score_adj = 200, mentre il processo UI principale scende a PREV (oom_score_adj = 700) e successivamente al bucket memorizzato nella cache (oom_score_adj >= 900):

    Proc #3: prcp     F/S/FGS  ---NFUATI  t: 0 13544:com.android.memorylab:remote/u0a284 (fg-service)
        oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200
        state: cur=FGS  set=FGS   lastRss=0.00 lastCachedRss=0.00
    Proc #1: prev     b/ /LAST -------TI  t: 0 13312:com.android.memorylab/u0a284 (previous)
        oom: max=1001 curRaw=700 setRaw=700 cur=700 set=700
        state: cur=LAST set=LAST  lastRss=0.00 lastCachedRss=0.00

Una volta scaduto il timeout dell'app precedente, il processo principale passa al bucket memorizzato nella cache (cch, oom_score_adj = 900, previous-expired) e perde tutte le sue funzionalità di processo (---------), incluso il tempo di CPU implicito (I). A quel punto, il sistema lo blocca e può compattarlo o recuperarlo in qualsiasi momento senza interrompere il processo worker.

2. Confronta dumpsys meminfo tra i processi

Confronta il riepilogo di dumpsys meminfo del worker con privilegi elevati con il processo della UI:

adb shell dumpsys meminfo com.example.app:bg
adb shell dumpsys meminfo com.example.app

Esempio di output App Summary per il processo worker MemoryLab all'indirizzo oom_score_adj = 200:

 App Summary
                       Pss(KB)                        Rss(KB)
                        ------                         ------
           Java Heap:      380                          13840
         Native Heap:     1548                           2832
                Code:      132                          55816
               Stack:      200                            208
            Graphics:        0                              0
       Private Other:      496
              System:     1926
             Unknown:                                    4168

           TOTAL PSS:     4682            TOTAL RSS:    76864       TOTAL SWAP PSS:      441

Output di esempio di App Summary per il processo della UI di MemoryLab in oom_score_adj = 700, che contiene l'allocazione nativa di 64 MB in Private Other:

 App Summary
                       Pss(KB)                        Rss(KB)
                        ------                         ------
           Java Heap:     1316                          19816
         Native Heap:    30408                          31596
                Code:     3148                         115796
               Stack:      656                            664
            Graphics:        0                              0
       Private Other:    67044
              System:    13687
             Unknown:                                   71092

           TOTAL PSS:   116259            TOTAL RSS:   238964       TOTAL SWAP PSS:      259

Il processo worker richiede meno di 5 MB di PSS, perché condivide la maggior parte delle sue pagine di codice e di runtime con Zygote. In un'app che esegue il rendering con Vulkan o OpenGL ES, la riga Graphics del processo UI include anche la memoria GPU. Questo esempio è stato acquisito su un dispositivo virtuale Cuttlefish con rendering software, quindi Graphics è 0 in entrambi i processi.

3. Ispeziona l'accounting cgroup per processo

Ogni processo con il tuo UID ha la propria directory cgroup v2 all'interno di un cgroup principale per la tua app. Il seguente comando elenca oom_score_adj, memory.current e memory.high a livello di app per ogni processo della tua app:

adb shell '
  for PID in $(pidof com.example.app com.example.app:bg); do
    CG=/sys/fs/cgroup$(sed -n "s/^0:://p" /proc/$PID/cgroup)
    echo "pid=$PID ($(tr -d "\0" < /proc/$PID/cmdline))" \
      "adj=$(cat /proc/$PID/oom_score_adj)" \
      "memory.current=$(cat $CG/memory.current)" \
      "app.memory.high=$(cat ${CG%/*}/memory.high)"
  done
'

Esempio di output per MemoryLab dopo aver premuto Home:

pid=15301 (com.android.memorylab) adj=700 memory.current=103260160 app.memory.high=966787072
pid=16518 (com.android.memorylab:remote) adj=200 memory.current=3608576 app.memory.high=966787072

Al processo worker vengono addebitati meno di 4 MB, mentre al processo UI vengono addebitati circa 98 MB. Ogni processo ha il proprio memory.current, ma il limitatore di memoria imposta il limite sul cgroup principale che contiene tutti i processi della tua app. Entrambe le righe mostrano lo stesso app.memory.high di 966.787.072 byte, ovvero il limite non visibile di 922 MB nell'output seguente.

Per visualizzare i limiti visibili e non visibili applicati dal limitatore di memoria sul dispositivo, esegui questo comando:

adb shell am memory-limiter status

Output di esempio da un dispositivo con 4 GB di RAM, in cui il limite non visibile è la metà del limite visibile:

Memory limiter
  enabled                  limits=true              killing=true             ignored=none
  visible:                 memHigh=1843MB           memMax=2048MB            swapMax=1024MB
  not-visible:             memHigh=922MB            memMax=1024MB            swapMax=1024MB

Osserva l'effetto in Perfetto

Perfetto ti consente di visualizzare la cronologia di oom_score_adj e RSS in entrambi i processi mentre l'utente passa dalla UI in primo piano alla schermata Home mentre il lavoro in background continua.

1. Registra una traccia

Registra una traccia di 15 secondi che acquisisce i contatori della memoria di processo, la memoria della GPU e le modifiche di oom_score_adj durante l'avvio di un job in background nella tua app e la navigazione alla schermata Home:

adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/process-splitting.perfetto-trace <<'EOF'
buffers: {
  size_kb: 32768
  fill_policy: DISCARD
}
data_sources: {
  config {
    name: "linux.process_stats"
    process_stats_config {
      scan_all_processes_on_start: true
      proc_stats_poll_ms: 250
    }
  }
}
data_sources: {
  config {
    name: "android.gpu.memory"
  }
}
data_sources: {
  config {
    name: "linux.ftrace"
    ftrace_config {
      ftrace_events: "oom/oom_score_adj_update"
      ftrace_events: "gpu_mem/gpu_mem_total"
    }
  }
}
duration_ms: 15000
EOF
adb pull /data/misc/perfetto-traces/process-splitting.perfetto-trace

2. Ispeziona le tracce dei processi nella UI di Perfetto

Apri la traccia nell'interfaccia utente di Perfetto ed espandi i gruppi di processi per com.example.app e com.example.app:bg:

  1. Blocca le tracce oom_score_adj e mem.rss.anon per entrambi i processi in modo che vengano visualizzate una accanto all'altra nella parte superiore della cronologia.
  2. Individua il timestamp in cui l'utente preme Home:
    • Nel processo worker, oom_score_adj rimane a 200 e mem.rss.anon rimane piatto e piccolo.
    • Nel processo UI, oom_score_adj sale da 0 a 200, rimane lì per circa un secondo mentre Activity si arresta, quindi sale a 700 (PREV). Dopo il timeout dell'app precedente, passa all'intervallo memorizzato nella cache, dove il sistema può compattare e recuperare la memoria senza influire sul worker.
Tracce di Perfetto per il processo della UI e il processo worker. Il valore oom_score_adj del processo dell'interfaccia utente sale da 0 a 200 e poi a 700 dopo Home, mentre il processo worker rimane a 200 con circa 19 MB di RSS anonimo.
Figura 2. Suddividi i processi dopo che l'utente ha premuto Home.

Nella Figura 2, il processo UI rimane brevemente a 200 mentre il suo Activity si interrompe e poi sale a 700, mentre il worker rimane a 200 con circa 19 MB di RSS anonimo. Per un confronto, la Figura 3 mostra una traccia di 35 secondi della configurazione in co-location, in cui il singolo processo rimane a 50 e poi a 200 con tutta la memoria della UI.

Tracce di Perfetto per un singolo processo co-localizzato. oom_score_adj
       sale da 0 a 50 dopo Home e a 200 circa 15 secondi dopo, mentre
       l'RSS anonimo rimane alto.
Figura 3. Un processo in co-location dopo che l'utente preme Home.

Per esplorare queste visualizzazioni, apri la traccia divisa e la traccia in co-location nell'interfaccia utente di Perfetto.

3. Esegui query sul footprint della memoria elevato con PerfettoSQL

Esegui la seguente query PerfettoSQL nella scheda Query (SQL) per misurare l'RSS anonimo e dei file medio di ogni processo della tua app a ogni livello oom_score_adj:

INCLUDE PERFETTO MODULE android.memory.process;

SELECT
  process_name,
  score AS oom_score_adj,
  ROUND(SUM(dur) / 1e9, 2) AS duration_sec,
  ROUND(SUM(anon_rss * dur) / SUM(dur) / 1048576.0, 1) AS avg_anon_mb,
  ROUND(SUM(file_rss * dur) / SUM(dur) / 1048576.0, 1) AS avg_file_mb
FROM memory_oom_score_with_rss_and_swap_per_process
WHERE process_name GLOB 'com.example.app*'
GROUP BY process_name, score
ORDER BY process_name, score;

Esempio di output per la traccia divisa nella Figura 2:

process_name                  oom_score_adj  duration_sec  avg_anon_mb  avg_file_mb
com.android.memorylab                     0          4.11         93.3        110.4
com.android.memorylab                   200          1.06        126.8        110.5
com.android.memorylab                   700          9.72        115.1        110.5
com.android.memorylab:remote              0          0.01         NULL         NULL
com.android.memorylab:remote            200         12.87         18.9         53.7

Esempio di output per la traccia in co-localizzazione nella Figura 3:

process_name           oom_score_adj  duration_sec  avg_anon_mb  avg_file_mb
com.android.memorylab              0          4.10         96.0        110.4
com.android.memorylab             50         16.04        117.6        110.5
com.android.memorylab            200         14.68        116.5        110.6

Nella configurazione divisa, solo circa 19 MB di memoria anonima rimangono in oom_score_adj = 200 dopo il primo secondo. Nella configurazione co-localizzata, circa 117 MB rimangono in 50 e poi in 200 per tutto il tempo in cui il servizio in primo piano è in esecuzione.


← Restituisci le pagine di memoria al sistema operativo | ↑ Su