Ustawianie budżetów pamięci aplikacji

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:

  1. 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.
  2. Zmodyfikowane strony powiązane z plikami są zapisywane z powrotem w pamięci i usuwane.
  3. 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:

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 foreground na 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 background na 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 jak BIND_NOT_FOREGROUND (patrz Kontrolowanie dziedziczenia za pomocą flag BIND). Chociaż wewnętrzne dane platformy mogą oznaczać niektóre z tych zadań jako PROCESS_STATE_IMPORTANT_FOREGROUND, ta wewnętrzna stała nie oznacza widocznego okna i jest regulowana przez budżet perceptible.
  • 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_FOREGROUND
IMPORTANCE_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_SERVICE
IMPORTANCE_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_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
IMPORTANCE_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=4 i state: cur=FGS wskazują, że proces jest w stanie usługi na pierwszym planie. Jeśli aplikacja hostuje aktywną, widoczną aktywność, jest to oznaczone jako TOP. Jeśli działa w niej ważny komponent na pierwszym planie (np. pobieranie lub synchronizacja), jest wyświetlana jako IMPF.
  • W tabeli kontroli braku pamięci procesu prcp oznacza, że proces jest oceniany na poziomie priorytetu „zauważalny”, co odpowiada budżetowi perceptible.

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

  1. Proces główny: hostuje widoczny interfejs i silnik odtwarzania dźwięku (MediaSessionService z usługą mediaPlayback na 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 stan perceptible, w którym budżet 64 MB wystarczy na silnik odtwarzania i bufor audio.
  2. 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ędem mdpi (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świetlaczu xxhdpi (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 &approx; 2,59 mln pikseli) dodaje to &approx; 41,4 MB do budżetu podstawowego. Na wyświetlaczu 1440p (1440 × 3120 &approx; 4,49 mln pikseli) dodaje &approx; 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() lub clearPackageBudget() albo wywoływanie ich, gdy nie jest aktywny żaden budżet dynamiczny, jest bezpieczną operacją, która nie powoduje żadnych skutków. Podobnie ustawienie funkcji setProcessBudgetBytes() lub setPackageBudgetBytes() 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 processCurrentUsageBytes i packageCurrentUsageBytes pobierają 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 zwrotnych OnOverBudgetListener), 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ło HandlerThread Looper do registerProcessOverBudgetListener lub registerPackageOverBudgetListener. 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. Zarezerwuj Looper.getMainLooper dla 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 (lub unregisterPackageOverBudgetListener) w wywołaniu zwrotnym albo zabezpiecz czyszczenie za pomocą sygnatury czasowej czasu oczekiwania lub AtomicBoolean.
  • Zarejestruj się przed zmniejszeniem budżetu: jeśli bieżące wykorzystanie pamięci przekracza już nowy budżet, gdy wywołujesz funkcję setProcessBudgetBytes lub setPackageBudgetBytes, 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ą:

  1. High-Level Watcher (AMemoryBudgetManager_Watcher_create): monitoruje zdarzenia na ALooper z automatycznym usuwaniem drgań styków.
  2. Deskryptor pliku niskiego poziomu: AMemoryBudgetManager_getProcessMemoryPressureFd zwraca deskryptor pliku natywnego, który można zintegrować bezpośrednio z niestandardową pętlą silnika epoll.
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;
};