Utrzymywanie działania w tle za pomocą uproszczonego procesu

Wiele aplikacji wykonuje prawidłowe działania, gdy ich interfejs nie jest już widoczny, np. pobieranie dużych pakietów zasobów, synchronizowanie lokalnych baz danych, strumieniowe przesyłanie dźwięku, rejestrowanie treningów czy utrzymywanie sesji połączonego urządzenia. Gdy ta praca w tle jest wykonywana w tym samym procesie systemu operacyjnego co interfejs użytkownika na pierwszym planie i silnik graficzny, cały proces pozostaje na podwyższonym poziomie ważności (oom_score_adj między 0 a 250) wraz z całą pamięcią przydzieloną przez interfejs użytkownika.

Na tej stronie wyjaśniamy, dlaczego szkodzi to Twojej aplikacji i innym funkcjom urządzenia, jak przenieść pracę w tle do dedykowanego, lekkiego procesu za pomocą android:process oraz jak sprawdzić oszczędność pamięci za pomocą adb shell i Perfetto.

Dlaczego praca w jednym procesie zachowuje pamięć interfejsu

Android zarządza pamięcią i kończeniem procesów na poziomie procesu systemu operacyjnego (pid), a nie poszczególnych komponentów (Activity, Service lub BroadcastReceiver). Menedżer aktywności (OomAdjuster) przypisuje każdemu procesowi jedną wartość oom_score_adj, która odzwierciedla najważniejszy aktywny komponent lub przychodzące powiązanie w tym procesie:

  • Gdy użytkownik wchodzi w interakcję z aplikacją Activity, proces działa w stanie TOP (oom_score_adj = 0).
  • Gdy użytkownik opuści aplikację, a nie ma ona aktywnych komponentów działających w tle, proces zostanie przeniesiony do zasobnika PREV (oom_score_adj = 700) lub CACHED (oom_score_adj >= 900). W zasobniku z pamięcią podręczną platforma zawiesza proces za pomocą zamrażarki cgroup v2, kompaktuje jego pamięć za pomocą CachedAppOptimizer i pozwala lmkd zakończyć proces najpierw pod wpływem obciążenia pamięci.
  • Jeśli jednak ten sam proces uruchamia również usługę działającą na pierwszym planie (np. dataSync, mediaPlayback, connectedDevice lub location typu, na poziomie oom_score_adj = 200) lub hostuje usługę, z którą wiąże się system lub inny ważny klient, cały proces zachowuje ten podwyższony priorytet nawet po zatrzymaniu Activity.
Porównanie kolokacji pojedynczego procesu z przypiętą pamięcią interfejsu przy oom_score_adj 200 z izolacją podzielonego procesu, w której tylko proces roboczy ma oom_score_adj 200, a proces interfejsu jest przenoszony do pamięci podręcznej.
Rysunek 1. Kolokacja w ramach jednego procesu w porównaniu z izolacją w ramach podzielonego procesu podczas pracy w tle.

Gdy proces interfejsu o dużym obciążeniu pozostaje na poziomie oom_score_adj = 200, kilka dużych kategorii pamięci pozostaje rezydentnych:

  • Zatrzymany obiekt Activity i hierarchia widoków: jeśli obiekt Activity nie zostanie wyraźnie zakończony, jego obiekt Window, drzewo widoków, struktury układu i stan widoku pozostaną dostępne na stercie Javy, gdy użytkownik będzie korzystać z innej aplikacji.
  • Natywne konteksty graficzne i alokacje GPU: standardowy element HardwareRenderer zachowuje swój kontekst GPU i pamięć podręczną, a zatrzymany element Activity działa w procesie, który nie jest przechowywany w pamięci podręcznej. Aplikacje i silniki gier, które tworzą własne konteksty Vulkan (VkDevice, VkSwapchainKHR, pule deskryptorów, pamięci podręczne potoków) lub OpenGL ES (EGLContext, tekstury, bufor wierzchołków), zachowują też swoje struktury sterowników, programy cieniujące i bufory graficzne, chyba że silnik wyraźnie wyłączy urządzenie graficzne po zatrzymaniu.
  • Fragmentacja sterty i narzut środowiska wykonawczego: nawet gdy aplikacja zwalnia pamięć podręczną w onTrimMemory, częściowo wypełnione strony natywnego alokatora, zmodyfikowane strony Zygote z mechanizmem copy-on-write, wczytane biblioteki udostępnione (.so) i metadane środowiska wykonawczego pozostają w pamięci prywatnej procesu. Ponieważ proces przekracza próg pamięci podręcznej (oom_score_adj < 900), platforma nie zamraża go ani nie przeprowadza kompaktowania aplikacji w pamięci podręcznej. Więcej informacji o tym efekcie w przypadku powiązanych usług znajdziesz w artykule Kolokacja powiązanej usługi z interfejsem wymagającym dużej ilości pamięci.

Jak proces w tle chroni Twoją aplikację

Utrzymywanie pamięci o rozmiarze odpowiednim dla pierwszego planu w stanie tła naraża na ryzyko działanie w tle:

  • Limity pamięci w tle są bardziej rygorystyczne niż limity na pierwszym planie. W Androidzie 17 i nowszych ogranicznik pamięci stosuje mniejszy limit pamięci niewidocznej, gdy żaden z procesów aplikacji nie wyświetla widocznego interfejsu, w tym podczas działania usługi na pierwszym planie. Limit obejmuje wszystkie procesy aplikacji. Jeśli proces, który uruchamia usługę na pierwszym planie, nadal przechowuje pamięć interfejsu i karty graficznej, ta pamięć jest wliczana do mniejszego limitu. Gdy aplikacja przekroczy limit, jądro ograniczy jej działanie, odzyskując jej strony, co spowolni pobieranie, synchronizację lub odtwarzanie. Jeśli aplikacja nadal przydziela pamięć po wyczerpaniu pamięci wymiany, system ją zamyka. Aby sprawdzić, czy problem dotyczy Twojej aplikacji, zapoznaj się z sekcją Limity pamięci aplikacji.
  • W przypadku braku pamięci najpierw są zamykane większe procesy. Gdy lmkd musi zwolnić pamięć i jest skonfigurowany tak, aby wybierać najbardziej obciążający proces w zasobniku ważności (ro.lmk.kill_heaviest_task), proces usługi na pierwszym planie, który również zajmuje setki megabajtów pamięci interfejsu, jest przerywany przed mniejszymi procesami o tym samym poziomie ważności, a wraz z nim zatrzymuje się praca w tle. Zobacz Interakcja kswapd i lmkd.
  • Stan interfejsu w pamięci podręcznej można zamrozić, skompresować i zapisać z powrotem w pamięci flash. Zezwolenie na przeniesienie stanu interfejsu na pierwszym planie do procesu w pamięci podręcznej umożliwia systemowi operacyjnemu zamrożenie procesu, skompresowanie jego pamięci w zRAM i zapisanie jego nieużywanych stron z powrotem w pamięci flash. Wykorzystanie pamięci rezydentnej zmniejsza się, a stan interfejsu pozostaje nienaruszony, dzięki czemu zarówno praca w tle, jak i interfejs w pamięci podręcznej mają największe szanse na przetrwanie do momentu powrotu użytkownika.
  • Budżety pamięci na proces stają się praktyczne. Proces, który wykonuje tylko zadania w tle, ma mały, przewidywalny zestaw roboczy, więc możesz mu przydzielić niewielki budżet pamięci bez ograniczania interfejsu.

Uruchomienie drugiego procesu może sprawiać wrażenie, że zwiększa się całkowite wykorzystanie pamięci przez aplikację, ponieważ drugi proces ma niewielki koszt podstawowy. To porównanie jest prawdziwe tylko wtedy, gdy pojedynczy proces może całkowicie zwolnić cały interfejs i stan grafiki, gdy przechodzi do pracy w tle, czego aplikacje i zestawy narzędzi interfejsu nie mogą w praktyce niezawodnie zrobić. Zezwolenie systemowi operacyjnemu na zamrażanie, kompaktowanie i zapisywanie oddzielnego procesu interfejsu użytkownika w pamięci podręcznej zwykle wymaga znacznie mniej pamięci fizycznej, utrzymuje aplikację w niewidocznym limicie i ogranicza zamykanie aplikacji z powodu braku pamięci na urządzeniu.

Izolowanie pracy w tle w dedykowanym procesie

Aby wyeliminować rezydentność interfejsu i grafiki podczas pracy w tle, uruchamiaj długotrwałe komponenty działające w tle w dedykowanym, lekkim procesie, zachowując interfejs i silnik graficzny w głównym procesie:

Proces Deklaracja w pliku manifestu Komponenty hostowane Stan, gdy użytkownik opuszcza interfejs podczas pracy w tle
Główny proces interfejsu Domyślnie (com.example.app) Activity, widoki interfejsu, pamięci podręczne obrazów, mechanizm renderowania Vulkan lub OpenGL ES, silnik audio i sceny Spada do PREV, a następnie do CACHED (oom_score_adj >= 900). Zamrożone, skompresowane i zapisane z powrotem w pamięci flash.
Proces roboczy w tle android:process=":bg" Service na pierwszym planie, pobieranie zasobów w tle, pracownik synchronizacji lub menedżer połączonych urządzeń Działa w FGS (oom_score_adj = 200) tylko z własną stertą i kodem instancji roboczej, bez widoków interfejsu i kontekstów GPU.

Deklarowanie budżetów procesów i pamięci

Przypisz tło Service do prywatnego podprocesu, ustawiając android:process=":bg" w AndroidManifest.xml. Dwukropek na początku (:) tworzy proces prywatny ograniczony do identyfikatora UID aplikacji.

W Androidzie 17 QPR2 (poziom API 37.2) i nowszych możesz połączyć podział procesów z budżetami pamięci aplikacji na proces w bloku <processes>, aby system operacyjny wymuszał ścisły limit zestawu roboczego na procesie roboczym w tle bez ograniczania procesu interfejsu na pierwszym planie:

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

Pamięć używana w :bg jest wliczana zarówno do budżetu tego elementu, jak i do budżetu całej aplikacji. Na urządzeniach z wersjami starszymi niż Android 17 QPR2 system ignoruje <memory-budget>, a android:process=":bg" działa na wszystkich poziomach interfejsu API.

Inicjowanie bramki w Application.onCreate

Android tworzy instancję klasy Application i wywołuje metodę Application.onCreate w każdym procesie utworzonym dla Twojego pakietu. Częstym błędem podczas wdrażania android:process=":bg" jest pozostawianie ciężkich zestawów narzędzi interfejsu, pul ładowania obrazów, pakietów SDK do analityki lub inicjowania silnika natywnego (System.loadLibrary) w niechronionej metodzie Application.onCreate. Spowoduje to zwiększenie procesu :bg o ten sam podstawowy ślad, którego próbujesz uniknąć.

Sprawdź Application.getProcessName, który jest dostępny w Androidzie 9 (poziom interfejsu API 28) i nowszych, w onCreate i zainicjuj tylko zależności wymagane przez bieżący proces:

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

Biblioteki, które są inicjowane przez mechanizm dostarczania treści, takie jak Jetpack App Startup, są inicjowane tylko w głównym procesie, ponieważ mechanizm dostarczania treści zadeklarowany bez android:process działa w głównym procesie. Jeśli kod w :bg zależy od takiej biblioteki, zainicjuj ją bezpośrednio w initBackgroundWorkerProcess.

Jeśli tworzysz grę za pomocą silnika gry, takiego jak Unity, zaimplementuj usługę w tle w zwykłej bibliotece Androida lub wtyczce napisanej w języku Java lub Kotlin, która nie odwołuje się do klas silnika, takich jak UnityPlayer. Proces :bg nigdy nie wczytuje bibliotek natywnych silnika. Aby to potwierdzić, sprawdź, czy adb shell cat /proc/<pid>/maps dla procesu :bg nie zawiera libunity.so ani libil2cpp.so.

Komunikacja bez przypinania procesu interfejsu

Gdy użytkownik ma otwartą aplikację MainActivity, a AssetDownloadService działa w :bg, interfejs często musi wyświetlać postęp pobierania na żywo. Ważność procesu przepływa od klienta do procesu, który hostuje usługę lub mechanizm dostarczania treści, z którego korzysta, więc kierunek każdej zależności ma znaczenie:

  • UI binding to the worker is safe. Gdy MainActivity wiąże się z AssetDownloadService, system podnosi :bg do poziomu ważności widocznego interfejsu. Gdy użytkownik opuści aplikację, proces interfejsu zostanie zapisany w pamięci podręcznej, a powiązanie nie będzie już niczego podnosić.
  • Proces roboczy zależny od procesu interfejsu nie jest bezpieczny. Jeśli kod w :bg wiąże się z usługą lub wysyła zapytanie do mechanizmu dostarczania treści, który działa w głównym procesie, system podnosi ważność głównego procesu do poziomu ważności procesu roboczego, a pamięć interfejsu użytkownika jest ponownie przypięta. Zamiast tego zarejestruj w interfejsie wywołanie zwrotne z instancją roboczą, np. Messenger lub interfejs wywołania zwrotnego AIDL. Przekazanie obiektu binder jako wywołania zwrotnego nie zmienia ważności żadnego z procesów.

Wyrejestruj wywołanie zwrotne przed odłączeniem, aby proces roboczy nigdy nie wywoływał procesu interfejsu po jego zamrożeniu przez system. Poniższy przykład wiąże się z procesem roboczym tylko wtedy, gdy interfejs jest widoczny:

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

Więcej informacji o tym, jak flagi powiązań wpływają na oom_score_adj w różnych procesach, znajdziesz w artykule o powiązaniach usług i stanach procesów.

Bezpieczna koordynacja udostępnionego stanu na dysku

Ponieważ proces główny i :bg działają w oddzielnych przestrzeniach adresowych z oddzielnymi środowiskami wykonawczymi i deskryptorami plików:

  • SQLite i Room: używaj logów zapisywanych z wyprzedzeniem (domyślny tryb dziennika w Room na większości urządzeń), aby oba procesy mogły jednocześnie odczytywać i zapisywać bazę danych. Nie dzwoń pod numer enableMultiInstanceInvalidation z aplikacji :bg. Room hostuje usługę unieważniania w głównym procesie, więc proces roboczy powiąże się z głównym procesem i utrzyma pamięć interfejsu na poziomie podwyższonej ważności procesu roboczego. Zamiast tego pracownik powinien zgłaszać zmiany za pomocą wywołania zwrotnego, które rejestruje interfejs, a interfejs powinien ponownie wysyłać zapytanie do bazy danych po uruchomieniu.
  • Ustawienia: nie udostępniaj SharedPreferences między procesami. Użyj DataStore w kodzie wieloprocesowym lub przekaż stan bezpośrednio przez IPC.
  • Pliki zasobów: zapisz pobrane pakiety zasobów w pliku tymczasowym w :bg, wywołaj fdatasync, a następnie posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED) w :bg po zapisaniu (jak pokazano w sekcji Zwracanie stron pamięci do systemu operacyjnego) i niepodzielnie zmień nazwę pliku. Zamiast wywoływać proces interfejsu, który może być zawieszony, pozwól interfejsowi odebrać wynik przy następnym uruchomieniu i powiązaniu z instancją roboczą.

Obserwowanie efektu w powłoce adb

Możesz sprawdzić, czy rozdzielenie procesów na pierwszym planie i w tle izoluje zwiększone wykorzystanie pamięci, sprawdzając stany procesów w Menedżerze aktywności i dumpsys meminfo po przejściu na ekran główny. Przykładowe dane wyjściowe w tej sekcji pochodzą z przykładowej aplikacji MemoryLab, która w obu konfiguracjach uruchamia tę samą alokację natywną 64 MB w procesie interfejsu i hostuje podzieloną usługę na pierwszym planie w procesie :remote.

1. Porównywanie stanów procesów i wyników OOM

Uruchom zadanie w tle, naciśnij przycisk ekranu głównego, aby zatrzymać aplikację Activity, i wyślij zapytanie do menedżera aktywności:

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

W architekturze jednoprocesowej proces główny pozostaje podniesiony, mimo że jego Activity jest zatrzymany. Przez około 15 sekund po opuszczeniu aplikacji przez użytkownika Menedżer aktywności utrzymuje proces, który uruchamia usługę na pierwszym planie, na poziomie oom_score_adj = 50 (fg-service-act), a następnie obniża go do 200 (fg-service). Te dane wyjściowe zostały zarejestrowane 20 sekund po naciśnięciu przycisku ekranu głównego:

    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

W architekturze z podziałem procesów tylko proces roboczy pozostaje na poziomie oom_score_adj = 200, a główny proces interfejsu spada do poziomu PREV (oom_score_adj = 700), a później do zasobnika pamięci podręcznej (oom_score_adj >= 900):

    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

Po upływie limitu czasu poprzedniej aplikacji proces główny jest przenoszony do zasobnika pamięci podręcznej (cch, oom_score_adj = 900, previous-expired) i traci wszystkie możliwości (---------), w tym niejawny czas pracy procesora (I). W tym momencie system go zamraża i może go skompresować lub odzyskać w dowolnym momencie bez przerywania procesu roboczego.

2. Porównywanie dumpsys meminfo w różnych procesach

Porównaj podsumowanie dumpsys meminfo procesu roboczego z podwyższonymi uprawnieniami z procesem interfejsu:

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

Przykładowe dane wyjściowe App Summary dla procesu roboczego MemoryLab o 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

Przykładowe dane wyjściowe App Summary dla procesu interfejsu MemoryLab w oom_score_adj = 700, który przechowuje natywną alokację 64 MB w Private Other:

 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

Proces roboczy potrzebuje mniej niż 5 MB PSS, ponieważ większość kodu i stron środowiska wykonawczego współdzieli z Zygote. W aplikacji, która renderuje za pomocą Vulkan lub OpenGL ES, wiersz Graphics procesu interfejsu zawiera też pamięć GPU. Ta próbka została zarejestrowana na urządzeniu wirtualnym Cuttlefish z renderowaniem programowym, więc Graphics to 0 w obu procesach.

3. Sprawdzanie rozliczeń cgroup dla poszczególnych procesów

Każdy proces w ramach Twojego identyfikatora UID ma własny katalog cgroup v2 w nadrzędnej grupie kontrolnej aplikacji. To polecenie wyświetla oom_score_adj, memory.current i memory.high na poziomie aplikacji dla każdego procesu aplikacji:

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
'

Przykładowe dane wyjściowe MemoryLab po naciśnięciu przycisku strony głównej:

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

Proces roboczy jest obciążany mniej niż 4 MB, a proces interfejsu użytkownika – około 98 MB. Każdy proces ma własną wartość memory.current, ale ogranicznik pamięci ustawia limit dla nadrzędnej grupy kontrolnej, która zawiera wszystkie procesy aplikacji. Obie linie pokazują tę samą wartość app.memory.high wynoszącą 966 787 072 bajtów, czyli niewidoczny limit 922 MB w poniższych danych wyjściowych.

Aby zobaczyć widoczne i niewidoczne limity, które ogranicznik pamięci stosuje na urządzeniu, uruchom to polecenie:

adb shell am memory-limiter status

Przykładowe dane wyjściowe z urządzenia z 4 GB pamięci RAM, gdzie limit niewidocznych aplikacji wynosi połowę limitu widocznych aplikacji:

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

Obserwowanie efektu w Perfetto

Perfetto umożliwia wizualizację osi czasu oom_score_adj i RSS w obu procesach, gdy użytkownik przechodzi z interfejsu na pierwszym planie do ekranu głównego, podczas gdy praca w tle jest kontynuowana.

1. Nagrywanie logu czasu

Nagraj 15-sekundowy ślad, który rejestruje liczniki pamięci procesów, pamięć GPU i zmiany oom_score_adj podczas uruchamiania zadania w tle w aplikacji i przechodzenia do ekranu głównego:

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. Sprawdzanie śladów procesów w interfejsie Perfetto

Otwórz log śledzenia w interfejsie Perfetto i rozwiń grupy procesów dla com.example.app i com.example.app:bg:

  1. Przypnij ścieżki oom_score_adj i mem.rss.anon dla obu procesów, aby wyświetlały się obok siebie u góry osi czasu.
  2. Znajdź sygnaturę czasową, przy której użytkownik naciska przycisk ekranu głównego:
    • W procesie instancji roboczej oom_score_adj pozostaje na poziomie 200, a mem.rss.anon pozostaje na niskim poziomie.
    • W procesie interfejsu oom_score_adj wzrasta z 0 do 200, pozostaje na tym poziomie przez około sekundę, podczas gdy Activity się zatrzymuje, a następnie wzrasta do 700 (PREV). Po upływie limitu czasu poprzedniej aplikacji przechodzi do zakresu pamięci podręcznej, gdzie system może skompresować i odzyskać pamięć bez wpływu na proces roboczy.
Ślady Perfetto dla procesu interfejsu i procesu roboczego. Po naciśnięciu przycisku ekranu głównego wartość oom_score_adj procesu UI wzrasta z 0 do 200, a następnie do 700, podczas gdy wartość procesu roboczego pozostaje na poziomie 200 przy około 19 MB anonimowego RSS.
Rysunek 2. Procesy dzielone po naciśnięciu przez użytkownika przycisku ekranu głównego.

Na rysunku 2 proces interfejsu użytkownika przez chwilę pozostaje na poziomie 200, podczas gdy jego Activity zatrzymuje się, a następnie wzrasta do 700, podczas gdy proces roboczy pozostaje na poziomie 200 z około 19 MB anonimowego RSS. Dla porównania rysunek 3 przedstawia 35-sekundowy ślad konfiguracji współdzielonej, w której pojedynczy proces pozostaje w stanie 50, a następnie 200 z całą pamięcią interfejsu.

Ślady Perfetto dla pojedynczego procesu w tej samej lokalizacji. oom_score_adj wzrasta z 0 do 50 po przejściu na ekran główny i do 200 około 15 sekund później, podczas gdy anonimowy RSS pozostaje wysoki.
Rysunek 3. procesu współlokowanego po naciśnięciu przez użytkownika przycisku ekranu głównego;

Aby zapoznać się z tymi widokami, otwórz ślad podzielony i ślad współlokalizowany w interfejsie Perfetto.

3. Wykonywanie zapytania dotyczącego zwiększonego wykorzystania pamięci za pomocą PerfettoSQL

Aby zmierzyć średni rozmiar anonimowego i plikowego zbioru roboczego (RSS) utrzymywanego przez każdy proces aplikacji na każdym poziomie oom_score_adj, uruchom na karcie Zapytanie (SQL) to zapytanie PerfettoSQL:

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;

Przykładowe dane wyjściowe dla śladu podziału na rysunku 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

Przykładowe dane wyjściowe śladu w tej samej lokalizacji na rysunku 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

W konfiguracji podzielonej po pierwszej sekundzie w oom_score_adj = 200 pozostaje tylko około 19 MB pamięci anonimowej. W konfiguracji współdzielonej około 117 MB pozostaje w 50, a następnie w 200 przez cały czas działania usługi na pierwszym planie.


← Zwracanie stron pamięci do systemu operacyjnego | ↑ W górę