Hintergrundprozesse mit einem schlanken Prozess am Laufen halten

Viele Apps führen legitime Aufgaben aus, wenn ihre Benutzeroberfläche nicht mehr sichtbar ist, z. B. das Herunterladen großer Asset-Pakete, das Synchronisieren lokaler Datenbanken, das Streamen von Audio, das Aufzeichnen von Trainingseinheiten oder das Aufrechterhalten einer Sitzung mit einem verbundenen Gerät. Wenn diese Hintergrundarbeit im selben Betriebssystemprozess wie die Benutzeroberfläche und die Grafik-Engine im Vordergrund ausgeführt wird, bleibt der gesamte Prozess zusammen mit dem gesamten von der Benutzeroberfläche zugewiesenen Arbeitsspeicher auf einer erhöhten Wichtigkeitsstufe (oom_score_adj zwischen 0 und 250).

Auf dieser Seite wird erläutert, warum dies sowohl Ihrer eigenen App als auch dem Rest des Geräts schadet, wie Sie Hintergrundaufgaben mit android:process in einen dedizierten, einfachen Prozess verschieben und wie Sie die Speichereinsparungen mit adb shell und Perfetto überprüfen können.

Warum bei der Arbeit mit einem einzelnen Prozess der UI-Speicher erhalten bleibt

Android verwaltet den Arbeitsspeicher und die Beendigung von Prozessen auf der Ebene eines Betriebssystemprozesses (pid), nicht auf der Ebene einzelner Komponenten (Activity, Service oder BroadcastReceiver). Der Activity Manager (OomAdjuster) weist jedem Prozess einen einzelnen oom_score_adj-Wert zu, der die wichtigste aktive Komponente oder eingehende Bindung in diesem Prozess widerspiegelt:

  • Wenn der Nutzer mit Ihrem Activity interagiert, wird der Prozess im Status TOP (oom_score_adj = 0) ausgeführt.
  • Wenn der Nutzer die App verlässt und die App keine aktiven Hintergrundkomponenten hat, wird der Prozess in den Bucket PREV (oom_score_adj = 700) oder CACHED (oom_score_adj >= 900) verschoben. Im im Cache gespeicherten Bucket friert die Plattform den Prozess mit dem cgroup v2-Freezer ein, komprimiert den Arbeitsspeicher mit CachedAppOptimizer und lässt lmkd den Prozess bei Speicherdruck zuerst beenden.
  • Wenn im selben Prozess jedoch auch ein Dienst im Vordergrund ausgeführt wird (z. B. die Typen dataSync, mediaPlayback, connectedDevice oder location bei oom_score_adj = 200) oder ein Dienst gehostet wird, an den das System oder ein anderer wichtiger Client gebunden ist, bleibt der gesamte Prozess auch nach dem Beenden des Activity auf dieser erhöhten Priorität.
Vergleich der Co-Location mit einem einzelnen Prozess, bei der der UI-Speicher bei oom_score_adj 200 angepinnt bleibt, mit der Isolation mit geteilten Prozessen, bei der nur der Worker bei oom_score_adj 200 bleibt, während der UI-Prozess in den Cache verschoben wird.
Abbildung 1. Co-Location in einem einzelnen Prozess im Vergleich zur Isolation in einem separaten Prozess während der Hintergrundarbeit.

Wenn ein rechenintensiver UI-Prozess bei oom_score_adj = 200 bleibt, bleiben mehrere große Kategorien von Arbeitsspeicher resident:

  • Gestoppte Activity und Ansichtshierarchie: Wenn die Activity nicht explizit beendet wird, bleiben ihre Window, der Ansichtsbaum, die Layoutstrukturen und der Ansichtsstatus im Java-Heap erreichbar, während der Nutzer eine andere App verwendet.
  • Native Grafikkontexte und GPU-Zuweisungen: Der Standard-HardwareRenderer behält seinen GPU-Kontext und seine Caches bei, während ein gestoppter Activity in einem Prozess lebt, der nicht im Cache gespeichert wird. Apps und Game-Engines, die ihre eigenen Vulkan-Kontexte (VkDevice, VkSwapchainKHR, Deskriptorpools, Pipeline-Caches) oder OpenGL ES-Kontexte (EGLContext, Texturen, Vertex-Zwischenspeicher) erstellen, behalten auch ihre Treiberstrukturen, Shader-Programme und Grafikzwischenspeicher bei, es sei denn, die Engine löscht das Grafikgerät beim Beenden explizit.
  • Heap-Fragmentierung und Laufzeit-Overhead: Auch wenn eine App Caches in onTrimMemory freigibt, bleiben teilweise gefüllte native Allocator-Seiten, Dirty-Zygote-Copy-on-Write-Seiten, geladene gemeinsam genutzte Bibliotheken (.so) und Laufzeitmetadaten im privaten Speicher des Prozesses. Da der Prozess über dem Grenzwert für im Cache gespeicherte Prozesse (oom_score_adj < 900) liegt, wird er von der Plattform nicht eingefroren und es wird keine Komprimierung für im Cache gespeicherte Apps durchgeführt. Weitere Informationen zu diesem Effekt bei gebundenen Diensten finden Sie unter Gebundenen Dienst mit einer speicherintensiven UI zusammenlegen.

Wie ein schlanker Hintergrundprozess Ihre App schützt

Wenn Sie im Hintergrund Arbeitsspeicher in der Größe des Vordergrunds belegen, gefährden Sie Ihre eigene Hintergrundarbeit:

  • Die Speicherlimits für Hintergrundprozesse sind strenger als die für Vordergrundprozesse. In Android 17 und höher wendet der Memory Limiter ein kleineres nicht sichtbares Speicherlimit auf Ihre App an, wenn keiner ihrer Prozesse eine sichtbare Benutzeroberfläche anzeigt, auch wenn ein Dienst im Vordergrund ausgeführt wird. Das Limit gilt für alle Prozesse Ihrer App zusammen. Wenn der Prozess, der den Dienst im Vordergrund ausführt, weiterhin die Benutzeroberfläche und den Grafikspeicher belegt, wird dieser Speicher auf das kleinere Limit angerechnet. Wenn deine App das Limit überschreitet, wird sie vom Kernel gedrosselt, während ihre Seiten freigegeben werden. Dadurch werden der Download, die Synchronisierung oder die Wiedergabe verlangsamt. Wenn Ihre Anwendung weiterhin Speicher zuweist, nachdem der Swap-Speicher erschöpft ist, wird sie vom System beendet. Unter Arbeitsspeicherlimits für Apps können Sie prüfen, ob Ihre App betroffen ist.
  • Bei Speichermangel werden größere Prozesse zuerst beendet. Wenn lmkd Speicher freigeben muss und so konfiguriert ist, dass der rechenintensivste Prozess in einem Wichtigkeits-Bucket (ro.lmk.kill_heaviest_task) ausgewählt wird, wird ein Prozess für Dienste im Vordergrund, der auch Hunderte von MB UI-Speicher belegt, vor kleineren Prozessen mit derselben Wichtigkeit beendet. Ihre Hintergrundarbeit wird damit ebenfalls beendet. Siehe Interaktion zwischen kswapd und lmkd.
  • Der im Cache gespeicherte UI-Status kann eingefroren, komprimiert und zurück in den Flash-Speicher geschrieben werden. Wenn du zulässt, dass der Status der Benutzeroberfläche im Vordergrund in einen im Cache gespeicherten Prozess verschoben wird, kann das Betriebssystem den Prozess einfrieren, den Arbeitsspeicher in zRAM komprimieren und die kalten Seiten zurück in den Flash-Speicher schreiben. Der Speicherbedarf im Arbeitsspeicher wird reduziert, während der UI-Status intakt bleibt. So haben sowohl die Hintergrundarbeit als auch die im Cache gespeicherte UI die besten Chancen, bis zur Rückkehr des Nutzers zu überleben.
  • Speicherbudgets pro Prozess werden praktisch. Ein Prozess, der nur Hintergrundaufgaben ausführt, hat einen kleinen, vorhersehbaren Arbeitssatz. Sie können ihm also ein knappes Arbeitsspeicherbudget zuweisen, ohne die Benutzeroberfläche einzuschränken.

Das Ausführen eines zweiten Prozesses kann den Eindruck erwecken, dass sich der Gesamtspeicherbedarf Ihrer Anwendung erhöht, da eine zweite Laufzeit geringe Grundkosten verursacht. Dieser Vergleich gilt nur, wenn ein einzelner Prozess seinen gesamten UI- und Grafikstatus vollständig freigeben könnte, wenn er in den Hintergrund wechselt. Das ist bei Apps und UI-Toolkits in der Praxis nicht zuverlässig möglich. Wenn das Betriebssystem einen separaten im Cache gespeicherten UI-Prozess einfrieren, komprimieren und zurückschreiben kann, wird im Normalfall viel weniger physischer Arbeitsspeicher verwendet. Ihre App bleibt innerhalb des Limits für nicht sichtbare Apps und die Anzahl der Beendigungen aufgrund von Speichermangel wird für Ihre App und auf dem gesamten Gerät reduziert.

Hintergrundarbeit in einem dedizierten Prozess isolieren

Wenn du die UI- und Grafikresidenz während der Hintergrundarbeit eliminieren möchtest, führe langlebige Hintergrundkomponenten in einem dedizierten einfachen Prozess aus, während du die UI und die Grafik-Engine im Hauptprozess behältst:

Prozess Manifestdeklaration Gehostete Komponenten Status, wenn der Nutzer die Benutzeroberfläche während der Hintergrundarbeit verlässt
Haupt-UI-Prozess Standard (com.example.app) Activity, UI-Ansichten, Bild-Caches, Vulkan- oder OpenGL ES-Renderer, Audio- und Szenen-Engine Fällt auf PREV und dann auf CACHED (oom_score_adj >= 900). Eingefroren, komprimiert und zurück in den Flash-Speicher geschrieben.
Hintergrund-Worker-Prozess android:process=":bg" Vordergrund-Service, Hintergrund-Asset-Downloader, Synchronisierungs-Worker oder Manager für verbundene Geräte Wird mit FGS (oom_score_adj = 200) nur mit dem eigenen Heap und Code des Workers und ohne UI-Ansichten oder GPU-Kontexte ausgeführt.

Prozess- und Speicherbudgets deklarieren

Weisen Sie den Hintergrund Service einem privaten Unterprozess zu, indem Sie android:process=":bg" in AndroidManifest.xml festlegen. Der führende Doppelpunkt (:) erstellt einen privaten Prozess, der auf die UID Ihrer App beschränkt ist.

In Android 17 QPR2 (API-Level 37.2) und höher können Sie die Prozessaufteilung mit App-Arbeitsspeicherbudgets pro Prozess im <processes>-Block kombinieren, damit das Betriebssystem eine strenge Obergrenze für den Arbeitssatz des Hintergrund-Workers erzwingt, ohne den UI-Prozess im Vordergrund einzuschränken:

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

Die Speichernutzung in :bg wird sowohl auf das eigene Budget als auch auf das App-weite Budget angerechnet. Auf Geräten mit Versionen vor Android 17 QPR2 wird <memory-budget> vom System ignoriert, während android:process=":bg" auf allen API-Levels funktioniert.

Gate-Initialisierung in Application.onCreate

Android instanziiert die Klasse Application und ruft Application.onCreate in jedem Prozess auf, der für das Paket erstellt wird. Ein häufiger Fehler bei der Einführung von android:process=":bg" besteht darin, umfangreiche UI-Toolkits, Pools zum Laden von Bildern, Analyse-SDKs oder die Initialisierung nativer Engines (System.loadLibrary) in einer ungeschützten Application.onCreate-Methode zu belassen. Dadurch wird der :bg-Prozess mit demselben Baseline-Speicherbedarf aufgebläht, den Sie vermeiden möchten.

Prüfe Application.getProcessName, das in Android 9 (API-Level 28) und höher verfügbar ist, in onCreate und initialisiere nur die Abhängigkeiten, die für den aktuellen Prozess erforderlich sind:

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");
    }
}

Bibliotheken, die über einen Contentanbieter initialisiert werden, z. B. Jetpack App Startup, werden nur im Hauptprozess initialisiert, da ein ohne android:process deklarierter Contentanbieter im Hauptprozess ausgeführt wird. Wenn Code in :bg von einer solchen Bibliothek abhängt, initialisieren Sie sie explizit in initBackgroundWorkerProcess.

Wenn Sie mit einer Spiel-Engine wie Unity entwickeln, implementieren Sie den Hintergrunddienst in einer einfachen Android-Bibliothek oder einem Plug-in, das in Java oder Kotlin geschrieben ist und keine Engine-Klassen wie UnityPlayer referenziert. Der :bg-Prozess lädt dann nie die nativen Bibliotheken der Engine. Prüfen Sie zur Bestätigung, ob adb shell cat /proc/<pid>/maps für den Prozess :bg nicht libunity.so oder libil2cpp.so enthält.

Kommunikation ohne Anpinnen des UI-Prozesses

Wenn der Nutzer MainActivity geöffnet hat, während AssetDownloadService in :bg ausgeführt wird, muss die Benutzeroberfläche oft den aktuellen Downloadfortschritt anzeigen. Die Prozesswichtigkeit fließt von einem Client zu dem Prozess, der den verwendeten Dienst oder Contentanbieter hostet. Daher ist die Richtung jeder Abhängigkeit wichtig:

  • Die UI-Bindung an den Worker ist sicher. Wenn MainActivity an AssetDownloadService gebunden wird, erhöht das System :bg auf die Wichtigkeit der sichtbaren UI. Wenn der Nutzer die App verlässt, wird der UI-Prozess im Cache gespeichert und die Bindung erhöht nichts mehr.
  • Der Worker, der vom UI-Prozess abhängt, ist nicht sicher. Wenn Code in :bg an einen Dienst gebunden wird oder einen Contentanbieter abfragt, der im Hauptprozess ausgeführt wird, erhöht das System die Priorität des Hauptprozesses auf die des Workers und der UI-Arbeitsspeicher bleibt wieder angepinnt. Lassen Sie die UI stattdessen einen Callback beim Worker registrieren, z. B. einen Messenger oder eine AIDL-Callback-Schnittstelle. Die Übergabe eines Binder-Objekts als Callback ändert nichts an der Wichtigkeit der beiden Prozesse.

Melden Sie den Callback ab, bevor Sie die Bindung aufheben, damit der Worker den UI-Prozess nicht aufruft, nachdem er vom System eingefroren wurde. Im folgenden Beispiel wird nur dann eine Bindung an den Worker hergestellt, wenn die UI sichtbar ist:

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);
    }
}

Weitere Informationen dazu, wie sich Bindungs-Flags prozessübergreifend auf oom_score_adj auswirken, findest du unter Dienstbindungen und Prozessstatus.

Gemeinsamen On-Disk-Status sicher koordinieren

Da der Hauptprozess und :bg in separaten Adressräumen mit separaten Laufzeiten und Dateideskriptoren ausgeführt werden, gilt Folgendes:

  • SQLite und Room: Verwende Write-Ahead-Logging (der Standard-Journalmodus in Room auf den meisten Geräten), damit beide Prozesse gleichzeitig in der Datenbank lesen und schreiben können. Rufen Sie enableMultiInstanceInvalidation nicht über :bg auf. Room hostet seinen Entwertungsservice im Hauptprozess. Der Worker würde sich also an den Hauptprozess binden und den UI-Arbeitsspeicher auf der erhöhten Wichtigkeit des Workers halten. Stattdessen sollte der Worker Änderungen über den Callback melden, den die UI registriert, und die UI sollte die Datenbank beim Start neu abfragen.
  • Einstellungen: SharedPreferences darf nicht prozessübergreifend freigegeben werden. Verwenden Sie DataStore in mehrprozessigem Code oder übergeben Sie den Status explizit über IPC.
  • Asset-Dateien: Schreiben Sie heruntergeladene Asset-Packs in eine Staging-Datei in :bg, rufen Sie fdatasync und dann posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED) in :bg nach dem Schreiben auf (wie unter Speicherseiten an das Betriebssystem zurückgeben gezeigt) und benennen Sie die Datei atomar um. Lassen Sie die Benutzeroberfläche das Ergebnis abrufen, wenn sie das nächste Mal gestartet und an den Worker gebunden wird, anstatt einen UI-Prozess aufzurufen, der möglicherweise eingefroren ist.

Auswirkungen in der adb-Shell beobachten

Sie können prüfen, ob die Prozessisolation Ihrer Vordergrund- und Hintergrundprozesse die erhöhte Speichernutzung isoliert, indem Sie die Prozessstatus des Activity Managers und dumpsys meminfo nach dem Aufrufen des Startbildschirms prüfen. Die Beispielausgabe in diesem Abschnitt stammt aus der MemoryLab-Beispiel-App, die in beiden Konfigurationen dieselbe native 64-MB-Zuweisung in ihrem UI-Prozess ausführt und ihren aufgeteilten Dienst im Vordergrund in einem :remote-Prozess hostet.

1. Prozessstatus und OOM-Werte vergleichen

Starte die Hintergrundarbeitslast, drücke den Home-Button, damit die Activity beendet wird, und frage den Activity Manager ab:

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

In einer Architektur mit einem einzelnen Prozess bleibt der Hauptprozess erhöht, obwohl seine Activity beendet wurde. Nachdem der Nutzer die App verlassen hat, behält Activity Manager etwa 15 Sekunden lang einen Prozess bei, der einen Dienst im Vordergrund mit oom_score_adj = 50 (fg-service-act) ausführt, und senkt ihn dann auf 200 (fg-service). Die folgende Ausgabe wurde 20 Sekunden nach dem Drücken der Startbildschirmtaste erfasst:

    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 einer Architektur mit aufgeteilten Prozessen bleibt nur der Worker-Prozess bei oom_score_adj = 200, während der Haupt-UI-Prozess auf PREV (oom_score_adj = 700) und später auf den im Cache gespeicherten Bucket (oom_score_adj >= 900) fällt:

    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

Nach Ablauf des Zeitlimits für die vorherige App wird der Hauptprozess in den im Cache gespeicherten Bucket (cch, oom_score_adj = 900, previous-expired) verschoben und verliert alle seine Prozessfunktionen (---------), einschließlich der impliziten CPU-Zeit (I). An diesem Punkt wird er vom System eingefroren und kann jederzeit komprimiert oder zurückgefordert werden, ohne den Worker-Prozess zu unterbrechen.

2. „dumpsys meminfo“ prozessübergreifend vergleichen

Vergleichen Sie die dumpsys meminfo-Zusammenfassung des Worker-Prozesses mit erhöhten Berechtigungen mit dem UI-Prozess:

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

Beispiel für die App Summary-Ausgabe für den MemoryLab-Worker-Prozess unter 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

Beispiel für die App Summary-Ausgabe für den MemoryLab-UI-Prozess unter oom_score_adj = 700, der die native 64-MB-Zuweisung in Private Other enthält:

 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

Der Worker-Prozess benötigt weniger als 5 MB PSS, da er die meisten seiner Code- und Laufzeitseiten mit der Zygote teilt. In einer App, die mit Vulkan oder OpenGL ES rendert, enthält die Zeile Graphics des UI-Prozesses auch GPU-Arbeitsspeicher. Dieses Beispiel wurde auf einem virtuellen Cuttlefish-Gerät mit Software-Rendering erfasst. Daher ist Graphics in beiden Prozessen 0.

3. Prozessspezifische cgroup-Ressourcenerfassung prüfen

Jeder Prozess unter Ihrer UID hat ein eigenes cgroup v2-Verzeichnis in einer übergeordneten cgroup für Ihre App. Mit dem folgenden Befehl werden oom_score_adj, memory.current und die app-weite memory.high für jeden Prozess Ihrer App aufgelistet:

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
'

Beispielausgabe für MemoryLab nach Drücken der Home-Taste:

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

Für den Worker-Prozess werden weniger als 4 MB berechnet, für den UI-Prozess etwa 98 MB. Jeder Prozess hat eine eigene memory.current, aber der Memory Limiter legt sein Limit für die übergeordnete cgroup fest, die alle Prozesse Ihrer Anwendung enthält. Beide Zeilen zeigen dieselbe app.memory.high von 966.787.072 Byte, was dem nicht sichtbaren Limit von 922 MB in der folgenden Ausgabe entspricht.

Führen Sie den folgenden Befehl aus, um die sichtbaren und nicht sichtbaren Limits aufzurufen, die der Memory Limiter auf dem Gerät anwendet:

adb shell am memory-limiter status

Beispielausgabe von einem Gerät mit 4 GB RAM, bei dem das Limit für nicht sichtbare Apps die Hälfte des Limits für sichtbare Apps beträgt:

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

Effekt in Perfetto beobachten

Mit Perfetto kannst du die Zeitachse von oom_score_adj und RSS für beide Prozesse visualisieren, während der Nutzer von der Benutzeroberfläche im Vordergrund zum Startbildschirm wechselt und die Hintergrundarbeit fortgesetzt wird.

1. Trace aufzeichnen

Zeichnen Sie einen 15-sekündigen Trace auf, der Prozessspeicherzähler, GPU-Speicher und oom_score_adj-Änderungen erfasst, während Sie einen Hintergrundjob in Ihrer App starten und zum Startbildschirm wechseln:

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. Prozess-Tracks in der Perfetto-Benutzeroberfläche prüfen

Öffnen Sie den Trace in der Perfetto-Benutzeroberfläche und maximieren Sie die Prozessgruppen für com.example.app und com.example.app:bg:

  1. Pinnen Sie die Tracks oom_score_adj und mem.rss.anon für beide Prozesse an, damit sie nebeneinander oben auf der Zeitachse angezeigt werden.
  2. Suchen Sie den Zeitstempel, an dem der Nutzer die Starttaste drückt:
    • Im Worker-Prozess bleibt oom_score_adj bei 200 und mem.rss.anon bleibt flach und klein.
    • Im UI-Prozess steigt oom_score_adj von 0 auf 200, bleibt dort etwa eine Sekunde lang, während die Activity beendet wird, und steigt dann auf 700 (PREV). Nach dem Zeitlimit für die vorherige App wird sie in den Cache-Bereich verschoben, wo das System den Arbeitsspeicher komprimieren und freigeben kann, ohne den Worker zu beeinträchtigen.
Perfetto-Tracks für den UI-Prozess und den Worker-Prozess. Der oom_score_adj-Wert des UI-Prozesses steigt von 0 auf 200 und dann nach dem Startbildschirm auf 700, während der Worker-Prozess bei 200 mit etwa 19 MB anonymem RSS bleibt.
Abbildung 2. Prozesse werden aufgeteilt, nachdem der Nutzer die Startseite aufgerufen hat.

In Abbildung 2 bleibt der UI-Prozess kurz bei 200, während sein Activity stoppt und dann auf 700 ansteigt. Der Worker bleibt bei 200 mit etwa 19 MB anonymem RSS. Zum Vergleich zeigt Abbildung 3 einen 35-sekündigen Trace der Konfiguration mit Co-Location, bei der der einzelne Prozess bei 50 und dann bei 200 mit seinem gesamten UI-Arbeitsspeicher bleibt.

Perfetto-Tracks für einen einzelnen Co-Location-Prozess. „oom_score_adj“ steigt nach dem Startbildschirm von 0 auf 50 und etwa 15 Sekunden später auf 200, während der anonyme RSS hoch bleibt.
Abbildung 3. Ein Prozess am selben Ort, nachdem der Nutzer auf „Startseite“ gedrückt hat.

Öffnen Sie zum Erkunden dieser Ansichten den geteilten Trace und den Trace am selben Ort in der Perfetto-Benutzeroberfläche.

3. Erhöhten Speicherbedarf mit PerfettoSQL abfragen

Führen Sie auf dem Tab Abfrage (SQL) die folgende PerfettoSQL-Abfrage aus, um die durchschnittliche anonyme und Datei-RSS zu messen, die von jedem Prozess Ihrer App auf jeder oom_score_adj-Ebene gehalten wird:

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;

Beispielausgabe für den geteilten Trace in Abbildung 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

Beispielausgabe für den am selben Ort befindlichen Trace in Abbildung 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

In der geteilten Konfiguration bleiben nach der ersten Sekunde nur etwa 19 MB an anonymem Speicher bei oom_score_adj = 200. In der Co-Location-Konfiguration bleiben etwa 117 MB bei 50 und dann 200, solange der Dienst im Vordergrund ausgeführt wird.


← Speicherseiten an das Betriebssystem zurückgeben | ↑ Nach oben