Liaisons de service et états de processus

Les processus d'application sur Android n'existent pas de manière isolée. Les applications s'appuient souvent sur des services fournis par d'autres applications ou par le système lui-même. Lorsqu'un processus se connecte à un autre via une liaison de service, il crée une dépendance qui a un impact profond sur la façon dont le framework Android gère la mémoire.

États des processus et scores OOM

Le framework Android utilise les états de processus pour suivre l'importance de chaque processus en cours d'exécution. Ces états sont ensuite utilisés par OomAdjuster pour attribuer une valeur OOM Score Adjustment (oom_score_adj), qui peut aller de -1 000 à 1 000.

Un oom_score_adj plus faible signifie que le processus est plus important et moins susceptible d'être arrêté par le Low Memory Killer (LMK).

États de processus courants

Le tableau suivant présente certains des états de processus les plus courants et leurs valeurs oom_score_adj typiques. Pour obtenir une liste complète et à jour, consultez android.app.ActivityManager et com.android.server.am.psc.Constants dans le code source Android.

État du processus (abrégé) Description oom_score_adj type
PER (persistant) Processus système qui doivent toujours être exécutés (par exemple, la téléphonie). -800
TOP Processus avec lequel l'utilisateur interagit actuellement. 0
VIS (visible) Le processus comporte une activité visible (par exemple, derrière une boîte de dialogue translucide). 100
PERC (Perceptible) Processus en arrière-plan dont l'utilisateur est informé (par exemple, la lecture de musique). 200
FGS Processus hébergeant un service de premier plan. 0 à 200 (variable)
BTOP (Bound Top) Processus lié à une application TOP. 100
BFGS Service de premier plan lié (généralement lié au système). 0
PREV (Précédent) Dernier processus dans lequel l'utilisateur se trouvait avant celui en cours. 700
EN CACHE Applications en arrière-plan pouvant être arrêtées sans risque. 900 à 999

Impact des liaisons de service

Lorsqu'un processus client (par exemple, une application à l'état TOP) se lie à un service dans un processus serveur, le processus serveur hérite souvent d'une priorité élevée. Cela garantit que le service reste disponible aussi longtemps que le client en a besoin.

Schéma montrant le processus A (TOP) appelant bindService() via system_server, ce qui élève le processus B à BTOP

Contrôler l'héritage avec les indicateurs BIND

L'héritage est le comportement par défaut lorsque vous utilisez Context.BIND_AUTO_CREATE. Toutefois, les développeurs peuvent contrôler l'impact de la liaison sur l'importance du processus cible à l'aide de différents indicateurs dans bindService().

Indicateurs BIND clés pour le score OOM

Les indicateurs suivants sont les plus pertinents pour gérer la pression exercée sur la mémoire à l'échelle du système :

  • BIND_AUTO_CREATE : le flag le plus courant. Il garantit que le processus de service est démarré et maintenu en vie tant que la liaison existe. Par défaut, il élève également la priorité du processus serveur pour qu'elle corresponde à celle du client.
  • BIND_NOT_FOREGROUND : empêche le processus du service cible d'être élevé à la priorité de planification (priorité du CPU) au premier plan. Toutefois, il permet toujours d'élever la priorité de la mémoire (oom_score_adj). Cela est utile pour les tâches en arrière-plan qui ne doivent pas entrer en concurrence avec l'UI pour les cycles de processeur, mais qui doivent tout de même être protégées contre l'arrêt.
  • BIND_WAIVE_PRIORITY : indicateur très fort qui indique au système de ne pas impacter la priorité de planification ou de gestion de la mémoire du processus cible. Le processus de service sera géré comme s'il s'agissait d'un processus d'arrière-plan normal sur la liste LRU, ce qui le rend éligible à l'arrêt OOM même lorsqu'il est lié.
  • BIND_ABOVE_CLIENT : indique que le service est plus important que l'application cliente elle-même. Lorsque le système doit récupérer de la mémoire, il préfère arrêter l'application cliente avant d'arrêter le service lié. Cette méthode est "plus forte" que BIND_AUTO_CREATE, car elle offre une couche de protection supplémentaire pour le service au détriment du client.
  • BIND_NOT_PERCEPTIBLE : réduit l'importance du service cible à un niveau inférieur à PERCEPTIBLE, ce qui permet au système de récupérer sa mémoire pour faire de la place à des processus plus critiques et perceptibles par l'utilisateur.

Atelier pratique : observer les effets de la liaison

Nous utiliserons l'application MemoryLab pour montrer comment une liaison à partir d'une application TOP affecte l'état d'un processus distinct.

1. Lancer MemoryLab

La commande suivante lance l'application. Une fois l'application ouverte, assurez-vous qu'elle reste au premier plan (n'appuyez pas encore sur le bouton d'accueil et ne changez pas d'application).

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

2. Identifier les processus

Vérifiez les états du processus avant la liaison. MemoryLab exécute son UI principale dans un processus et possède un RemoteService qui s'exécute dans un processus :remote.

adb shell dumpsys activity processes com.android.memorylab

Exemple d'extrait de résultat :

  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

Le processus principal com.android.memorylab s'affiche à l'état TOP. La procédure :remote n'a pas encore commencé.

3. Liaison de déclencheur

Envoyez une diffusion à l'application pour déclencher la liaison de service :

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

4. Observer l'état élevé

Vérifiez à nouveau les états des processus :

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

Exemple d'extrait de résultat :

  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

Le processus :remote est maintenant en cours d'exécution et se trouve dans l'état BTOP (Bound TOP) avec une oom_score_adj de 100. Ce niveau de protection est nettement supérieur à celui d'un service d'arrière-plan classique (qui serait au niveau 500 ou supérieur). La notation <=Proc{...} indique le processus responsable de cette élévation de priorité.

5. Mettre à l'arrière-plan

Appuyez sur le bouton ACCUEIL de l'appareil. Vérifiez à nouveau les états :

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

Exemple d'extrait de résultat :

    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

Les deux processus sont désormais passés à un état de priorité inférieure (PREV/oom_score_adj 700), car le processus client n'est plus TOP. (Remarque : LAST dans le vidage d'état fait référence à l'état interne LAST_ACTIVITY, qui correspond à PREV dans les récapitulatifs de haut niveau.)

Analyser avec procstats

L'outil procstats fournit un historique de ces états.

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

Exemple d'extrait de résultat :

  *   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%

Ici, Bnd Top indique le pourcentage de temps pendant lequel le processus à distance a été lié par une application dans l'état TOP.

Capturer et analyser les liaisons avec Perfetto

Alors que dumpsys vous donne un aperçu, Perfetto vous permet de voir le moment exact où une liaison se produit et comment le score OOM change en temps réel.

1. Enregistrer une trace

Utilisez une configuration qui inclut linux.process_stats et la catégorie 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. Transitions du score OOM des requêtes

À l'aide de PerfettoSQL, vous pouvez voir comment le score OOM du processus distant a changé par rapport au processus d'UI :

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. Identifier les événements de liaison

Pour savoir exactement quand une dépendance de liaison a été établie et quel processus l'a initiée, utilisez cette requête :

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%';

Liaisons système vers application

Le système Android lui-même se lie souvent aux services des applications tierces pour fournir des fonctionnalités de base. L'objectif de ces liaisons est souvent de réduire la latence. En gardant un processus actif et en mémoire, le système évite la surcharge coûteuse d'un "démarrage à froid" (chargement de l'APK, initialisation de l'exécution et création de l'objet Application) lorsqu'une interaction de l'utilisateur critique se produit. D'autres liaisons existent pour éviter les démarrages à froid fréquents pour les applications qui doivent gérer des flux d'événements en arrière-plan.

Voici quelques exemples concrets que vous pouvez observer sur un appareil typique :

VoiceInteractor

Les utilisateurs s'attendent à ce qu'un assistant numérique soit intégré à l'OS de leur téléphone, à pouvoir l'invoquer instantanément avec un mot clé vocal ou un geste d'entrée rapide, et à ce que l'interaction soit fluide et transparente.

Lorsqu'un déclencheur d'assistant se produit (comme le mot clé "OK Google" sur les téléphones Google Pixel), l'assistant numérique doit répondre instantanément. Pour ce faire, system_server maintient une liaison permanente avec le service d'interaction vocale sélectionné par l'utilisateur.

Schéma montrant la liaison system_server au processus d'interaction de l'appli Google

Si vous vérifiez les états des processus (par exemple, à l'aide de dumpsys activity processes), vous pouvez voir un processus tel que com.google.android.googlequicksearchbox:interactor dans l'état BFGS (service de premier plan lié), maintenu en vie par une liaison à partir de system_server (UID 1000).

NotificationListenerService

Pour certaines liaisons système-application, l'objectif n'est pas la latence, mais plutôt d'éviter les démarrages à froid fréquents. NotificationListenerService, un service qui reçoit des appels du système lorsque de nouvelles notifications sont publiées ou supprimées, en est un excellent exemple. Un utilisateur de smartphone typique peut recevoir des centaines de notifications tout au long de la journée. Si le système se dissocie d'un écouteur de notifications, le processus de cette application passera probablement à l'état mis en cache et pourra être arrêté par le LMK.

Lorsque la notification suivante arrive (potentiellement quelques secondes plus tard), le système est obligé de redémarrer à froid le processus de l'application juste pour transmettre l'événement. Ce cycle constant d'arrêt et de démarrage à froid consommerait beaucoup plus de processeur et de batterie que le simple maintien du processus lié et actif en arrière-plan.

L'écran "-1" du lanceur d'applications (flux d'actualités)

Les applications de lanceur d'applications modernes combinent généralement la fonctionnalité de navigation de base (icônes et widgets de l'écran d'accueil) avec un flux d'actualités disponible sur l'un des écrans du lanceur d'applications et parfaitement intégré à l'UI du lanceur d'applications. Le flux d'actualités peut être fourni par une autre application. Par exemple, sur Google Pixel, le lanceur d'applications s'intègre à un flux fourni par l'appli Google.

Lorsque vous balayez l'écran d'accueil vers la gauche pour afficher le flux d'actualités, la transition doit être fluide. Pour ce faire, le lanceur d'applications se lie à une interface de service dans l'application qui fournit le flux d'actualités et maintient cette liaison active tant que le lanceur d'applications est actif. Le contenu du flux est ainsi rendu et prêt en mémoire, même lorsque vous ne le regardez pas.

Autres exemples courants

  • Lanceur d'applications (HOME_APP_ADJ) : l'application de lanceur d'applications (Home) dispose de son propre emplacement spécial dans la liste des priorités. Bien qu'il ne soit pas toujours lié à un service, il est associé à l'ID HOME_APP_ADJ (généralement 600). Le système préfère maintenir le lanceur d'applications en vie, car l'utilisateur y revient fréquemment. En fait, le système préfère arrêter l'application précédemment utilisée (PREV_APP_ADJ = 700) plutôt que le lanceur d'applications, car cela entraînerait une expérience utilisateur lente lors de la fermeture d'une application, car l'utilisateur devrait attendre le démarrage à froid du lanceur d'applications.
  • Éditeur de méthode de saisie (IME) : lorsque vous saisissez du texte, le système se lie à l'application de clavier que vous avez choisie (Gboard, par exemple). Le processus du clavier reste dans un état élevé même si le clavier est temporairement masqué. Cela permet au clavier de réapparaître instantanément lorsque vous appuyez sur un autre champ de texte.
  • Paiements NFC : lorsque vous appuyez sur votre téléphone pour payer, le système se lie au service de paiement NFC (par exemple, Google Wallet). Ces transactions sont souvent soumises à des exigences strictes en temps réel de la part du terminal du marchand. Si l'application de paiement a dû démarrer à froid, la transaction peut expirer et échouer.

Compromis et seuil de performance

Bien que les liaisons soient nécessaires pour les performances et l'exactitude, elles ont un coût pour l'intégrité de la mémoire du système.

  • Flexibilité réduite : chaque processus lié est un processus que le LMK ne peut pas arrêter facilement. Cela réduit la "marge" de processus mis en cache que le système peut utiliser pour libérer de la mémoire en cas de besoin.
  • Aggravation de la dégradation des performances : si trop de processus sont liés, le système peut se retrouver avec presque aucun processus d'arrière-plan pouvant être arrêté. Lorsque la pression sur la mémoire augmente, le système "tombe de la falaise des performances" beaucoup plus rapidement, car il est obligé de supprimer des processus plus importants ou de vider le cache de page.

Antimotifs courants de liaison de service

Étant donné que les liaisons de service élèvent directement oom_score_adj, de subtiles erreurs de cycle de vie dans la façon dont vous acquérez ou structurez les liaisons peuvent épingler de grandes quantités de mémoire dans des états privilégiés (BTOP, BFGS ou PERC) pendant des heures. Faites attention à ces anti-modèles courants lorsque vous concevez ou auditez des services liés.

unbindService() oublié dans les clients de longue durée

La liaison à un service à partir d'un singleton Application, d'un gestionnaire d'arrière-plan ou d'un Activity qui appelle bindService() dans onStart() sans unbindService() correspondant dans onStop() entraîne une fuite de ServiceConnection.

Tant que cette liaison reste active, le processus cible hérite de la priorité élevée du client. Si le client est un composant système persistant ou une application au premier plan, le processus de service lié reste épinglé dans BFGS ou BTOP indéfiniment (avec une utilisation de la mémoire proche de 100% dans procstats), ce qui empêche le LMK de récupérer sa mémoire même lorsque le service est complètement inactif.

Solution : Limitez strictement les liaisons de portée au cycle de vie du composant qui en a besoin, ou implémentez un délai d'inactivité qui appelle unbindService() après une période d'inactivité. Évitez de dissocier et d'associer à nouveau à chaque appel RPC individuel, ce qui provoque un thrashing de processus et une surcharge de configuration Binder répétée. Au lieu de cela, regroupez les rafales de travail derrière un court minuteur d'inactivité (par exemple, de 5 à 30 secondes).

Colocalisation d'un service lié avec une UI gourmande en mémoire

Par défaut, tous les composants d'un APK s'exécutent dans le même processus. Si votre application expose un service lié léger (tel qu'un NotificationListenerService, un fournisseur de widget ou un service de plug-in auquel le système ou le lanceur d'applications se lie) dans le même processus que votre Activity principal, l'ensemble du processus hérite de l'état élevé du service (BFGS ou PERC, généralement oom_score_adj de 200 ou moins).

Lorsque l'utilisateur ouvre l'UI de votre application, le processus alloue de grandes hiérarchies de vues, des bitmaps décodés et des tampons graphiques. Lorsque l'utilisateur quitte l'application, le processus ne passe pas à CACHED (oom_score_adj de 900 ou plus), car la liaison de service active maintient le processus à un niveau élevé. Cela entraîne deux problèmes cumulatifs :

  • Aucune compaction de la mémoire en arrière-plan : le CachedAppOptimizer du système ne compacte les processus qu'après leur passage à l'état CACHED.
  • Aucune réduction de mémoire LRU mise en cache ni récupération LMK : le système ne fournit pas de rappels de réduction en arrière-plan liés à la liste LRU mise en cache (TRIM_MEMORY_BACKGROUND et versions ultérieures) lorsqu'une liaison de service maintient le processus dans un état élevé. Si votre application ne libère pas explicitement les ressources d'UI sur TRIM_MEMORY_UI_HIDDEN ou Activity.onStop(), les allocations d'UI maximales restent épinglées dans la RAM à un score OOM privilégié où le LMK ne peut pas les récupérer facilement.

Solution : Éjectez explicitement les caches d'UI, les bitmaps décodés et les références de vue lorsque votre UI cesse d'être visible, à l'aide de Activity.onStop(), ComponentCallbacks2.onTrimMemory(TRIM_MEMORY_UI_HIDDEN), Application.ActivityLifecycleCallbacks ou ProcessLifecycleOwner. Vous pouvez également déplacer le service toujours lié dans un processus léger distinct à l'aide de l'attribut de fichier manifeste android:process. Le système peut ainsi déplacer votre processus d'UI principal dans l'état CACHED pour compacter ou récupérer sa mémoire de manière indépendante.

Omission des indicateurs de renonciation à la priorité

L'appel de bindService() avec BIND_AUTO_CREATE seul transfère la priorité de planification et de mémoire complète de l'appelant au service cible. Lorsqu'une application de premier plan se lie à un service d'analyse, de journalisation ou de récupération préalable en arrière-plan en utilisant uniquement BIND_AUTO_CREATE, elle promeut involontairement ce worker en arrière-plan à BTOP.

Solution : lorsque vous liez des services auxiliaires ou de type "au mieux" qui n'ont pas besoin du même niveau de protection que l'UI au premier plan, combinez BIND_AUTO_CREATE avec BIND_WAIVE_PRIORITY, BIND_NOT_FOREGROUND ou BIND_NOT_PERCEPTIBLE afin que le système puisse toujours gérer le processus cible dans la liste LRU mise en cache.


← Localité | ↑ Haut | À l'échelle du système →