Установите лимиты памяти для приложений.

Бюджеты памяти приложений позволяют приложениям самостоятельно устанавливать для себя лимит использования памяти, который указывает системе на необходимость сокращения использования памяти, если приложение использует больше установленного лимита. Это особенно полезно для системных и встроенных приложений, а также для приложений, ориентированных на устройства с ограниченными ресурсами памяти, где разработчик знает ожидаемый объем используемой памяти и хочет убедиться, что его приложение не использует слишком много ресурсов общей оперативной памяти системы.

Бюджет памяти поддерживается в сбалансированном состоянии за счет использования вытеснения и подкачки для удаления страниц памяти, которые недавно не использовались, концентрируя использование памяти приложением на текущем рабочем наборе. Когда приложение превышает заявленный бюджет, операционная система направляет высвобождение памяти именно на это приложение:

  1. Чистые страницы, хранящиеся в файлах (например, неактивный код и сопоставленные ресурсы), удаляются в первую очередь, поскольку при необходимости их можно повторно прочитать из хранилища.
  2. Загрязненные страницы, хранящиеся в архиве, записываются обратно в хранилище и удаляются.
  3. Анонимные страницы памяти (например, выделенные в куче) сжимаются и загружаются в zRAM.

Пока бюджет не превышает установленный рабочий набор, приложение будет хорошо работать, используя не больше памяти, чем предусмотрено его бюджетом. Операционная система вытесняет неиспользуемую память и сжимает неактивные страницы кучи в файл подкачки, чтобы выделение памяти оставалось ограниченным и процесс не завершался.

Укажите бюджеты в манифесте Android.

Объявление лимитов памяти в файле AndroidManifest.xml — это основной и рекомендуемый способ определения лимитов. Он не требует кода во время выполнения, вступает в силу немедленно после запуска процесса и обеспечивает четкий контракт для операционной системы.

Объявить базовый бюджет

Для большинства приложений достаточно определить единый бюджет памяти. Объявите элемент <memory-budget> непосредственно внутри тега <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>

Это устанавливает лимит оперативной памяти в 256 МБ для всех процессов и состояний пакета. Когда объем памяти, занимаемый приложением, превышает 256 МБ, операционная система удаляет неактивные страницы памяти с помощью вытеснения и подкачки.

Изменение бюджетов в зависимости от состояния процесса.

Приложению требуется разный объем памяти в зависимости от того, насколько оно доступно для пользователя:

  • В фоновом режиме : процесс содержит видимое Activity, взаимодействующее с пользователем. Как правило, этот режим занимает больше всего места из-за активного пользовательского интерфейса и графики.
  • Ощутимый : Процесс ощутим для пользователя, но не содержит видимого окна (например, служба воспроизведения мультимедиа, пошаговая навигация или активный метод ввода).
  • Предыстория : Процесс выполняет фоновые задачи, обрабатывает данные с устройств приема или синхронизирует данные. Предполагается, что он будет оказывать минимальное воздействие на систему.

Для соответствия этим состояниям можно объявить несколько условий <memory-budget> :

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

В первом условии нет необходимости указывать android:state="foreground" . Условие без android:state действует как резервный вариант по умолчанию для всех состояний. Более строгие условия ниже переопределяют бюджет при переходе приложения в perceptible или background состояние.

Многопроцессные приложения

Если ваше приложение распределяет работу между несколькими процессами, настройте выделенные бюджеты для процессов, используя тег <process> внутри <processes> .

Например, рассмотрим приложение для потоковой передачи музыки ( com.example.radio ):

  1. Основной процесс : содержит видимый пользовательский интерфейс и механизм воспроизведения звука ( MediaSessionService с фоновой службой mediaPlayback ). В видимом состоянии процесс работает в рамках выделенного для него бюджета в 180 МБ. Когда пользователь покидает приложение, а музыка продолжает воспроизводиться, процесс переходит в perceptible состояние, где для механизма воспроизведения и аудиобуфера достаточно 64 МБ выделенного пространства.
  2. Процесс синхронизации ( :sync ) : Специальный процесс, выполняющий фоновую синхронизацию метаданных и индексирование загрузок. Поскольку этот процесс всегда активен только в фоновом режиме, вам не нужно явно указывать state="background" ; применяется единый бюджет.
<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>

Использование памяти в любом дочернем процессе учитывается как в рамках его собственного бюджета процесса, так и в рамках бюджета окружающего пакета. Процесс испытывает нехватку памяти во время выполнения, если он превышает либо свой собственный бюджет процесса, либо бюджет пакета, в зависимости от того, какой порог будет достигнут первым.

Масштабные бюджеты для дисплеев высокой плотности

Для приложений, объем используемой памяти которых значительно зависит от количества пикселей, которые необходимо отобразить на экране одновременно (например, для фотогалереи, кэширующей растровые изображения, размер которых соответствует размеру экрана), Android предоставляет два альтернативных механизма для динамического масштабирования бюджета памяти в соответствии со спецификациями дисплея:

  • Масштабирование по плотности экрана ( android:additionalMbPerDensity ) : добавляет мегабайты пропорционально коэффициенту плотности экрана относительно mdpi (1,0x / 160 dpi). Это подходит, когда использование памяти масштабируется в зависимости от плотности пользовательского интерфейса, например, при кэшировании растровых изображений или ресурсов пользовательского интерфейса с более высоким разрешением:

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

    На дисплее mdpi (1,0x) бюджет составляет 180 + 16 × 1 = 196 МБ. На дисплее xxhdpi (3,0x) бюджет увеличивается до 180 + 16 × 3 = 228 МБ.

  • Масштабирование по физическому разрешению дисплея ( android:additionalBytesPerDisplayPixel ): добавляет байты непосредственно на каждый физический пиксель дисплея (ширина × высота). Это идеально подходит для приложений, выделяющих полноэкранные графические поверхности, буферы рендеринга или кэши фотографий с полным разрешением, где потребление памяти масштабируется непосредственно в зависимости от количества пикселей дисплея, а не от плотности элементов пользовательского интерфейса:

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

    На дисплее с разрешением 1080p (1080 × 2400 ≈ 2,59 млн пикселей) это добавляет примерно 41,4 МБ к базовому бюджету. На дисплее с разрешением 1440p (1440 × 3120 ≈ 4,49 млн пикселей) это добавляет примерно 71,8 МБ.

Эти два атрибута являются альтернативными. Выберите атрибут, соответствующий основному коэффициенту масштабирования вашего приложения, и избегайте объединения обоих в одном предложении.

Специализация на форм-факторах устройств

При распространении APK-файла на телефоны, планшеты и Wear OS используйте атрибут android:feature для корректировки параметров под различные аппаратные платформы.

На часах Wear OS объем оперативной памяти ограничен, а пользовательский интерфейс и набор функций приложения значительно проще. Можно выделить более ограниченный бюджет, специально предназначенный для функционала 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" />

Правило разрешения: вступает в силу последний применимый пункт.

При определении нескольких элементов <memory-budget> для приложения или процесса система оценивает их в порядке, в котором они объявлены в манифесте. Применяется последний применимый пункт бюджета.

Поскольку приоритет имеет последний применимый бюджет, порядок имеет значение. Всегда сначала указывайте наиболее общий базовый бюджет, а затем более конкретные исключения (например, положения, специфичные для штата или оборудования).

Справочник атрибутов XML

Все атрибуты размера памяти выражаются в мегабайтах (МБ) и соответствуют параметру memory.current в группе Linux (который исключает разделяемую память, такую ​​как Zygote).

Атрибут Формат По умолчанию Описание
android:maxMb Целое число (> 0) Необходимый Базовый лимит бюджета резидентной памяти в МБ.
android:state Перечисление Любой Данный бюджет относится к следующим категориям элементов процесса: foreground , perceptible или background .
android:additionalMbPerDensity Целое число (≥ ​​0) 0 Дополнительные мегабайты, добавляемые на единицу коэффициента плотности дисплея относительно mdpi (1,0x).
android:additionalBytesPerDisplayPixel Целое число (≥ ​​0) 0 Дополнительные байты, выделяемые на каждый физический пиксель дисплея (ширина × высота), полезны для буферов поверхностей и растровых изображений.
android:feature Нить Любой Ограничивает действие данного пункта устройствами, которые заявляют о наличии определенных аппаратных характеристик: watch , automotive или leanback .

API среды выполнения (дополнительная динамическая опция)

Статическое определение бюджетов в файле AndroidManifest.xml является предпочтительным решением практически для всех приложений. Однако для приложений с динамическими нагрузками или для экспериментов во время выполнения Android предоставляет API SDK и NDK в качестве дополнительной опции.

API среды выполнения позволяет вам:

  • Запрос текущего использования памяти и эффективного бюджета памяти.
  • Динамически корректируйте бюджет процесса в сторону уменьшения.
  • Отслеживайте события, связанные с превышением бюджета, чтобы заблаговременно очищать кэш до того, как операционная система запустит прямое высвобождение ресурсов.

API Kotlin ( MemoryBudgetManager )

Системная служба MemoryBudgetManager доступна начиная с Android 17 QPR2 (версия Android 26Q4 SDK, уровень API 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2 ).

Восстановить услугу

val budgetManager = context.getSystemService(MemoryBudgetManager::class.java)

Использование запросов и бюджеты

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

Динамическая установка или сброс бюджетов

Вы можете установить более жесткий бюджет во время выполнения, чтобы ограничить использование памяти при выполнении легковесных задач, или очистить ее после завершения задачи:

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

Прислушивайтесь к звонкам, оповещающим о превышении бюджета.

Приложения могут зарегистрировать обработчик событий, чтобы получать уведомления, когда использование памяти превышает установленный лимит. Это позволяет приложению выполнять упреждающую очистку на уровне приложения (например, очистку кэша битовых карт в памяти) до того, как операционная система запустит механизм прямого высвобождения памяти.

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)

Рекомендации по проведению обратных звонков при превышении бюджета:

  • Действуйте быстро : операции по рекультивации должны обеспечивать немедленное снижение давления. Сложные вычисления в условиях повышенного давления ухудшают производительность.
  • Избегайте выделения памяти : не выделяйте новые объекты и не запускайте новые потоки внутри функции обратного вызова, так как это может привести к немедленному прямому высвобождению ресурсов операционной системой.
  • Сосредоточьтесь на высокоэффективных задачах : удаление больших растровых изображений, буферов рендеринга или закрытие файлов, отображаемых в память, гораздо эффективнее, чем освобождение множества мелких объектов.

Нативный API NDK ( <android/memory_budget_manager.h> )

Нативные приложения могут использовать API C NDK, предоставляемый библиотекой libandroid.so .

Конфигурация CMake

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

Укажите использование заголовков и запросов.

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

Динамическая настройка собственного бюджета

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

Отслеживайте события, связанные с нехваткой памяти.

NDK предоставляет два способа отслеживания событий, связанных с памятью:

  1. Наблюдатель высокого уровня ( AMemoryBudgetManager_Watcher_create ) : отслеживает события в ALooper с автоматическим подавлением дребезга контактов.
  2. Дескриптор файла низкого уровня : AMemoryBudgetManager_getProcessMemoryPressureFd возвращает собственный дескриптор файла, который можно напрямую интегрировать в пользовательский цикл обработки 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 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);

Примеры API среды выполнения

Следующие примеры демонстрируют, как реализовать API среды выполнения на Kotlin и C++.

Пример на Kotlin: Адаптивный редактор изображений

В этом примере показано приложение для редактирования изображений ( com.example.imageeditor ), которое динамически увеличивает свой бюджет памяти, когда пользователь открывает многослойный холст для редактирования, и очищает динамический бюджет при возвращении к просмотру галереи миниатюр. Оно также регистрирует OnOverBudgetListener для удаления кэшированных изображений предварительного просмотра при перегрузке.

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

Пример использования NDK на C++: Нативный 3D-движок

В этом примере показано, как нативный игровой движок на C++ управляет бюджетом памяти в зависимости от активного уровня качества графики, используя AMemoryBudgetManager_Watcher_create на ALooper для выгрузки мипмапов текстур при превышении бюджета.

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