Associazioni dei servizi e stati del processo

I processi delle app su Android non esistono in modo isolato. Le applicazioni spesso si basano su servizi forniti da altre applicazioni o dal sistema stesso. Quando un processo si connette a un altro tramite un Service Binding, crea una dipendenza che ha un profondo impatto sul modo in cui il framework Android gestisce la memoria.

Stati del processo e punteggi OOM

Il framework Android utilizza gli stati del processo per monitorare l'importanza di ogni processo in esecuzione. Questi stati vengono poi utilizzati dal OomAdjuster per assegnare un valore di aggiustamento del punteggio OOM (oom_score_adj) compreso tra -1000 e 1000.

Un valore di oom_score_adj inferiore indica che il processo è più importante e meno probabile che venga interrotto da Low Memory Killer (LMK).

Stati processo comuni

La tabella seguente mostra alcuni degli stati di elaborazione più comuni e i relativi valori oom_score_adj tipici. Per un elenco completo e aggiornato, consulta android.app.ActivityManager e com.android.server.am.psc.Constants nel codice sorgente di Android.

Stato processo (abbreviazione) Descrizione Valore tipico: oom_score_adj
PER (persistente) Processi di sistema che devono essere sempre in esecuzione (ad es. Telefonia). -800
TOP Il processo con cui l'utente sta interagendo. 0
VIS (visibile) Il processo ha un'attività visibile (ad es. dietro una finestra di dialogo traslucida). 100
PERC (Perceptible) Processo in background di cui l'utente è a conoscenza (ad es. riproduzione di musica). 200
FGS Processo di hosting di un servizio in primo piano. Da 0 a 200 (varia)
BTOP (Bound Top) Processo vincolato da un'applicazione TOP. 100
BFGS Servizio in primo piano associato (in genere associato al sistema). 0
PREV (Indietro) L'ultimo processo in cui si trovava l'utente prima di quello attuale. 700
COPIA CACHE App in background che possono essere chiuse in modo sicuro. Da 900 a 999

Impatto delle associazioni dei servizi

Quando un processo client (ad es. un'app nello stato TOP) si associa a un servizio in un processo server, quest'ultimo spesso eredita una priorità elevata. In questo modo, il servizio rimane disponibile finché il cliente ne ha bisogno.

Diagramma che mostra il processo A (TOP) che chiama bindService() tramite system_server,
elevando il processo B a BTOP

Controllare l'ereditarietà con i flag BIND

L'ereditarietà è il comportamento predefinito quando utilizzi Context.BIND_AUTO_CREATE. Tuttavia, gli sviluppatori possono controllare in che modo il binding influisce sull'importanza del processo di destinazione utilizzando vari flag in bindService().

Flag BIND chiave per il punteggio OOM

I seguenti flag sono più pertinenti quando si gestisce la pressione della memoria a livello di sistema:

  • BIND_AUTO_CREATE: il flag più comune. Garantisce che il processo di servizio venga avviato e mantenuto attivo finché esiste il binding. Per impostazione predefinita, aumenta anche la priorità del processo del server in modo che corrisponda a quella del client.
  • BIND_NOT_FOREGROUND: impedisce che la priorità di pianificazione (priorità della CPU) del processo del servizio di destinazione venga aumentata al primo piano. Tuttavia, consente comunque di aumentare la priorità della memoria (oom_score_adj). Questo è utile per il lavoro in background che non deve competere con la UI per i cicli della CPU, ma deve comunque essere protetto dalla chiusura.
  • BIND_WAIVE_PRIORITY: un flag molto forte che indica al sistema di non influire sulla priorità di pianificazione o di gestione della memoria del processo di destinazione. Il processo di servizio verrà gestito come se fosse un normale processo in background nell'elenco LRU, rendendolo idoneo all'interruzione per esaurimento della memoria anche durante l'associazione.
  • BIND_ABOVE_CLIENT: indica che il servizio è più importante dell'app client stessa. Quando il sistema deve recuperare memoria, preferisce terminare l'app client prima di terminare il servizio associato. È "più forte" di BIND_AUTO_CREATE perché fornisce un ulteriore livello di protezione per il servizio a spese del client.
  • BIND_NOT_PERCEPTIBLE: riduce l'importanza del servizio di destinazione al di sotto del livello PERCEPTIBLE, consentendo al sistema di recuperare la memoria per fare spazio a processi più critici percepibili dall'utente.

Attività pratica: osservare gli effetti del binding

Utilizzeremo l'applicazione MemoryLab per mostrare come un binding di un'app TOP influisce sullo stato di un processo separato.

1. Avvia MemoryLab

Il seguente comando avvia l'app. Dopo l'apertura, assicurati che l'app rimanga in primo piano (non premere ancora il tasto Home o cambiare app).

adb shell am start -n com.android.memorylab/.MainActivity

2. Identificare i processi

Controlla gli stati del processo prima dell'associazione. MemoryLab esegue la sua UI principale in un processo e ha un RemoteService che viene eseguito in un processo :remote.

adb shell dumpsys activity processes com.android.memorylab

Esempio di snippet di output:

  Process OOM control (154 total, non-act at 7, non-svc at 7):
    Proc #0: fg       T/A/TOP  LCMNFUATI  t: 0 13470:com.android.memorylab/u0a417 (top-activity)
        oom: max=1001 curRaw=0 setRaw=0 cur=0 set=0
        state: cur=TOP  set=TOP   lastRss=0.00 lastCachedRss=0.00

Vedrai il processo principale com.android.memorylab nello stato TOP. La procedura :remote non è ancora iniziata.

3. Associazione di trigger

Invia una trasmissione all'app per attivare l'associazione dei servizi:

adb shell am broadcast -a com.android.memorylab.LEAK_BINDER

4. Osserva lo stato elevato

Controlla di nuovo gli stati del processo:

adb shell dumpsys activity processes | grep -A 10 "com.android.memorylab"

Esempio di snippet di output:

  Proc #  1: vis      F/ /BTOP ---NFUATI  t: 0 13560:com.android.memorylab:remote/u0a417 (service)
    com.android.memorylab/.RemoteService<=Proc{13470:com.android.memorylab/u0a417}
    oom: max=1001 curRaw=100 setRaw=100 cur=100 set=100
    state: cur=BTOP set=BTOP  lastRss=0.00 lastCachedRss=0.00

Il processo :remote è ora in esecuzione e nello stato BTOP (Bound TOP) con un oom_score_adj di 100. È molto più protetto di un tipico servizio in background (che sarebbe a 500 o superiore). La notazione <=Proc{...} mostra il processo responsabile di questo aumento di priorità.

5. Invia in background

Premi il pulsante HOME sul dispositivo. Controlla di nuovo gli stati:

adb shell dumpsys activity processes | grep -A 10 "com.android.memorylab"

Esempio di snippet di output:

    Proc #  2: prev     b/ /LAST --------I  t: 0 13560:com.android.memorylab:remote/u0a417 (service)
        com.android.memorylab/.RemoteService<=Proc{13470:com.android.memorylab/u0a417}
        oom: max=1001 curRaw=700 setRaw=700 cur=700 set=700
        state: cur=LAST set=LAST  lastRss=0.00 lastCachedRss=0.00
    Proc #  1: prev     b/ /LAST --------I  t: 0 13470:com.android.memorylab/u0a417 (previous)
        oom: max=1001 curRaw=700 setRaw=700 cur=700 set=700
        state: cur=LAST set=LAST  lastRss=209MB lastCachedRss=0.00

Ora entrambi i processi sono passati a uno stato di priorità inferiore (PREV / oom_score_adj 700), perché il processo client non è più TOP. (Nota: LAST nel dump dello stato si riferisce allo stato interno LAST_ACTIVITY, che corrisponde a PREV nei riepiloghi di alto livello).

Analisi con procstats

Lo strumento procstats fornisce una visualizzazione cronologica di questi stati.

# View stats for MemoryLab over the last hour
adb shell dumpsys procstats --hours 1 com.android.memorylab

Esempio di snippet di output:

  *   com.android.memorylab / u0a417 / v37:
      *   Prc com.android.memorylab / u0a417 / v37:
             TOTAL: 0.89% (0.00-0.00-0.00/0.00-0.00-0.00/210MB-210MB-210MB over 1)
               Top: 0.89% (0.00-0.00-0.00/0.00-0.00-0.00/210MB-210MB-210MB over 1)
      *   Prc com.android.memorylab:remote / u0a417 / v37:
             TOTAL: 0.19%
           Bnd Top: 0.19%

In questo caso, Bnd Top indica la percentuale di tempo in cui il processo remoto è stato vincolato da un'applicazione nello stato TOP.

Acquisizione e analisi dei binding con Perfetto

dumpsys ti offre un'istantanea, mentre Perfetto ti consente di vedere il momento esatto in cui si verifica un binding e come cambia il punteggio OOM in tempo reale.

1. Registrare una traccia

Utilizza una configurazione che includa linux.process_stats e la categoria atrace am:

adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/service_bindings.perfetto-trace <<EOF
buffers: { size_kb: 65536 }
data_sources: {
    config {
        name: "linux.process_stats"
        process_stats_config { proc_stats_poll_ms: 100 }
    }
}
data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config { ftrace_events: "am/am_proc_bound" }
    }
}
duration_ms: 15000
EOF

2. Transizioni del punteggio OOM delle query

Utilizzando PerfettoSQL, puoi vedere come è cambiato il punteggio OOM del processo remoto rispetto al processo dell'interfaccia utente:

SELECT ts, p.name, value AS oom_score_adj
FROM counter c
JOIN process_counter_track t ON c.track_id = t.id
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.memorylab%'
  AND t.name = 'oom_score_adj'
ORDER BY ts;

3. Identificare gli eventi di binding

Per vedere esattamente quando è stata stabilita una dipendenza di binding e quale processo l'ha avviata, utilizza questa query:

SELECT
    s.ts,
    p.name AS process_name,
    t.name AS thread_name,
    s.name AS slice_name
FROM slice s
JOIN thread_track tt ON s.track_id = tt.id
JOIN thread t USING (utid)
JOIN process p USING (upid)
WHERE s.name LIKE 'bindService:{com.android.memorylab%';

Associazioni da sistema ad app

Il sistema Android stesso spesso si lega ai servizi nelle app di terze parti per fornire funzionalità di base. Spesso l'obiettivo di queste associazioni è la riduzione della latenza. Mantenendo un processo attivo e in memoria, il sistema evita il costoso overhead di un "avvio a freddo" (caricamento dell'APK, inizializzazione del runtime e creazione dell'oggetto Application) quando si verifica un'interazione utente critica. Esistono altri binding per evitare avvii a freddo frequenti per le app che devono gestire flussi di eventi in background.

Ecco alcuni esempi reali che puoi osservare su un dispositivo tipico:

VoiceInteractor

Gli utenti si aspettano che un assistente digitale sia integrato nel sistema operativo del proprio smartphone, di poterlo richiamare istantaneamente con una hotword vocale o un gesto di input rapido e che l'interazione sia fluida e senza interruzioni.

Quando si verifica un trigger dell'assistente (ad esempio la hotword "Hey Google" sugli smartphone Google Pixel), l'assistente digitale deve rispondere immediatamente. Per garantire questo, system_server mantiene un binding permanente con il servizio di interazione vocale selezionato dall'utente.

Diagramma che mostra l'associazione di system_server al processo interattore dell'app Google

Se controlli gli stati dei processi (ad es. utilizzando dumpsys activity processes), potresti visualizzare un processo come com.google.android.googlequicksearchbox:interactor nello stato BFGS (servizio di primo piano associato), mantenuto attivo da un'associazione da system_server (UID 1000).

NotificationListenerService

Per alcuni binding da sistema ad app, l'obiettivo non è la latenza, ma piuttosto la prevenzione di frequenti avvii a freddo. NotificationListenerService, un servizio che riceve chiamate dal sistema quando vengono pubblicate o rimosse nuove notifiche, è un ottimo esempio. Un tipico utente di smartphone può ricevere centinaia di notifiche durante il giorno. Se il sistema si scollega da un listener di notifiche, il processo dell'app probabilmente passerà allo stato memorizzato nella cache e potrebbe essere interrotto da LMK.

Quando arriva la notifica successiva, potenzialmente pochi secondi dopo, il sistema sarebbe costretto ad avviare a freddo il processo dell'app da zero solo per inviare l'evento. Questo ciclo costante di interruzione e avvio a freddo consumerebbe molta più CPU e batteria rispetto al semplice mantenimento del processo associato e attivo in background.

La schermata -1 (feed di notizie) del launcher

Le app di avvio moderne in genere combinano la funzionalità di navigazione di base (icone e widget della home page) con un feed di notizie disponibile in una delle schermate di avvio e integrato perfettamente con l'esperienza utente di avvio. Il feed di notizie potrebbe essere fornito da un'altra app. Ad esempio, su Google Pixel il launcher si integra con un feed fornito dall'app Google.

Quando scorri verso sinistra sulla schermata Home per visualizzare il feed delle notizie, la transizione deve essere fluida. Il launcher lo fa eseguendo il binding a un'interfaccia di servizio nell'app che fornisce il feed di notizie e mantenendo attivo il binding finché il launcher è attivo. In questo modo, i contenuti del feed vengono sottoposti a rendering e sono pronti in memoria anche quando non li stai guardando.

Altri esempi comuni

  • Avvio app (HOME_APP_ADJ): l'app di avvio app (Home) ha un proprio slot speciale nell'elenco delle priorità. Anche se non sempre vincolato a un servizio, gli viene assegnato il HOME_APP_ADJ (in genere 600). Il sistema preferisce mantenere attivo il launcher, perché l'utente lo utilizza spesso. Infatti, il sistema preferisce chiudere l'app utilizzata in precedenza (PREV_APP_ADJ = 700) piuttosto che chiudere l'avvio app, perché la chiusura dell'avvio app comporterebbe un'esperienza utente lenta all'uscita da qualsiasi app, poiché l'utente dovrà attendere l'avvio a freddo dell'avvio app.
  • Input Method Editor (IME): durante la digitazione, il sistema si lega all'app tastiera scelta (ad es. Gboard). In questo modo, il processo della tastiera rimane in uno stato elevato anche se la tastiera viene temporaneamente nascosta. In questo modo, la tastiera può riapparire immediatamente quando tocchi un altro campo di testo.
  • Pagamenti NFC: quando tocchi lo smartphone per pagare, il sistema si lega al servizio di pagamento NFC (ad es. Google Wallet). Queste transazioni spesso hanno requisiti rigorosi in tempo reale dal terminale del commerciante. Se l'app di pagamento avesse dovuto avviarsi a freddo, la transazione potrebbe scadere e non andare a buon fine.

Compromessi e calo delle prestazioni

Sebbene i binding siano necessari per le prestazioni e la correttezza, hanno un costo per l'integrità della memoria del sistema.

  • Flessibilità ridotta: ogni processo vincolato è un processo che LMK non può terminare facilmente. In questo modo si riduce il "cuscinetto" di processi memorizzati nella cache che il sistema può utilizzare per liberare memoria sotto pressione.
  • Aggravamento del calo delle prestazioni: se sono associati troppi processi, il sistema potrebbe ritrovarsi con quasi nessun processo in background terminabile. Quando la pressione della memoria aumenta, il sistema "precipita" molto più velocemente, poiché è costretto a terminare processi più importanti o a svuotare la cache delle pagine.

Anti-pattern comuni di associazione dei servizi

Poiché i service binding aumentano direttamente oom_score_adj, piccoli errori del ciclo di vita nel modo in cui acquisisci o strutturi i binding possono bloccare grandi quantità di memoria in stati privilegiati (BTOP, BFGS o PERC) per ore. Fai attenzione a questi anti-pattern comuni durante la progettazione o il controllo dei servizi associati.

unbindService() dimenticato nei client di lunga durata

Il binding a un servizio da un singleton Application, un gestore in background o un Activity che chiama bindService() in onStart() senza un unbindService() corrispondente in onStop() comporta una perdita di ServiceConnection.

Finché questo binding rimane attivo, il processo di destinazione eredita la priorità elevata del client. Se il client è un componente di sistema persistente o un'app in primo piano, il processo del servizio associato rimane bloccato in BFGS o BTOP indefinitamente (mostrando un valore vicino al 100% in procstats), impedendo al LMK di recuperare la memoria anche quando il servizio è completamente inattivo.

Soluzione: limita i binding dell'ambito al ciclo di vita del componente che li richiede oppure implementa un timeout di inattività che chiama unbindService() dopo un periodo di inattività. Evita di annullare e ripetere l'associazione a ogni singola chiamata RPC, che causa un'attività eccessiva del processo e un sovraccarico ripetuto della configurazione di Binder. Raggruppa invece i burst di lavoro dietro un breve timer di inattività (ad esempio, da 5 a 30 secondi).

Collocazione di un servizio associato con un'interfaccia utente ad alta intensità di memoria

Per impostazione predefinita, tutti i componenti di un APK vengono eseguiti nello stesso processo. Se la tua app espone un servizio associato leggero (ad esempio un NotificationListenerService, un provider di widget o un servizio di plug-in a cui il sistema o il launcher si associa) nello stesso processo del Activity principale, l'intero processo eredita lo stato elevato del servizio (BFGS o PERC, in genere oom_score_adj di 200 o inferiore).

Quando l'utente apre la UI della tua app, il processo alloca grandi gerarchie di visualizzazione, bitmap decodificati e buffer grafici. Quando l'utente esce dall'app, il processo non scende a CACHED (oom_score_adj di 900 o superiore) perché l'associazione dei servizi attiva mantiene il processo elevato. Ciò causa due problemi combinati:

  • Nessuna compattazione della memoria in background: CachedAppOptimizer del sistema compatta i processi solo dopo che entrano nello stato CACHED.
  • Nessun taglio della memoria LRU memorizzata nella cache o recupero LMK: il sistema non fornisce callback di taglio in background associati all'elenco LRU memorizzato nella cache (TRIM_MEMORY_BACKGROUND e versioni successive) mentre un'associazione dei servizi mantiene il processo in uno stato elevato. Se la tua app non rilascia esplicitamente le risorse UI su TRIM_MEMORY_UI_HIDDEN o Activity.onStop(), le allocazioni UI di picco rimangono bloccate nella RAM con un punteggio OOM privilegiato in cui LMK non può recuperarle facilmente.

Soluzione: elimina esplicitamente le cache dell'interfaccia utente, le bitmap decodificate e i riferimenti alle visualizzazioni quando l'interfaccia utente smette di essere visibile, utilizzando Activity.onStop(), ComponentCallbacks2.onTrimMemory(TRIM_MEMORY_UI_HIDDEN), Application.ActivityLifecycleCallbacks o ProcessLifecycleOwner. In alternativa, sposta il servizio sempre associato in un processo separato e leggero utilizzando l'attributo manifest android:process in modo che il sistema possa spostare il processo dell'UI principale nello stato CACHED per compattare o recuperare la memoria in modo indipendente.

Omissione dei flag di rinuncia alla priorità

La chiamata di bindService() con BIND_AUTO_CREATE trasferisce la priorità di pianificazione e memoria completa del chiamante al servizio di destinazione. Quando un'app in primo piano si associa a un servizio di analisi, logging o prefetching in background utilizzando solo BIND_AUTO_CREATE, promuove involontariamente il worker in background a BTOP.

Soluzione: quando esegui il binding a servizi ausiliari o best-effort che non richiedono lo stesso livello di protezione dell'interfaccia utente in primo piano, combina BIND_AUTO_CREATE con BIND_WAIVE_PRIORITY, BIND_NOT_FOREGROUND o BIND_NOT_PERCEPTIBLE in modo che il sistema possa comunque gestire il processo di destinazione nell'elenco LRU memorizzato nella cache.


← Località | ↑ Su | A livello di sistema →