Budżety pamięci aplikacji umożliwiają aplikacjom deklarowanie własnego budżetu pamięci, co informuje system, że ma ograniczyć wykorzystanie pamięci, gdy aplikacja przekroczy ustawiony budżet. Jest to szczególnie przydatne w przypadku aplikacji systemowych i pakietów aplikacji lub aplikacji przeznaczonych na urządzenia z ograniczoną pamięcią, gdy deweloper zna oczekiwany zestaw roboczy pamięci (pamięć, której aplikacja aktywnie potrzebuje do wykonywania bieżących działań użytkownika) i chce mieć pewność, że aplikacja nie będzie używać zbyt dużej ilości współdzielonych zasobów pamięci RAM systemu.
Budżet jest utrzymywany na odpowiednim poziomie dzięki wykluczaniu i wymianie pamięci, co pozwala usunąć strony pamięci, które nie były ostatnio używane, i skoncentrować wykorzystanie pamięci przez aplikację na jej bieżącym zestawie roboczym. Gdy aplikacja przekroczy zadeklarowany budżet, system operacyjny odzyskuje pamięć tylko z tej aplikacji:
- Czyste strony oparte na plikach (takie jak nieaktywny kod i zmapowane zasoby) są usuwane jako pierwsze, ponieważ w razie potrzeby można je ponownie odczytać z pamięci.
- Zmodyfikowane strony powiązane z plikami są zapisywane z powrotem w pamięci i usuwane.
- Anonimowe strony pamięci (np. alokacje sterty) są kompresowane i przenoszone do zRAM.
Dopóki zestaw roboczy nie przekracza budżetu, aplikacja będzie działać prawidłowo, nie zużywając więcej pamięci niż określono w budżecie. System operacyjny usuwa nieużywaną pamięć i kompresuje nieaktywne strony sterty do pamięci wymiany, dzięki czemu alokacje pamięci pozostają ograniczone bez przerywania procesu.
Co się stanie, gdy aplikacja przekroczy budżet
Przekroczenie budżetu pamięci nie powoduje zamknięcia ani awarii aplikacji. Zamiast tego, gdy aplikacja używa więcej pamięci, niż pozwala na to jej budżet, system operacyjny znajduje pamięć, której aplikacja nie używała od jakiegoś czasu, i bezpiecznie ją odkłada, dopóki nie będzie ponownie potrzebna. Dzięki temu można przydzielić pamięć nowym aplikacjom, a ogólne wykorzystanie pamięci przez aplikacje nie przekracza limitu.
Dopóki istnieje margines między tym, czego aplikacja potrzebuje w danym momencie (jej zestawem roboczym), a ustawionym budżetem, aplikacja działa normalnie. Jeśli aplikacja ustawi budżet mniejszy niż jej zestaw roboczy, jej wydajność w czasie działania może się zmniejszyć.
Aby dowiedzieć się, jak system operacyjny śledzi i odzyskuje pamięć, zapoznaj się z przewodnikiem po architekturze pamięci:
- Odzyskiwanie pamięci i wymiana: wyjaśnia, jak system operacyjny obciąża aplikację prywatnymi anonimowymi stronami sterty i stronami opartymi na plikach oraz jak odzyskuje nieużywane strony, gdy przekroczony zostanie limit.
- Podstawowe pojęcia: wyjaśnia, dlaczego współdzielona pamięć Zygote jest wykluczona z budżetu aplikacji i jak budżety pamięci stałej są powiązane z anonimowym RSS i pamięcią wymiany w telemetrii terenowej.
- Bitmapy i pamięć: wyjaśnia, jak bitmapy sprzętowe i alokacje DMA-BUF obsługiwane przez GPU są rozliczane poza stertą i budżetem pamięci aplikacji.
Deklarowanie budżetów w pliku manifestu Androida
Zadeklarowanie budżetów pamięci w pliku AndroidManifest.xml jest podstawową i zalecaną metodą definiowania budżetów. Nie wymaga kodu środowiska wykonawczego, zaczyna działać natychmiast po uruchomieniu procesu i zapewnia jasną umowę dla systemu operacyjnego.
Deklaracje <memory-budget> obowiązują na urządzeniach z Androidem 17 QPR2 (poziom interfejsu API 37.2) i nowszym. W starszych wersjach Androida parser pliku manifestu platformy bezpiecznie ignoruje nierozpoznane elementy XML, dzięki czemu możesz używać <memory-budget> bez wpływu na zgodność wsteczną.
Zadeklaruj budżet podstawowy
W przypadku większości aplikacji wystarczy zdefiniować jeden budżet. Zadeklaruj element <memory-budget> bezpośrednio w tagu <application>:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.simpleapp">
<application
android:label="@string/app_name">
<!-- Baseline budget for the application -->
<memory-budget android:maxMb="256" />
</application>
</manifest>
Ustawia budżet pamięci rezydentnej na poziomie 256 MB dla wszystkich procesów i stanów pakietu. Gdy wykorzystanie pamięci przez aplikację przekracza 256 MB, system operacyjny przycina nieaktywne strony pamięci, używając wykluczania i wymiany.
Zmienianie budżetów w zależności od stanu procesu
Aplikacja wymaga różnych ilości pamięci w zależności od widoczności dla użytkownika:
- Na pierwszym planie: proces hostuje widoczną aktywność, która wchodzi w interakcję z użytkownikiem. Ten stan zwykle zajmuje najwięcej pamięci ze względu na aktywny interfejs i grafikę.
- Zauważalny: proces jest zauważalny dla użytkownika, ale nie zawiera widocznego okna (np. zawiera usługę na pierwszym planie odtwarzającą multimedia, aktywne pobieranie w tle, szczegółowe wskazówki dojazdu lub aktywną metodę wprowadzania danych).
- W tle: proces uruchamia zadania w tle, odbiorniki lub synchronizację danych. Powinien on zajmować minimalną ilość miejsca.
Możesz zadeklarować wiele klauzul <memory-budget>, aby dopasować te stany:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.simpleapp">
<application
android:label="@string/app_name">
<!-- Default budget for visible foreground UI -->
<memory-budget android:maxMb="200" />
<!-- Tighter budget when playing audio in background -->
<memory-budget
android:maxMb="120"
android:state="perceptible" />
<!-- Minimal budget when fully in background -->
<memory-budget
android:maxMb="48"
android:state="background" />
</application>
</manifest>
Klauzula podstawowa bez android:state nie jest wymagana. Jeśli określisz tylko klauzule dotyczące konkretnych stanów (np. android:state="background"), inne stany nie będą ograniczone budżetem aplikacji. Jeśli klauzula nie zawiera android:state, działa jako domyślna klauzula zastępcza dla nieokreślonych stanów (takich jak pierwszy plan), którą kolejne, bardziej restrykcyjne klauzule zastępują, gdy aplikacja przechodzi w stan perceptible lub background.
Podczas określania budżetów dla każdego stanu należy kierować się aktywnym zestawem roboczym tego stanu, a nie szczytowym wykorzystaniem pamięci (maksymalnym poziomem RSS):
- Określ budżety
foregroundna potrzeby płynnego renderowania klatek: zapewnij interfejsowi na pierwszym planie wystarczającą ilość miejsca na aktywny zestaw roboczy, aby synchroniczne odzyskiwanie nie opóźniało renderowania klatek. Jeśli aplikacja zawiera samodzielny proces wymagający dużej ilości pamięci (np. edytor obrazów lub filmów w wysokiej rozdzielczości), rozważ przeniesienie tego działania do osobnego procesu (zobacz Aplikacje wieloprocesowe), aby jego alokacje były rozliczane niezależnie i usuwane, gdy użytkownik wyjdzie z aplikacji. - Ścisłe określanie budżetów
backgroundna potrzeby zadań bez interfejsu: wysoki poziom RSS w tle w telemetrii terenowej często odzwierciedla alokacje interfejsu zachowane po opuszczeniu pierwszego planu (patrz Umieszczanie powiązanej usługi w tym samym miejscu co interfejs wymagający dużej ilości pamięci i Zwalnianie pamięci w odpowiedzi na zdarzenia) lub jednorazowe odczyty plików podczas indeksowania i synchronizacji w tle. Ponieważ zadania w tle bez interfejsu nie mają terminów klatek, wykluczenie jednorazowych stron pamięci podręcznej plików i zamiana nieaktywnych stron sterty na zRAM, gdy synchronizacja w tle na krótko przekracza budżet, nie powoduje widocznych dla użytkownika zacięć, a jednocześnie zapobiega wykluczaniu przez skoki w tle aplikacji użytkownika na pierwszym planie.
Jak stany budżetu są mapowane na stany procesu
Platforma mapuje stany budżetu z pliku manifestu na stany procesu środowiska wykonawczego na podstawie RunningAppProcessInfo.importance, które menedżer aktywności oblicza na podstawie aktywnych komponentów procesu i przychodzących powiązań usług lub zapytań dostawcy treści z innych aplikacji:
foreground: aktywne, widoczne interakcje użytkownika, takie jak hostowanie wznowionej aktywności, utrzymywanie stanu aplikacji na pierwszym planie po wyłączeniu ekranu lub hostowanie usługi albo dostawcy treści aktywnie powiązanych z aplikacją na pierwszym planie lub przez nią odpytywanych.perceptible: zadania, które są zauważalne dla użytkownika bez widocznego okna, takie jak aktywne odtwarzanie multimediów, szczegółowa nawigacja, nagrywanie aparatem lub mikrofonem, aktywne pobieranie w tle, usługi na pierwszym planie synchronizujące dane lub usługi powiązane przez klienta na pierwszym planie za pomocą flag takich jakBIND_NOT_FOREGROUND(patrz Kontrolowanie dziedziczenia za pomocą flag BIND). Chociaż wewnętrzne dane platformy mogą oznaczać niektóre z tych zadań jakoPROCESS_STATE_IMPORTANT_FOREGROUND, ta wewnętrzna stała nie oznacza widocznego okna i jest regulowana przez budżetperceptible.background: zadania, które nie są od razu widoczne dla użytkownika, takie jak zadania w tle, alarmy, odbiorniki komunikatów lub poprzednia aktywność po opuszczeniu aplikacji przez użytkownika.
Tabela poniżej pokazuje, jak poziomy ważności środowiska wykonawczego są mapowane na stany pliku manifestu:
Plik manifestu android:state |
Ważność środowiska wykonawczego (RunningAppProcessInfo) |
Typowe komponenty |
|---|---|---|
foreground |
IMPORTANCE_FOREGROUNDIMPORTANCE_TOP_SLEEPING |
Wznowiona widoczna aktywność, aplikacja na pierwszym planie z zablokowanym ekranem, usługa lub mechanizm dostarczania treści powiązany z aplikacją na pierwszym planie |
perceptible |
IMPORTANCE_FOREGROUND_SERVICEIMPORTANCE_VISIBLE |
Aktywne usługi działające na pierwszym planie, które odtwarzają multimedia, obsługują nawigację, pobierają lub synchronizują dane; usługi powiązane z flagami foreground-service lub visible |
background |
IMPORTANCE_PERCEPTIBLEIMPORTANCE_CANT_SAVE_STATEIMPORTANCE_SERVICE |
Zadania w tle, odbiorniki, synchronizacja w tle, poprzednia aktywność lub aplikacja główna w tle |
Szczegółowe informacje o tym, jak powiązania klienta podnoszą proces hosta usługi lub dostawcy i jak proces wraca do stanu background lub cached po odłączeniu klienta lub przeniesieniu go poza ekran, znajdziesz w artykule Powiązania usług i stany procesów.
Gdy proces przechodzi w stan buforowania (IMPORTANCE_CACHED) bez aktywnych komponentów, platforma zwalnia jego aktywny budżet pamięci i zarządza pamięcią za pomocą standardowego odzyskiwania pamięci buforowanej.
Sprawdzanie stanu procesu i budżetu aplikacji
Jednym ze sposobów sprawdzenia stanu i ważności aktywnego procesu aplikacji podczas jej tworzenia jest wysłanie zapytania do Menedżera aktywności za pomocą ADB:
adb shell dumpsys activity processes <package-name>
Poniższy skrócony przykład pokazuje wpisy dotyczące rekordu procesu i kontroli OOM w przypadku aplikacji, która uruchamia usługę na pierwszym planie:
ACTIVITY MANAGER RUNNING PROCESSES (dumpsys activity processes)
All known processes:
*APP* UID 10123 ProcessRecord{edf056c 3919:com.example.app/u0a123}
pid=3919
oom adj: max=1001 curRaw=200 setRaw=200 cur=200 set=200
curProcState=4 mRepProcState=4 setProcState=4 lastStateTime=-42s360ms
hasStartedServices=true
mHasForegroundServices=true forcingToImportant=null
...
Process OOM control (48 total):
Proc #22: prcp F/S/FGS ---NFU-TI t: 0 3919:com.example.app/u0a123 (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 tych danych wyjściowych:
curProcState=4istate: cur=FGSwskazują, że proces jest w stanie usługi na pierwszym planie. Jeśli aplikacja hostuje aktywną, widoczną aktywność, jest to oznaczone jakoTOP. Jeśli działa w niej ważny komponent na pierwszym planie (np. pobieranie lub synchronizacja), jest wyświetlana jakoIMPF.- W tabeli kontroli braku pamięci procesu
prcpoznacza, że proces jest oceniany na poziomie priorytetu „zauważalny”, co odpowiada budżetowiperceptible.
Aby sprawdzić aktualnie wymuszany budżet pamięci i wykorzystanie pamięci rezydentnej:
adb shell dumpsys meminfo <package-name>
Począwszy od Androida 17 QPR2, te dane wyjściowe zawierają sekcję Budżet pamięci, która wyświetla aktywny limit, źródło limitu (np. AndroidManifest lub MemoryBudgetManager) i bieżącą pamięć rezydentną.
Aplikacje wieloprocesowe
Jeśli aplikacja dzieli pracę na wiele procesów, skonfiguruj dedykowane budżety procesów za pomocą tagu <process> w tagu <processes>.
Weźmy na przykład aplikację do strumieniowego odtwarzania muzyki (com.example.radio):
- Proces główny: hostuje widoczny interfejs i silnik odtwarzania dźwięku (
MediaSessionServicez usługąmediaPlaybackna pierwszym planie). Gdy proces jest widoczny, działa w ramach budżetu 180 MB na procesy na pierwszym planie. Gdy użytkownik opuści aplikację, a muzyka będzie nadal odtwarzana, proces przejdzie w stanperceptible, w którym budżet 64 MB wystarczy na silnik odtwarzania i bufor audio. - Proces synchronizacji (
:sync): dedykowany proces, który w tle synchronizuje metadane i indeksuje pobrane pliki. Ponieważ ten proces jest aktywny tylko w tle, nie musisz wyraźnie deklarowaćstate="background". Obowiązuje jeden budżet.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.radio">
<application
android:label="@string/app_name">
<!-- Package baseline: main process with UI and audio playback -->
<memory-budget android:maxMb="180" />
<!-- Tighter budget when audio plays in the background -->
<memory-budget
android:maxMb="64"
android:state="perceptible" />
<processes>
<!-- Dedicated background sync process -->
<process android:process=":sync">
<memory-budget android:maxMb="32" />
</process>
</processes>
<service
android:name=".playback.AudioPlayerService"
android:foregroundServiceType="mediaPlayback"
android:exported="false" />
<service
android:name=".sync.PlaylistSyncService"
android:process=":sync"
android:exported="false" />
</application>
</manifest>
Gdy deklarujesz blok <processes> w pliku manifestu, podaj wpisy <process> tylko w przypadku procesów, które deklarują <memory-budget>. Jeśli chcesz określić budżet tylko dla konkretnych podprocesów, a dla pozostałych części aplikacji pozostawić limit systemowy urządzenia, możesz pominąć elementy <memory-budget> bezpośrednio pod <application>.
Wykorzystanie pamięci w dowolnym podprocesie jest wliczane zarówno do budżetu procesu, jak i budżetu pakietu, który go zawiera. Proces doświadcza obciążenia pamięci w czasie działania, jeśli przekroczy budżet procesu lub budżet pakietu, w zależności od tego, który próg zostanie osiągnięty jako pierwszy.
Skalowanie budżetów w przypadku wyświetlaczy o dużej gęstości
W przypadku aplikacji, których wykorzystanie pamięci znacznie się zwiększa wraz z liczbą pikseli do narysowania na ekranie jednocześnie (np. aplikacja galerii zdjęć, która buforuje bitmapy o rozmiarze ekranu), Android udostępnia 2 alternatywne mechanizmy dynamicznego skalowania budżetów zgodnie ze specyfikacją wyświetlacza:
Skalowanie według przedziału gęstości wyświetlacza (
android:additionalMbPerDensity): dodaje megabajty proporcjonalnie do kwadratu współczynnika gęstości wyświetlacza względemmdpi(1,0x / 160 dpi), ponieważ elementy rysowalne rastrowe 2D i zasoby interfejsu są skalowane zarówno pod względem szerokości, jak i wysokości:<!-- Baseline 180MB + 16MB multiplied by (density ratio)^2 --> <memory-budget android:maxMb="180" android:additionalMbPerDensity="16" />Na wyświetlaczu
mdpi(1,0x) budżet wynosi 180 + 16 × 12 = 196 MB. Na wyświetlaczuxxhdpi(3,0x) budżet skaluje się do 180 + 16 × 32 = 324 MB.Skalowanie według fizycznej rozdzielczości wyświetlacza (
android:additionalBytesPerDisplayPixel): dodaje bajty bezpośrednio na piksel fizycznego wyświetlacza (szerokość × wysokość). Jest to idealne rozwiązanie w przypadku aplikacji, które przydzielają pełnoekranowe powierzchnie graficzne, bufory renderowania lub pamięć podręczną zdjęć w pełnej rozdzielczości, gdzie zużycie pamięci skaluje się bezpośrednio z liczbą nieprzetworzonych pikseli wyświetlacza, a nie z zasobnikami gęstości interfejsu:<!-- Baseline 128MB + 16 bytes per physical display pixel --> <!-- For example, a 4-byte RGBA full-screen buffer with double or quadruple buffering --> <memory-budget android:maxMb="128" android:additionalBytesPerDisplayPixel="16" />Na wyświetlaczu 1080p (1080 × 2400 ≈ 2,59 mln pikseli) dodaje to ≈ 41,4 MB do budżetu podstawowego. Na wyświetlaczu 1440p (1440 × 3120 ≈ 4,49 mln pikseli) dodaje ≈ 71,8 MB.
Te 2 atrybuty są alternatywne. Wybierz atrybut, który odpowiada głównemu współczynnikowi skalowania aplikacji, i unikaj łączenia obu tych atrybutów w jednej klauzuli.
Dostosowywanie do formatów urządzeń
Jeśli wysyłasz plik APK na telefony, tablety i urządzenia z Wear OS, użyj atrybutu android:feature, aby dostosować budżety do różnych urządzeń docelowych.
Na zegarkach z Wear OS pamięć RAM jest ograniczona, a interfejs i zestaw funkcji aplikacji są znacznie prostsze. Możesz zadeklarować bardziej rygorystyczny budżet przeznaczony na funkcję watch:
<!-- General phone and tablet baseline -->
<memory-budget android:maxMb="180" />
<!-- Wear OS override: simpler UI and constrained hardware -->
<memory-budget
android:maxMb="48"
android:feature="watch" />
Reguła rozwiązywania: obowiązuje ostatnia klauzula, która ma zastosowanie
Jeśli dla aplikacji lub procesu zdefiniowano kilka elementów <memory-budget>, system ocenia je w kolejności, w jakiej zostały zadeklarowane w pliku manifestu. Egzekwowana jest ostatnia obowiązująca klauzula budżetowa.
Kolejność ma znaczenie, ponieważ wygrywa ostatni odpowiedni budżet. Zawsze najpierw umieszczaj najbardziej ogólny budżet bazowy, a potem bardziej szczegółowe zastąpienia (np. klauzule dotyczące konkretnego stanu lub sprzętu).
Dokumentacja atrybutów XML
Wszystkie atrybuty rozmiaru pamięci są wyrażone w megabajtach (MB) i odpowiadają obciążeniu memory.current cgroup w systemie Linux (które nie obejmuje pamięci współdzielonej, takiej jak Zygote).
| Atrybut | Format | Domyślny | Opis |
|---|---|---|---|
android:maxMb |
Liczba całkowita (> 0) | Wymagany | Podstawowy limit budżetu pamięci rezydentnej w MB. |
android:state |
Typ wyliczeniowy | Dowolna | Stan procesu, którego dotyczy ten budżet: foreground, perceptible lub background. Jeśli zostanie pominięta, klauzula będzie działać jako zastępcza dla każdego nieokreślonego stanu. |
android:additionalMbPerDensity |
Liczba całkowita (≥ 0) | 0 |
Dodatkowe megabajty pomnożone przez kwadrat współczynnika gęstości wyświetlania względem mdpi (1,0x). |
android:additionalBytesPerDisplayPixel |
Liczba całkowita (≥ 0) | 0 |
Dodatkowe bajty przydzielone na piksel wyświetlacza fizycznego (szerokość × wysokość), przydatne w przypadku buforów powierzchni i bitmap. |
android:feature |
Ciąg znaków | Dowolna | Ogranicza klauzulę do urządzeń, które deklarują określone funkcje sprzętowe: watch, automotive lub leanback. |
Interfejsy API środowiska wykonawczego (dodatkowa opcja dynamiczna)
Deklarowanie budżetów statycznie w pliku AndroidManifest.xml jest preferowanym rozwiązaniem w przypadku niemal wszystkich aplikacji. Jednak w przypadku aplikacji z dynamicznymi zadaniami lub eksperymentów w czasie działania Android udostępnia interfejsy API SDK i NDK jako opcję dodatkową.
Interfejs API środowiska wykonawczego umożliwia:
- Sprawdź bieżące wykorzystanie pamięci i efektywne budżety.
- dynamicznie zmniejszać budżet procesu;
- Nasłuchuj zdarzeń przekroczenia budżetu, aby proaktywnie przycinać pamięć podręczną, zanim system operacyjny uruchomi bezpośrednie odzyskiwanie.
Interfejs API pakietu Android SDK (MemoryBudgetManager)
Usługa systemowa MemoryBudgetManager jest dostępna dla aplikacji napisanych w Kotlin i Java od Androida 17 QPR2 (mniejsza wersja pakietu SDK, poziom API 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2).
Pobieranie usługi
Zanim uzyskasz dostęp do aplikacji MemoryBudgetManager, sprawdź, czy na urządzeniu jest zainstalowany Android 17 QPR2 lub nowszy. Możesz to zrobić w sekcji SDK_INT_FULL. Jeśli minSdk aplikacji jest niższy niż 36, najpierw sprawdź SDK_INT, aby urządzenia z Androidem 15 (API na poziomie 35) lub starszym nie zgłaszały błędu NoSuchFieldError podczas odczytywania SDK_INT_FULL:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA &&
Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN_2
) {
val budgetManager = context.getSystemService(MemoryBudgetManager::class.java)
}
Wykorzystanie zapytań i budżety
// Query current memory charged to this process and the package UID
val processUsageBytes = budgetManager.processCurrentUsageBytes
val packageUsageBytes = budgetManager.packageCurrentUsageBytes
// Query effective budgets (returns LIMIT_IS_DISABLED if unconstrained)
val processBudgetBytes = budgetManager.processBudgetBytes
val packageBudgetBytes = budgetManager.packageBudgetBytes
dynamicznie ustawiać lub usuwać budżety,
Możesz ustawić bardziej rygorystyczny budżet w czasie działania, aby ograniczyć pamięć podczas wykonywania lekkich zadań, lub wyczyścić go po zakończeniu zadania:
// Set a tighter dynamic budget on the current process (e.g., 96 MB)
try {
budgetManager.processBudgetBytes = 96L * 1024L * 1024L
} catch (e: IllegalArgumentException) {
// Thrown if the budget is <= 0 or exceeds the manifest ceiling or system limit
Log.e(TAG, "Requested budget exceeds manifest or system ceiling", e)
}
// Clear the dynamic process budget to restore the manifest limit (or unconstrained baseline)
budgetManager.clearProcessBudget()
W przeciwieństwie do klauzul manifestu <memory-budget>, które automatycznie przełączają budżety, gdy proces przechodzi między stanami foreground, perceptible i background, budżet dynamiczny ustawiony za pomocą MemoryBudgetManager stosuje jeden pułap, dopóki kod go nie zaktualizuje lub nie wyczyści. Jeśli używasz interfejsu API środowiska wykonawczego do przeprowadzania eksperymentów terenowych lub zarządzania limitami związanymi ze stanem, zaktualizuj lub wyczyść budżet dynamiczny w ramach przejść cyklu życia (np. za pomocą ProcessLifecycleOwner lub wywołań zwrotnych cyklu życia działania, jak pokazano w przykładzie adaptacyjnego edytora obrazów).
Wydajność i idempotencja
Podczas pracy z MemoryBudgetManager w czasie działania pamiętaj o tych cechach:
- Idempotentność: wszystkie interfejsy API do mutacji budżetu są idempotentne. Wielokrotne wywoływanie funkcji
clearProcessBudget()lubclearPackageBudget()albo wywoływanie ich, gdy nie jest aktywny żaden budżet dynamiczny, jest bezpieczną operacją, która nie powoduje żadnych skutków. Podobnie ustawienie funkcjisetProcessBudgetBytes()lubsetPackageBudgetBytes()na tę samą wartość nie powoduje zbędnych rekonfiguracji systemu. - Częstotliwość wywołań: ustawienie, aktualizacja lub wyczyszczenie budżetu powoduje wywołanie międzyprocesowe usługi systemowej w celu zaktualizowania kontrolera pamięci systemu operacyjnego dla procesu. Chociaż jest lekki, nie jest całkowicie bezpłatny. Wywołuj te interfejsy API podczas dyskretnych przejść cyklu życia i kamieni milowych zadań (np. wejścia lub wyjścia z aktywności, rozpoczęcia lub zakończenia przetwarzania w tle albo obsługi żądań mowy). Unikaj wywoływania ich w pętlach, wątkach przetwarzania dźwięku lub procedurach renderowania klatek.
- Częstotliwość zapytań: właściwości zapytania
processCurrentUsageBytesipackageCurrentUsageBytespobierają z usługi systemowej informacje o wykorzystaniu pamięci na żywo. Mimo że zapytania są szybkie, powinny być wywoływane na żądanie (np. podczas przejść między zadaniami lub w wywołaniach zwrotnychOnOverBudgetListener), a nie odpytywane w pętlach ciągłych.
Nasłuchiwanie wywołań zwrotnych dotyczących przekroczenia budżetu
Aplikacje mogą zarejestrować detektor, który będzie powiadamiany, gdy wykorzystanie pamięci przekroczy próg budżetu. Dzięki temu aplikacja może przeprowadzać proaktywne czyszczenie na poziomie aplikacji (np. czyszczenie pamięci podręcznej bitmap w pamięci RAM), podczas gdy system operacyjny odzyskuje pamięć, aby utrzymać proces lub pakiet w ramach budżetu:
val cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }
val listener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
Log.w(TAG, "Process exceeded memory budget of $budgetBytes bytes")
// Proactively evict thread-safe caches on the background Looper
imageTileCache.evictAll()
}
// Register on a background Looper before tightening a dynamic budget
budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, listener)
// When done (for example, in onStop)
budgetManager.unregisterProcessOverBudgetListener(listener)
Sprawdzone metody dotyczące wywołań zwrotnych w przypadku przekroczenia budżetu:
- Używaj tła
Looper: gdy tylko jest to możliwe, przekazuj tłoHandlerThreadLooperdoregisterProcessOverBudgetListenerlubregisterPackageOverBudgetListener. Uruchamianie usuwania pamięci podręcznej lub czyszczenia plików w wątku w tle pozwala uniknąć rywalizacji z renderowaniem klatek w wątku UI, gdy proces jest pod presją pamięci. ZarezerwujLooper.getMainLooperdla obiektów nasłuchujących, które mają dostęp tylko do stanu interfejsu powiązanego z głównym wątkiem. - Spodziewaj się powtarzających się wywołań zwrotnych: wywołania zwrotne dotyczące przekroczenia budżetu nie są jednorazowymi powiadomieniami. Gdy wykorzystanie przekroczy budżet, system operacyjny najpierw odzyska nieużywane strony, a jeśli obciążenie będzie się utrzymywać, system będzie nadal odzyskiwać pamięć i wywoływać detektor maksymalnie raz na sekundę. Upewnij się, że wywołanie zwrotne jest idempotentne. Jeśli aplikacja potrzebuje tylko jednego powiadomienia lub dłuższego czasu oczekiwania między czyszczeniami, wywołaj
unregisterProcessOverBudgetListener(lubunregisterPackageOverBudgetListener) w wywołaniu zwrotnym albo zabezpiecz czyszczenie za pomocą sygnatury czasowej czasu oczekiwania lubAtomicBoolean. - Zarejestruj się przed zmniejszeniem budżetu: jeśli bieżące wykorzystanie pamięci przekracza już nowy budżet, gdy wywołujesz funkcję
setProcessBudgetByteslubsetPackageBudgetBytes, zdarzenie przekroczenia budżetu jest wywoływane, gdy tylko zostanie zastosowany nowy budżet. Zarejestruj detektor przed ustawieniem budżetu, aby aplikacja nie przegapiła początkowego powiadomienia o przekroczeniu budżetu. - Szybkość: operacje odzyskiwania muszą przynosić natychmiastową ulgę. Złożone obliczenia podczas obciążenia pogarszają wydajność.
- Unikaj alokacji: nie przydzielaj nowych obiektów ani nie uruchamiaj nowych wątków w wywołaniu zwrotnym, ponieważ może to spowodować natychmiastowe bezpośrednie odzyskanie pamięci przez system operacyjny.
- Skup się na celach o wysokiej wydajności: usuwanie dużych bitmap, buforów renderowania lub zamykanie plików mapowanych w pamięci jest znacznie skuteczniejsze niż zwalnianie wielu małych obiektów.
Natywny interfejs API NDK (<android/memory_budget_manager.h>)
Aplikacje natywne mogą używać interfejsu API C NDK udostępnianego przez libandroid.so od Androida 17 QPR2 (poziom API 37.2).
Konfiguracja CMake
find_library(android-lib android)
target_link_libraries(my_native_engine PRIVATE ${android-lib})
Uwzględnij użycie nagłówka i zapytania
#include <android/memory_budget_manager.h>
// Query current memory usage
int64_t process_usage = AMemoryBudgetManager_getProcessCurrentUsageBytes();
int64_t package_usage = AMemoryBudgetManager_getPackageCurrentUsageBytes();
// Query current budget
int64_t process_budget = 0;
AMemoryBudgetResult result = AMemoryBudgetManager_getProcessBudget(&process_budget);
if (result == AMEMORY_BUDGET_RESULT_SUCCESS) {
// Current budget available in process_budget
} else if (result == AMEMORY_BUDGET_RESULT_LIMIT_IS_DISABLED) {
// No budget is currently active
}
Dynamiczne konfigurowanie budżetu natywnego
// Set a tighter process budget (e.g. 160MB)
AMemoryBudgetResult result = AMemoryBudgetManager_setProcessBudget(160LL * 1024 * 1024);
if (result != AMEMORY_BUDGET_RESULT_SUCCESS) {
const char* error_msg = AMemoryBudgetManager_resultToString(result);
// Handle error (e.g. AMEMORY_BUDGET_RESULT_ERROR_EXCEEDS_MANIFEST_LIMIT)
}
// Clear the dynamic budget to resume manifest limits (or unconstrained baseline)
AMemoryBudgetManager_clearProcessBudget();
Monitorowanie zdarzeń związanych z obciążeniem pamięci
NDK udostępnia 2 sposoby monitorowania zdarzeń związanych z pamięcią:
- High-Level Watcher (
AMemoryBudgetManager_Watcher_create): monitoruje zdarzenia naALooperz automatycznym usuwaniem drgań styków. - Deskryptor pliku niskiego poziomu:
AMemoryBudgetManager_getProcessMemoryPressureFdzwraca deskryptor pliku natywnego, który można zintegrować bezpośrednio z niestandardową pętlą silnikaepoll.
void onMemoryPressure(int32_t event_mask, const AMemoryBudgetEvents* events, void* userdata) {
// High-yield eviction of unused native textures or geometry caches
purgeNativeTextureCaches();
}
// Register watcher on a background worker ALooper with a 1000ms debounce interval
AMemoryBudgetManagerWatcher* watcher = AMemoryBudgetManager_Watcher_create(
background_looper,
AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
1000 /* debounce_ms */,
&onMemoryPressure,
NULL /* userdata */
);
// When done:
AMemoryBudgetManager_Watcher_destroy(watcher);
Powiąż obserwatora z ALooper wątku instancji roboczej w tle, a nie z głównym wątkiem interfejsu użytkownika lub wątkiem renderowania, aby usuwanie pamięci podręcznej nie kolidowało z renderowaniem klatek. Utwórz obserwatora, zanim ograniczysz budżet za pomocą AMemoryBudgetManager_setProcessBudget lub AMemoryBudgetManager_setPackageBudget, aby aplikacja nie przegapiła początkowego zdarzenia przekroczenia budżetu.
Dopóki proces lub pakiet przekracza budżet, a system operacyjny nadal odzyskuje pamięć, obserwator wielokrotnie wywołuje wywołanie zwrotne (co najwyżej raz na interwał debounce_ms). Ustaw wywołanie zwrotne jako idempotentne lub wywołaj AMemoryBudgetManager_Watcher_destroy (co jest bezpieczne w przypadku wywołania zwrotnego), jeśli aplikacja potrzebuje tylko jednorazowego powiadomienia.
Przykłady interfejsu Runtime API
Poniższe przykłady pokazują, jak zaimplementować interfejsy API środowiska wykonawczego za pomocą interfejsu API pakietu Android SDK (napisanego w języku Kotlin) i interfejsu API Native NDK (napisanego w języku C++).
Przykład pakietu Android SDK: adaptacyjny edytor obrazów
Ten przykład pokazuje aplikację do edycji obrazów (com.example.imageeditor), której plik manifestu deklaruje limit 256 MB, aby pomieścić wielowarstwowy obszar roboczy:
<manifest ... >
<application ... >
<!-- Manifest ceiling accommodates the heaviest editing workload -->
<memory-budget android:maxMb="256" />
</application>
</manifest>
Gdy użytkownik przegląda lekką galerię miniatur, aplikacja używa interfejsu API Android SDK w języku Kotlin, aby dynamicznie zmniejszyć budżet procesu do 96 MB.
Gdy użytkownik otwiera obszar roboczy edycji wielowarstwowej, aplikacja czyści budżet dynamiczny, aby przywrócić pełny limit 256 MB w pliku manifestu. Rejestruje też OnOverBudgetListener w tle HandlerThread
Looper przed zastosowaniem budżetu Galerii, aby usunąć buforowane bitmapy podglądu pod obciążeniem bez blokowania wątku UI.
package com.example.imageeditor.ui
import android.app.Activity
import android.app.MemoryBudgetManager
import android.graphics.Bitmap
import android.os.Bundle
import android.os.HandlerThread
import android.util.Log
import android.util.LruCache
class ImageEditorActivity : Activity() {
private lateinit var budgetManager: MemoryBudgetManager
private lateinit var cleanupThread: HandlerThread
// Thread-safe in-memory cache for rendered preview tiles (32MB limit)
private val previewCache = object : LruCache<String, Bitmap>(32 * 1024 * 1024) {
override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount
}
private val overBudgetListener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
Log.w(TAG, "Process memory pressure detected (budget: ${budgetBytes / 1048576}MB). Evicting preview cache.")
previewCache.evictAll()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
budgetManager = getSystemService(MemoryBudgetManager::class.java)
cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }
}
override fun onStart() {
super.onStart()
// Register listener on a background Looper before tightening the budget
budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, overBudgetListener)
// Constrain memory during lightweight gallery browsing
applyGalleryBudget()
}
override fun onStop() {
super.onStop()
budgetManager.unregisterProcessOverBudgetListener(overBudgetListener)
}
override fun onDestroy() {
cleanupThread.quitSafely()
super.onDestroy()
}
/**
* Called when the user enters the high-resolution editing canvas.
* Clears the dynamic budget, restoring the full 256MB manifest ceiling.
*/
fun enterEditingCanvas() {
// Clear the tighter dynamic budget to restore the full manifest ceiling (256MB)
budgetManager.clearProcessBudget()
Log.i(TAG, "Restored manifest budget ceiling (256MB) for editing canvas")
}
/**
* Called when the user exits the editor back to the thumbnail gallery.
* Re-applies the tighter dynamic budget.
*/
fun exitToGallery() {
previewCache.trimToSize(8 * 1024 * 1024)
applyGalleryBudget()
}
private fun applyGalleryBudget() {
try {
// Dynamically tighten budget to 96MB for the lightweight gallery view
budgetManager.processBudgetBytes = 96L * 1024L * 1024L
Log.i(TAG, "Tighter dynamic budget applied for gallery: 96MB")
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Could not apply dynamic budget", e)
}
}
companion object {
private const val TAG = "ImageEditor"
}
}
Przykład NDK C++: natywny silnik 3D
Ten przykład pokazuje natywny silnik gry w C++ zarządzający budżetami pamięci na podstawie aktywnego poziomu jakości grafiki, przy założeniu, że manifest aplikacji deklaruje limit 512 MB, aby pomieścić grafikę wysokiej jakości (android:maxMb="512"). Silnik uruchamia AMemoryBudgetManager_Watcher_create w ALooper wątku instancji roboczej w tle, zanim ograniczy budżet dla ustawień wstępnych niższej jakości, i wyładowuje mapy mip tekstur, gdy budżet zostanie przekroczony.
#include <android/memory_budget_manager.h>
#include <android/looper.h>
#include <android/log.h>
#define LOG_TAG "Native3DEngineMemory"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
class MemoryGovernor {
public:
MemoryGovernor() : mWatcher(nullptr) {}
~MemoryGovernor() {
stopMonitoring();
}
// Configures process budget based on user graphics quality settings
bool setQualityBudget(int qualityLevel) {
int64_t targetBytes = 0;
switch (qualityLevel) {
case 0: // Low (budget: 128MB)
targetBytes = 128LL * 1024 * 1024;
break;
case 1: // Medium (budget: 256MB)
targetBytes = 256LL * 1024 * 1024;
break;
case 2: // High (budget: 512MB)
targetBytes = 512LL * 1024 * 1024;
break;
default:
// Clear dynamic override and restore manifest limit
AMemoryBudgetManager_clearProcessBudget();
return true;
}
AMemoryBudgetResult result = AMemoryBudgetManager_setProcessBudget(targetBytes);
if (result != AMEMORY_BUDGET_RESULT_SUCCESS) {
LOGW("Could not set quality budget: %s", AMemoryBudgetManager_resultToString(result));
return false;
}
return true;
}
// Call on a background worker ALooper before tightening the budget with setQualityBudget
bool startMonitoring(ALooper* backgroundLooper) {
if (!backgroundLooper) return false;
// Monitor process budget events, debounced to at most once every 1000ms
mWatcher = AMemoryBudgetManager_Watcher_create(
backgroundLooper,
AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
1000,
&MemoryGovernor::onPressureEvent,
this
);
return mWatcher != nullptr;
}
void stopMonitoring() {
if (mWatcher) {
AMemoryBudgetManager_Watcher_destroy(mWatcher);
mWatcher = nullptr;
}
}
void unloadUnusedTextures() {
LOGW("Memory pressure callback triggered. Purging cached texture mipmaps...");
// Fast, idempotent, high-yield eviction without allocating memory
}
private:
static void onPressureEvent(
int32_t event_mask,
const AMemoryBudgetEvents* events,
void* userdata
) {
auto* governor = static_cast<MemoryGovernor*>(userdata);
governor->unloadUnusedTextures();
}
AMemoryBudgetManagerWatcher* mWatcher;
};