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 stanieTOP(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) lubCACHED(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ąCachedAppOptimizeri pozwalalmkdzakoń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,connectedDevicelublocationtypu, na poziomieoom_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 zatrzymaniuActivity.
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
Activityi hierarchia widoków: jeśli obiektActivitynie zostanie wyraźnie zakończony, jego obiektWindow, 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
HardwareRendererzachowuje swój kontekst GPU i pamięć podręczną, a zatrzymany elementActivitydział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
lmkdmusi 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
MainActivitywiąże się zAssetDownloadService, system podnosi:bgdo 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
:bgwiąż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.Messengerlub 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
enableMultiInstanceInvalidationz 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
SharedPreferencesmię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łajfdatasync, a następnieposix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED)w:bgpo 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:
- Przypnij ścieżki
oom_score_adjimem.rss.anondla obu procesów, aby wyświetlały się obok siebie u góry osi czasu. - Znajdź sygnaturę czasową, przy której użytkownik naciska przycisk ekranu głównego:
- W procesie instancji roboczej
oom_score_adjpozostaje na poziomie200, amem.rss.anonpozostaje na niskim poziomie. - W procesie interfejsu
oom_score_adjwzrasta z0do200, pozostaje na tym poziomie przez około sekundę, podczas gdyActivitysię zatrzymuje, a następnie wzrasta do700(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.
- W procesie instancji roboczej
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.
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ę