Dienstbindungen und Prozessstatus

App-Prozesse unter Android sind nicht isoliert. Anwendungen sind oft auf Dienste angewiesen, die von anderen Anwendungen oder dem System selbst bereitgestellt werden. Wenn ein Prozess über eine Dienstbindung mit einem anderen verbunden wird, entsteht eine Abhängigkeit, die sich erheblich darauf auswirkt, wie das Android-Framework den Arbeitsspeicher verwaltet.

Prozessstatus und OOM-Werte

Das Android-Framework verwendet Prozessstatus, um die Wichtigkeit jedes laufenden Prozesses zu erfassen. Diese Status werden dann von OomAdjuster verwendet, um einen OOM-Faktor (oom_score_adj) zwischen -1000 und 1000 zuzuweisen.

Ein niedrigerer oom_score_adj-Wert bedeutet, dass der Prozess wichtiger ist und weniger wahrscheinlich vom Low Memory Killer (LMK) beendet wird.

Häufige Prozessstatus

In der folgenden Tabelle sind einige der häufigsten Prozessstatus und ihre typischen oom_score_adj-Werte aufgeführt. Eine vollständige und aktuelle Liste finden Sie im Android-Quellcode unter android.app.ActivityManager und com.android.server.am.psc.Constants.

Prozessstatus (Abk.) Beschreibung Typisch: oom_score_adj
PER (Persistent) Systemprozesse, die immer ausgeführt werden müssen (z.B. Telefonie). -800
TOP Der Prozess, mit dem der Nutzer gerade interagiert. 0
VIS (sichtbar) Der Prozess hat eine sichtbare Aktivität (z.B. hinter einem durchscheinenden Dialogfeld). 100
PERC (Wahrnehmbar) Hintergrundprozess, der dem Nutzer bekannt ist (z.B. Musikwiedergabe). 200
FGS Prozess, der einen Dienst im Vordergrund hostet. 0 bis 200 (variiert)
BTOP (Bound Top) Prozess, der an eine TOP-Anwendung gebunden ist. 100
BFGS Gebundener Dienst im Vordergrund (in der Regel systemgebunden). 0
PREV (Zurück) Der letzte Prozess, in dem sich der Nutzer vor dem aktuellen Prozess befand. 700
CACHED Hintergrund-Apps, die sicher beendet werden können. 900 bis 999

Auswirkungen von Dienstbindungen

Wenn ein Clientprozess (z.B. eine App im Status TOP) an einen Dienst in einem Serverprozess gebunden wird, erbt der Serverprozess häufig eine höhere Priorität. So bleibt der Dienst so lange verfügbar, wie der Client ihn benötigt.

Diagramm, das zeigt, wie Prozess A (TOP) bindService() über system_server aufruft und Prozess B zu BTOP hochgestuft wird

Vererbung mit BIND-Flags steuern

Die Übernahme ist das Standardverhalten bei Verwendung von Context.BIND_AUTO_CREATE. Entwickler können jedoch mit verschiedenen Flags in bindService() steuern, wie sich die Bindung auf die Priorität des Zielprozesses auswirkt.

Wichtige BIND-Flags für den OOM-Score

Die folgenden Flags sind am wichtigsten, wenn Sie den systemweiten Speicherdruck verwalten:

  • BIND_AUTO_CREATE: Das häufigste Flag. Dadurch wird sichergestellt, dass der Dienstprozess gestartet und so lange aktiv gehalten wird, wie die Bindung besteht. Standardmäßig wird auch die Priorität des Serverprozesses an die des Clients angepasst.
  • BIND_NOT_FOREGROUND: Verhindert, dass der Prozess des Zieldienstes auf die Planungspriorität (CPU-Priorität) im Vordergrund angehoben wird. Es ermöglicht jedoch weiterhin, dass die Speicherpriorität (oom_score_adj) erhöht wird. Das ist nützlich für Hintergrundaufgaben, die nicht mit der Benutzeroberfläche um CPU-Zyklen konkurrieren, aber trotzdem vor dem Beenden geschützt werden sollen.
  • BIND_WAIVE_PRIORITY: Ein sehr starkes Flag, das das System anweist, die Priorität des Zielprozesses für die Planung oder die Speicherverwaltung nicht zu beeinflussen. Der Dienstprozess wird wie ein regulärer Hintergrundprozess in der LRU-Liste verwaltet. Er kann also auch bei einer Bindung durch OOM beendet werden.
  • BIND_ABOVE_CLIENT: Gibt an, dass der Dienst wichtiger ist als die Client-App selbst. Wenn das System Arbeitsspeicher freigeben muss, wird es die Client-App lieber beenden als den gebundenen Dienst. Das ist „stärker“ als BIND_AUTO_CREATE, da es eine zusätzliche Schutzebene für den Dienst auf Kosten des Clients bietet.
  • BIND_NOT_PERCEPTIBLE: Die Wichtigkeit des Zieldienstes wird auf unter PERCEPTIBLE gesenkt, sodass das System seinen Arbeitsspeicher freigeben kann, um Platz für wichtigere, für den Nutzer wahrnehmbare Prozesse zu schaffen.

Praxis: Bindungseffekte beobachten

Wir verwenden die Anwendung MemoryLab, um zu veranschaulichen, wie sich eine Bindung aus einer TOP-App auf den Status eines separaten Prozesses auswirkt.

1. MemoryLab starten

Mit dem folgenden Befehl wird die App gestartet. Sorgen Sie dafür, dass die App im Vordergrund bleibt (drücken Sie noch nicht den Home-Button oder wechseln Sie noch nicht die App).

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

2. Prozesse identifizieren

Prüfen Sie die Prozessstatus vor der Bindung. MemoryLab führt die Haupt-Benutzeroberfläche in einem Prozess aus und hat einen RemoteService, der in einem :remote-Prozess ausgeführt wird.

adb shell dumpsys activity processes com.android.memorylab

Beispiel für ein Ausgabesnippet:

  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

Der Hauptprozess com.android.memorylab wird im Status TOP angezeigt. Der :remote-Prozess wurde noch nicht gestartet.

3. Triggerbindung

Senden Sie einen Broadcast an die App, um die Dienstbindung auszulösen:

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

4. Erhöhten Status beobachten

Prüfen Sie die Prozessstatus noch einmal:

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

Beispiel für ein Ausgabesnippet:

  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

Der :remote-Prozess wird jetzt ausgeführt und befindet sich im Status BTOP (Bound TOP) mit einem oom_score_adj von 100. Das ist deutlich besser geschützt als ein typischer Hintergrunddienst (der sich auf 500 oder höher befinden würde). Die Notation <=Proc{...} gibt an, welcher Prozess für diese Prioritätserhöhung verantwortlich ist.

5. In den Hintergrund senden

Drücke die HOME-Taste auf dem Gerät. Prüfen Sie die Status noch einmal:

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

Beispiel für ein Ausgabesnippet:

    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

Beide Prozesse haben jetzt einen niedrigeren Prioritätsstatus (PREV / oom_score_adj 700), da der Clientprozess nicht mehr TOP ist. Hinweis: LAST im State-Dump bezieht sich auf den internen Status LAST_ACTIVITY, der in Zusammenfassungen auf hoher Ebene PREV entspricht.

Analyse mit procstats

Das Tool procstats bietet eine Übersicht über diese Status.

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

Beispiel für ein Ausgabesnippet:

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

Hier gibt Bnd Top den Prozentsatz der Zeit an, in der der Remote-Prozess durch eine Anwendung im Status TOP gebunden war.

Bindungen mit Perfetto erfassen und analysieren

dumpsys bietet zwar eine Momentaufnahme, mit Perfetto können Sie jedoch genau sehen, wann eine Bindung erfolgt und wie sich der OOM-Score in Echtzeit ändert.

1. Trace aufzeichnen

Verwenden Sie eine Konfiguration, die linux.process_stats und die atrace-Kategorie am enthält:

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. Übergänge des OOM-Scores für Abfragen

Mit PerfettoSQL können Sie sehen, wie sich der OOM-Score des Remote-Prozesses im Vergleich zum UI-Prozess geändert hat:

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. Bindungsereignisse identifizieren

Mit dieser Abfrage können Sie genau sehen, wann eine Bindungsabhängigkeit eingerichtet wurde und welcher Prozess sie initiiert hat:

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

System-zu-App-Bindungen

Das Android-System selbst bindet häufig an Dienste in Drittanbieter-Apps, um Hauptfunktionen bereitzustellen. Das Ziel dieser Bindungen ist oft die Reduzierung der Latenz. Wenn ein Prozess aktiv und im Arbeitsspeicher gehalten wird, vermeidet das System den kostspieligen Overhead eines „Kaltstarts“ (Laden der APK, Initialisieren der Laufzeit und Erstellen des Application-Objekts), wenn eine wichtige Nutzerinteraktion erfolgt. Es gibt weitere Bindungen, um häufige Kaltstarts für Apps zu verhindern, die Streams von Hintergrundereignissen verarbeiten müssen.

Hier sind einige Beispiele aus der Praxis, die Sie auf einem typischen Gerät beobachten können:

VoiceInteractor

Nutzer erwarten, dass ein digitaler Assistent in ihr Smartphone-Betriebssystem eingebettet ist, dass sie ihn sofort mit einem gesprochenen Hotword oder einer schnellen Eingabe aufrufen können und dass die Interaktion reibungslos und nahtlos erfolgt.

Wenn ein Assistenten-Trigger ausgelöst wird (z. B. das Hotword „Ok Google“ auf Google Pixel Smartphones), muss der digitale Assistent sofort reagieren. Damit dies gewährleistet ist, wird in system_server eine dauerhafte Bindung an den vom Nutzer ausgewählten Sprachinteraktionsdienst aufrechterhalten.

Diagramm, das die Bindung von „system_server“ an den Interactor-Prozess der Google-App zeigt

Wenn Sie die Prozessstatus prüfen (z.B. mit dumpsys activity processes), sehen Sie möglicherweise einen Prozess wie com.google.android.googlequicksearchbox:interactor im Status BFGS (gebundener Vordergrunddienst), der durch eine Bindung von system_server (UID 1000) aktiv gehalten wird.

NotificationListenerService

Bei einigen System-zu-App-Bindungen geht es nicht um Latenz, sondern darum, häufige Kaltstarts zu vermeiden. NotificationListenerService, ein Dienst, der Anrufe vom System empfängt, wenn neue Benachrichtigungen gepostet oder entfernt werden, ist ein gutes Beispiel. Ein typischer Smartphone-Nutzer erhält im Laufe des Tages Hunderte von Benachrichtigungen. Wenn das System die Bindung an einen Benachrichtigungs-Listener aufhebt, wechselt der Prozess dieser App wahrscheinlich in den Cache-Status und wird möglicherweise vom LMK beendet.

Wenn die nächste Benachrichtigung eingeht – möglicherweise Sekunden später –, muss das System den Prozess der App noch einmal von vorn starten, um das Ereignis zu übermitteln. Dieser ständige Zyklus aus Beenden und Kaltstart würde viel mehr CPU und Akku beanspruchen als das einfache Beibehalten des Prozesses im Hintergrund.

Der „-1-Bildschirm“ des Launchers (Nachrichten-Feed)

Moderne Launcher-Apps kombinieren in der Regel die wichtigsten Navigationsfunktionen (Startbildschirmsymbole und ‑widgets) mit einem Nachrichten-Feed, der auf einem der Launcher-Bildschirme verfügbar ist und nahtlos in die Launcher-Benutzeroberfläche integriert wird. Der Nachrichten-Feed kann von einer anderen App bereitgestellt werden. Auf Google Pixel ist der Launcher beispielsweise in einen Feed eingebunden, der von der Google App bereitgestellt wird.

Wenn Sie auf dem Startbildschirm nach links wischen, um den Nachrichten-Feed aufzurufen, muss der Übergang flüssig sein. Der Launcher erreicht dies, indem er an eine Dienstschnittstelle in der App gebunden wird, die den Nachrichten-Feed bereitstellt, und diese Bindung so lange aufrechterhält, wie der Launcher aktiv ist. So bleiben die Feedinhalte gerendert und im Speicher verfügbar, auch wenn du sie dir gerade nicht ansiehst.

Weitere häufige Beispiele

  • Launcher (HOME_APP_ADJ): Die Launcher-App (Home) hat einen eigenen speziellen Slot in der Prioritätsliste. Sie ist zwar nicht immer an einen Dienst gebunden, erhält aber die HOME_APP_ADJ (in der Regel 600). Das System bevorzugt es, den Launcher aktiv zu halten, da der Nutzer häufig dorthin zurückkehrt. Tatsächlich würde das System die zuvor verwendete App (PREV_APP_ADJ = 700) lieber beenden als den Launcher, da das Beenden des Launchers zu einer trägen User Experience beim Beenden einer App führen würde, da der Nutzer warten müsste, bis der Launcher neu gestartet wird.
  • Eingabemethoden-Editor (Input Method Editor, IME): Während Sie tippen, wird das System an die von Ihnen ausgewählte Tastatur-App gebunden, z.B. Gboard. Dadurch bleibt der Tastaturprozess auch dann in einem erhöhten Status, wenn die Tastatur vorübergehend ausgeblendet wird. So kann die Tastatur sofort wieder angezeigt werden, wenn Sie auf ein anderes Textfeld tippen.
  • NFC-Zahlungen: Wenn Sie mit Ihrem Smartphone bezahlen, wird das System an den NFC-Zahlungsdienst gebunden (z.B. Google Wallet). Diese Transaktionen unterliegen oft strengen Echtzeitanforderungen des Händlerterminals. Wenn die Zahlungs-App einen Kaltstart durchführen musste, kann es bei der Transaktion zu einer Zeitüberschreitung kommen und sie kann fehlschlagen.

Kompromisse und der Leistungsabfall

Bindungen sind zwar für die Leistung und Richtigkeit erforderlich, beeinträchtigen jedoch den Arbeitsspeicher des Systems.

  • Eingeschränkte Flexibilität: Jeder gebundene Prozess ist ein Prozess, der vom LMK nicht einfach beendet werden kann. Dadurch wird der „Puffer“ an zwischengespeicherten Prozessen reduziert, den das System verwenden kann, um bei Bedarf Arbeitsspeicher freizugeben.
  • Leistungseinbruch verschärfen: Wenn zu viele Prozesse gebunden sind, hat das System möglicherweise fast keine beendbaren Hintergrundprozesse mehr. Wenn der Speicherdruck steigt, fällt das System viel schneller „von der Leistungsklippe“, da es gezwungen ist, wichtigere Prozesse zu beenden oder den Seitencache zu leeren.

Häufige Anti-Patterns für die Dienstbindung

Da Dienstbindungen oom_score_adj direkt erhöhen, können subtile Fehler im Lebenszyklus beim Abrufen oder Strukturieren von Bindungen dazu führen, dass große Mengen an Arbeitsspeicher stundenlang in privilegierten Status (BTOP, BFGS oder PERC) verbleiben. Achten Sie beim Entwerfen oder Überprüfen von gebundenen Diensten auf diese häufigen Antimuster.

Vergessene unbindService() in Clients mit langer Lebensdauer

Wenn Sie eine Bindung an einen Dienst über ein Application-Singleton, einen Hintergrundmanager oder ein Activity vornehmen, das bindService() in onStart() ohne übereinstimmendes unbindService() in onStop() aufruft, wird das ServiceConnection geleakt.

Solange diese Bindung aktiv ist, erbt der Zielprozess die erhöhte Priorität des Clients. Wenn der Client eine dauerhafte Systemkomponente oder eine App im Vordergrund ist, bleibt der Prozess des gebundenen Dienstes unbegrenzt in BFGS oder BTOP fixiert (mit einem Wert von fast 100% in procstats). Dadurch kann der LMK seinen Arbeitsspeicher nicht zurückfordern, auch wenn der Dienst vollständig inaktiv ist.

Abhilfe: Beschränken Sie die Bindungen von Bereichen streng auf den Lebenszyklus der Komponente, die sie benötigt, oder implementieren Sie ein Zeitlimit für Inaktivität, das unbindService() nach einem Zeitraum der Inaktivität aufruft. Vermeiden Sie das Aufheben und erneute Binden bei jedem einzelnen RPC-Aufruf, da dies zu Prozess-Thrashing und wiederholtem Binder-Einrichtungsaufwand führt. Fassen Sie stattdessen Arbeitsspitzen hinter einem kurzen Leerlauf-Timer (z. B. 5 bis 30 Sekunden) zusammen.

Gebundenen Dienst mit einer arbeitsspeicherintensiven Benutzeroberfläche zusammenlegen

Standardmäßig werden alle Komponenten in einem APK im selben Prozess ausgeführt. Wenn Ihre App einen einfachen gebundenen Dienst (z. B. einen NotificationListenerService, einen Widget-Anbieter oder einen Plug-in-Dienst, an den das System oder der Launcher gebunden ist) im selben Prozess wie Ihre Haupt-Activity bereitstellt, erbt der gesamte Prozess den erhöhten Status des Dienstes (BFGS oder PERC, in der Regel oom_score_adj von 200 oder niedriger).

Wenn der Nutzer die Benutzeroberfläche Ihrer App öffnet, werden im Prozess große Ansichtshierarchien, decodierte Bitmaps und Grafikpuffer zugewiesen. Wenn der Nutzer die Seite verlässt, sinkt die Priorität des Prozesses nicht auf CACHED (oom_score_adj von 900 oder höher), da die aktive Dienstbindung die Priorität des Prozesses hoch hält. Das führt zu zwei Problemen:

  • Keine Hintergrundspeicherkompaktierung: Das CachedAppOptimizer des Systems komprimiert Prozesse erst, nachdem sie in den Status CACHED gewechselt sind.
  • Kein LRU-Speicher-Trim für den Cache oder LMK-Rückforderung: Das System stellt keine Hintergrund-Trim-Callbacks bereit, die an die LRU-Liste für den Cache (TRIM_MEMORY_BACKGROUND und höher) gebunden sind, während eine Dienstbindung den Prozess in einem erhöhten Status hält. Wenn Ihre App UI-Ressourcen nicht explizit auf TRIM_MEMORY_UI_HIDDEN oder Activity.onStop() freigibt, bleiben die maximalen UI-Zuweisungen im RAM mit einem privilegierten OOM-Score fixiert, sodass der LMK sie nicht einfach zurückfordern kann.

Abhilfe: Entfernen Sie UI-Caches, decodierte Bitmaps und Ansichtsreferenzen explizit, wenn Ihre Benutzeroberfläche nicht mehr sichtbar ist. Verwenden Sie dazu Activity.onStop(), ComponentCallbacks2.onTrimMemory(TRIM_MEMORY_UI_HIDDEN), Application.ActivityLifecycleCallbacks oder ProcessLifecycleOwner. Alternativ können Sie den immer gebundenen Dienst in einen separaten, einfachen Prozess mit dem Manifestattribut android:process verschieben, damit das System den Haupt-UI-Prozess unabhängig in den Status CACHED verschieben kann, um seinen Speicher zu komprimieren oder freizugeben.

Flags für den Verzicht auf Priorität werden ausgelassen

Wenn Sie bindService() mit BIND_AUTO_CREATE aufrufen, werden die gesamte Planungs- und Arbeitsspeicherpriorität des Anrufers an den Zieldienst übertragen. Wenn eine Vordergrund-App nur mit BIND_AUTO_CREATE an einen Hintergrunddienst für Analysen, Logging oder Prefetching gebunden wird, wird dieser Hintergrund-Worker unbeabsichtigt zu BTOP hochgestuft.

Abhilfe: Wenn Sie eine Bindung an Hilfs- oder Best-Effort-Dienste vornehmen, die nicht dasselbe Schutzniveau wie die Benutzeroberfläche im Vordergrund benötigen, kombinieren Sie BIND_AUTO_CREATE mit BIND_WAIVE_PRIORITY, BIND_NOT_FOREGROUND oder BIND_NOT_PERCEPTIBLE, damit das System den Zielprozess weiterhin in der LRU-Liste im Cache verwalten kann.


← Lokalität | ↑ Nach oben | Systemweit →