Associazioni dei servizi e stati del processo

I processi delle app su Android non esistono in isolamento. 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'associazione dei servizi, crea una dipendenza che ha un impatto profondo sul modo in cui il framework Android gestisce la memoria.

Stati dei processi e punteggi OOM

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

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

Stati dei processi comuni

La tabella seguente mostra alcuni degli stati dei processi 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 del processo (abbr.) Descrizione oom_score_adj tipico
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 (percepibile) Processo in background di cui l'utente è a conoscenza (ad es. riproduzione di musica). 200
FGS Processo che ospita un servizio in primo piano. Da 0 a 200 (varia)
BTOP (Bound Top) Processo associato a un'applicazione TOP. 100
BFGS Servizio in primo piano associato (in genere associato al sistema). 0
PREV (precedente) L'ultimo processo in cui l'utente si trovava prima di quello attuale. 700
CACHED App in background che possono essere interrotte in sicurezza. Da 900 a 999

L'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, il processo server spesso eredita una priorità elevata. In questo modo, il servizio rimane disponibile finché il client ne ha bisogno.

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

Controllo dell'ereditarietà con i flag BIND

L'ereditarietà è il comportamento predefinito quando si utilizza Context.BIND_AUTO_CREATE. Tuttavia, gli sviluppatori possono controllare in che modo l'associazione 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 del servizio venga avviato e mantenuto attivo finché esiste l'associazione. Per impostazione predefinita, aumenta anche la priorità del processo 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). Questa opzione è utile per il lavoro in background che non deve competere con l'interfaccia utente per i cicli della CPU, ma deve comunque essere protetto dall'interruzione.
  • BIND_WAIVE_PRIORITY: un flag molto efficace che indica al sistema di non influire sulla priorità di pianificazione o di gestione della memoria del processo di destinazione. Il processo del servizio verrà gestito come se fosse un normale processo in background nell'elenco LRU, il che lo rende idoneo all'interruzione OOM anche se associato.
  • BIND_ABOVE_CLIENT: indica che il servizio è più importante dell'app client stessa. Quando il sistema deve recuperare memoria, preferirà interrompere l'app client prima di interrompere il servizio associato. Questo flag è "più efficace" di BIND_AUTO_CREATE perché fornisce un ulteriore livello di protezione per il servizio a scapito 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.

Esercitazione: osservare gli effetti dell'associazione

Utilizzeremo l'applicazione MemoryLab per mostrare in che modo un'associazione da 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. Identifica i processi

Controlla gli stati dei processi 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

Snippet di output di esempio:

  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. Il processo :remote non è ancora stato avviato.

3. Attiva l'associazione

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 dei processi:

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

Snippet di output di esempio:

  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. Questo è molto più protetto di un normale 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 tasto HOME sul dispositivo. Controlla di nuovo gli stati:

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

Snippet di output di esempio:

    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

Snippet di output di esempio:

  *   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 associato a un'applicazione nello stato TOP.

Acquisizione e analisi delle associazioni con Perfetto

Mentre dumpsys fornisce uno snapshot, Perfetto ti consente di vedere il momento esatto in cui si verifica un'associazione e come cambia il punteggio OOM in tempo reale.

1. Registra una traccia

Utilizza una configurazione che includa linux.process_stats e la categoria di traccia 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. Esegui una query sulle transizioni del punteggio OOM

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. Identifica gli eventi di associazione

Per vedere esattamente quando è stata stabilita una dipendenza di associazione 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 associa 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 altre associazioni per impedire 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 incorporato nel sistema operativo del loro smartphone, che possa essere richiamato istantaneamente con una hotword parlata o un gesto di input rapido e che l'interazione sia fluida e senza interruzioni.

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

Diagramma che mostra il binding di system_server al processo interattore dell'app Google

Se controlli gli stati dei processi (ad es. utilizzando dumpsys activity processes), potresti vedere un processo come com.google.android.googlequicksearchbox:interactor nello stato BFGS (Bound Foreground Service), mantenuto attivo da un'associazione da system_server (UID 1000).

NotificationListenerService

Per alcune associazioni da sistema ad app, l'obiettivo non è la latenza, ma piuttosto la prevenzione di avvii a freddo frequenti. NotificationListenerService, un servizio che riceve chiamate dal sistema quando vengono pubblicate o rimosse nuove notifiche, è un ottimo esempio. Un utente di smartphone tipico può ricevere centinaia di notifiche durante il giorno. Se il sistema annullasse l'associazione a un listener di notifiche, il processo dell'app probabilmente passerebbe 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 capo solo per recapitare 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) di Avvio applicazioni

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

Quando scorri verso sinistra sulla schermata Home per visualizzare il feed di notizie, la transizione deve essere fluida. Avvio applicazioni raggiunge questo obiettivo associandosi a un'interfaccia di servizio nell'app che fornisce il feed di notizie e mantenendo attiva l'associazione finché Avvio applicazioni è attivo. In questo modo, i contenuti del feed vengono visualizzati e sono pronti in memoria anche quando non li stai guardando.

Altri esempi comuni

  • Avvio applicazioni (HOME_APP_ADJ): l'app di avvio applicazioni (Home) ha un proprio slot speciale nell'elenco delle priorità. Sebbene non sia sempre associata a un servizio, le viene assegnato HOME_APP_ADJ (in genere 600). Il sistema preferisce mantenere attivo Avvio applicazioni, perché l'utente vi torna spesso. Infatti, il sistema preferirebbe interrompere l'app utilizzata in precedenza (PREV_APP_ADJ = 700) piuttosto che interrompere Avvio applicazioni, perché l'interruzione di Avvio applicazioni comporterebbe un'esperienza utente lenta all'uscita da qualsiasi app, in quanto l'utente dovrebbe attendere l'avvio a freddo di Avvio applicazioni.
  • Input Method Editor (IME): quando digiti, il sistema si associa all' app della tastiera scelta (ad es. Gboard). In questo modo, il processo della tastiera rimane in uno stato elevato anche se la tastiera è 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 associa 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 dovesse essere avviata a freddo, la transazione potrebbe andare in timeout e non riuscire.

Compromessi e il limite di prestazioni

Sebbene le associazioni siano necessarie per le prestazioni e la correttezza, comportano un costo per l'integrità della memoria del sistema.

  • Flessibilità ridotta: ogni processo associato è un processo che LMK non può interrompere 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 limite di prestazioni: se troppi processi sono associati, il sistema potrebbe ritrovarsi con quasi nessun processo in background interrompibile. Quando la pressione della memoria aumenta, il sistema "supererà il limite di prestazioni" molto più rapidamente, in quanto è costretto a interrompere processi più importanti o a eseguire il thrashing della cache delle pagine.

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