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 o konieczności ograniczenia zużycia pamięci, gdy aplikacja wykorzystuje więcej niż określony budżet. Jest to szczególnie przydatne w przypadku aplikacji systemowych i dołączonych lub aplikacji przeznaczonych na urządzenia o ograniczonej ilości pamięci, w których deweloper zna oczekiwany zestaw roboczy pamięci i chce mieć pewność, że aplikacja nie będzie wykorzystywać zbyt dużej ilości wspólnych zasobów pamięci RAM systemu.

Budżet jest utrzymywany w równowadze dzięki usuwaniu z pamięci i zamianie stron pamięci, które nie były ostatnio używane. Dzięki temu aplikacja może skupić się na bieżącym zestawie roboczym. Gdy aplikacja przekroczy zadeklarowany budżet, system operacyjny odzyska środki przeznaczone na kierowanie na tę aplikację:

  1. Strony oparte na plikach (takie jak nieaktywny kod i mapowane zasoby) są usuwane w pierwszej kolejności, ponieważ w razie potrzeby można je ponownie odczytać z pamięci.
  2. Zmodyfikowane strony oparte na plikach są zapisywane z powrotem w pamięci i usuwane.
  3. Anonimowe strony pamięci (np. przydzielanie sterty) są kompresowane i przenoszone do zRAM.

Dopóki budżet nie przekracza zestawu roboczego, aplikacja będzie działać prawidłowo, nie zużywając więcej pamięci niż określono w budżecie. System operacyjny zwalnia nieużywaną pamięć i kompresuje nieaktywne strony sterty do pliku wymiany, aby przydziały pamięci pozostawały ograniczone bez przerywania procesu.

Deklarowanie budżetów w pliku manifestu Androida

Deklarowanie budżetów pamięci w AndroidManifest.xml to podstawowa i zalecana metoda definiowania budżetów. Nie wymaga kodu w czasie działania, zaczyna działać natychmiast po uruchomieniu procesu i zapewnia jasną umowę z systemem operacyjnym.

Deklarowanie budżetu podstawowego

W przypadku większości aplikacji wystarczy określić 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 w wysokości 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 eksmisji i zamiany.

Zmieniaj budżety w zależności od stanu procesu

Aplikacja wymaga różnej ilości pamięci w zależności od widoczności dla użytkownika:

  • Pierwszy plan: proces hostuje widoczną aktywność, która wchodzi w interakcję z użytkownikiem. Ten stan zwykle zajmuje najwięcej miejsca ze względu na aktywny interfejs i grafikę.
  • Zauważalny: proces jest zauważalny dla użytkownika, ale nie ma widocznego okna (np. hostuje usługę na pierwszym planie odtwarzającą multimedia, nawigację krok po kroku lub aktywną metodę wprowadzania).
  • Tło: proces uruchamia zadania w tle, odbiorniki lub synchronizacje danych. Powinien on zajmować jak najmniej 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>

W pierwszej klauzuli nie musisz podawać wartości android:state="foreground". Klauzula bez symbolu android:state działa jako domyślna opcja rezerwowa we wszystkich stanach. Bardziej restrykcyjne klauzule poniżej zastępują budżet, gdy aplikacja przechodzi w stan perceptible lub background.

Aplikacje wieloprocesowe

Jeśli aplikacja dzieli pracę na wiele procesów, skonfiguruj dedykowane budżety procesów za pomocą tagu <process> w tagu <processes>.

Rozważmy na przykład aplikację do strumieniowego odtwarzania muzyki (com.example.radio):

  1. Główny proces: hostuje widoczny interfejs i silnik odtwarzania dźwięku (MediaSessionServicemediaPlayback usługą na pierwszym planie). Gdy jest widoczny, proces działa w ramach budżetu 180 MB na pierwszym planie. Gdy użytkownik opuści aplikację, a odtwarzanie muzyki będzie kontynuowane, proces przejdzie w stan perceptible, w którym silnikowi odtwarzania i buforowi audio wystarczy budżet 64 MB.
  2. Proces synchronizacji (:sync): dedykowany proces działający w tle, który synchronizuje metadane i indeksuje pobrane pliki. Ten proces jest zawsze aktywny w tle, więc nie musisz go 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" />

        <!-- Dedicated background sync process -->
        <processes>
            <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>

Wykorzystanie pamięci w dowolnym podprocesie jest wliczane zarówno do budżetu procesu, jak i do budżetu pakietu, w którym ten proces jest zawarty. Proces odczuwa presję 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 zużycie pamięci zależy w znacznym stopniu od liczby pikseli wyświetlanych jednocześnie na ekranie (np. aplikacji galerii zdjęć, która buforuje mapy bitowe o rozmiarze ekranu), Android udostępnia 2 alternatywne mechanizmy dynamicznego skalowania budżetów w zależności od specyfikacji wyświetlacza:

  • Skaluj według zakresu gęstości wyświetlacza (android:additionalMbPerDensity): dodaje megabajty proporcjonalnie do współczynnika gęstości wyświetlacza w stosunku do mdpi (1,0x / 160 dpi). Jest to odpowiednie, gdy zużycie pamięci skaluje się wraz z gęstością interfejsu, np. w przypadku buforowania rysunków rastrowych lub zasobów interfejsu o wyższej rozdzielczości:

    <!-- Baseline 180MB + 16MB per 1.0x density ratio -->
    <memory-budget
        android:maxMb="180"
        android:additionalMbPerDensity="16" />
    

    Na wyświetlaczu mdpi (1,0x) budżet wynosi 180 + 16 × 1 = 196 MB. Na wyświetlaczu xxhdpi (3,0x) budżet zwiększa się do 180 + 16 × 3 = 228 MB.

  • Skaluj według rozdzielczości fizycznej wyświetlacza (android:additionalBytesPerDisplayPixel): dodaje bajty bezpośrednio na piksel fizyczny wyświetlacza (szerokość × wysokość). Jest to idealne rozwiązanie w przypadku aplikacji, które przydzielają powierzchnie graficzne na pełnym ekranie, bufory renderowania lub pamięci podręczne zdjęć w pełnej rozdzielczości, w których zużycie pamięci jest bezpośrednio proporcjonalne do liczby pikseli na wyświetlaczu, a nie do zakresów 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 pikseli, czyli ok.2,59 mln pikseli) zwiększa to budżet bazowy o ok.41,4 MB. Na wyświetlaczu 1440p (1440 × 3120 pikseli, ok.4,49 M pikseli) dodaje ok.71,8 MB.

Te 2 atrybuty są alternatywne. Wybierz atrybut, który pasuje do głównego współczynnika skalowania aplikacji, i unikaj łączenia obu w tym samym warunku.

Dostosowywanie do formatów urządzeń

Podczas wysyłania pliku APK na telefony, tablety i urządzenia z Wear OS użyj atrybutu android:feature, aby dostosować budżety do różnych docelowych urządzeń.

Na zegarkach z Wear OS pamięć RAM jest ograniczona, a interfejs i funkcje aplikacji są znacznie prostsze. Możesz zadeklarować mniejszy budżet przeznaczony specjalnie na watch tę funkcję:

<!-- 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" />

Zasada rozstrzygania: obowiązuje ostatnia odpowiednia klauzula

Podczas definiowania wielu elementów <memory-budget> dla aplikacji lub procesu system ocenia je w kolejności, w jakiej są zadeklarowane w pliku manifestu. Obowiązuje ostatnia odpowiednia klauzula budżetowa.

Ponieważ wygrywa ostatni zastosowany budżet, kolejność ma znaczenie. Zawsze umieszczaj najpierw najbardziej ogólny budżet podstawowy, a potem bardziej szczegółowe zastąpienia (np. klauzule dotyczące konkretnego stanu lub sprzętu).

Odniesienie do atrybutu XML

Wszystkie atrybuty rozmiaru pamięci są wyrażone w megabajtach (MB) i odpowiadają parametrowi memory.current charge w grupach kontrolnych systemu Linux (który 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, do którego ma zastosowanie ten budżet: foreground, perceptible lub background.
android:additionalMbPerDensity Liczba całkowita (≥ 0) 0 Dodatkowe megabajty do dodania na jednostkę współczynnika gęstości wyświetlania w stosunku do 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ń deklarujących określone funkcje sprzętowe: watch, automotive lub leanback.

Interfejsy API środowiska wykonawczego (dodatkowa opcja dynamiczna)

Statyczne deklarowanie budżetów w AndroidManifest.xml jest preferowanym rozwiązaniem w przypadku niemal wszystkich aplikacji. W przypadku aplikacji z dynamicznymi zadaniami lub eksperymentów w czasie działania Android udostępnia interfejsy API pakietu SDK i NDK w czasie działania jako opcję dodatkową.

Interfejs API środowiska wykonawczego umożliwia:

  • Sprawdzanie bieżącego wykorzystania pamięci i efektywnych budżetów.
  • Dynamicznie zmniejszaj budżet procesu.
  • Nasłuchuj zdarzeń przekroczenia budżetu, aby proaktywnie przycinać pamięć podręczną, zanim system operacyjny wywoła bezpośrednie odzyskiwanie.

Interfejs Kotlin API (MemoryBudgetManager)

Usługa systemowa MemoryBudgetManager jest dostępna od Androida 17 QPR2 (wersja pakietu SDK Androida 26Q4, poziom API 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2).

Pobieranie usługi

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

Dynamiczne ustawianie i usuwanie budżetów

Możesz ustawić mniejszy budżet w czasie działania, aby ograniczyć pamięć podczas 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-declared ceiling
    Log.e(TAG, "Requested budget exceeds manifest or system ceiling", e)
}

// Clear the dynamic process budget to restore the manifest limit
budgetManager.clearProcessBudget()

Słuchaj wywołań zwrotnych dotyczących przekroczenia budżetu

Aplikacje mogą zarejestrować detektor, aby otrzymywać powiadomienia, gdy wykorzystanie pamięci przekroczy próg budżetu. Dzięki temu aplikacja może aktywnie czyścić pamięć na poziomie aplikacji (np. usuwać pamięci podręczne bitmap) zanim system operacyjny wywoła bezpośrednie odzyskiwanie pamięci:

val listener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
    Log.w(TAG, "Process exceeded memory budget of $budgetBytes bytes")
    // Proactively evict caches to release memory
    imageTileCache.evictAll()
}

// Register on the main Looper
budgetManager.registerProcessOverBudgetListener(mainLooper, listener)

// When done (e.g., in onStop)
budgetManager.unregisterProcessOverBudgetListener(listener)

Sprawdzone metody dotyczące oddzwaniania po przekroczeniu budżetu:

  • Szybkie działanie: operacje odzyskiwania muszą przynosić natychmiastową ulgę. Złożone obliczenia podczas obciążenia pogarszają wydajność.
  • Unikaj przydzielania pamięci: nie przydzielaj nowych obiektów ani nie uruchamiaj nowych wątków w funkcji zwrotnej, ponieważ może to spowodować natychmiastowe odzyskanie pamięci przez system operacyjny.
  • Skup się na celach o wysokiej wydajności: usuwanie dużych map bitowych, buforów renderowania lub zamykanie plików mapowanych w pamięci jest znacznie skuteczniejsze niż zwalnianie wielu małych obiektów.

Interfejs Native NDK API (<android/memory_budget_manager.h>)

Aplikacje natywne mogą korzystać z interfejsu C NDK API udostępnianego przez libandroid.so.

Konfiguracja CMake

find_library(android-lib android)
target_link_libraries(my_native_engine PRIVATE ${android-lib})

Uwzględnij nagłówek i użycie 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 na reklamy natywne

// 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
AMemoryBudgetManager_clearProcessBudget();

Monitorowanie zdarzeń związanych z niskim poziomem pamięci

NDK udostępnia 2 sposoby monitorowania zdarzeń związanych z pamięcią:

  1. Obserwator wysokiego poziomu (AMemoryBudgetManager_Watcher_create): monitoruje zdarzenia na ALooper z automatycznym odrzucaniem.
  2. Deskryptor pliku niskiego poziomu:AMemoryBudgetManager_getProcessMemoryPressureFd zwraca natywny deskryptor pliku, który można zintegrować bezpośrednio z pętlą niestandardowego epoll silnika.
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 an ALooper with a 1000ms debounce interval
AMemoryBudgetManagerWatcher* watcher = AMemoryBudgetManager_Watcher_create(
    looper,
    AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
    1000 /* debounce_ms */,
    &onMemoryPressure,
    NULL /* userdata */
);

// When done:
AMemoryBudgetManager_Watcher_destroy(watcher);

Przykłady interfejsu Runtime API

Poniższe przykłady pokazują, jak zaimplementować interfejsy API środowiska wykonawczego w językach Kotlin i C++.

Przykład w języku Kotlin: adaptacyjny edytor obrazów

Ten przykład pokazuje aplikację do edycji obrazów (com.example.imageeditor), która dynamicznie zwiększa budżet pamięci, gdy użytkownik otwiera wielowarstwową przestrzeń do edycji, i czyści budżet dynamiczny, gdy wraca do widoku galerii miniatur. Rejestruje też OnOverBudgetListener, aby w razie potrzeby usunąć z pamięci podręcznej mapy bitowe podglądu.

package com.example.imageeditor.ui

import android.app.Activity
import android.app.MemoryBudgetManager
import android.graphics.Bitmap
import android.os.Bundle
import android.util.Log
import android.util.LruCache

class ImageEditorActivity : Activity() {

    private lateinit var budgetManager: MemoryBudgetManager

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

    override fun onStart() {
        super.onStart()
        // Register listener for process-level memory breaches
        budgetManager.registerProcessOverBudgetListener(mainLooper, overBudgetListener)
    }

    override fun onStop() {
        super.onStop()
        budgetManager.unregisterProcessOverBudgetListener(overBudgetListener)
    }

    /**
     * Called when the user enters the high-resolution editing canvas.
     */
    fun enterEditingCanvas() {
        try {
            // Dynamically set budget to 256MB for the editing canvas
            budgetManager.processBudgetBytes = 256L * 1024L * 1024L
            Log.i(TAG, "Dynamic budget applied: 256MB")
        } catch (e: IllegalArgumentException) {
            Log.e(TAG, "Could not apply dynamic budget", e)
        }
    }

    /**
     * Called when the user exits the editor back to the thumbnail gallery.
     */
    fun exitToGallery() {
        previewCache.trimToSize(8 * 1024 * 1024)
        // Clear dynamic budget; restores the baseline manifest budget
        budgetManager.clearProcessBudget()
    }

    companion object {
        private const val TAG = "ImageEditor"
    }
}

Przykład NDK C++: natywny silnik 3D

Ten przykład pokazuje natywny silnik gry w C++, który zarządza budżetami pamięci na podstawie aktywnego poziomu jakości grafiki, używając AMemoryBudgetManager_Watcher_create na urządzeniu ALooper do zwalniania mapowania mipmap 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;
    }

    bool startMonitoring(ALooper* looper) {
        if (!looper) return false;

        // Monitor process budget events, debounced to at most once every 1000ms
        mWatcher = AMemoryBudgetManager_Watcher_create(
            looper,
            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, 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;
};