Как задать бюджет памяти для приложений

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

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

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

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

Что происходит, когда приложение превышает бюджет

Если приложение превысит лимит, система не завершит его работу и не произойдет сбой. Вместо этого ОС найдет память, которая не использовалась приложением в течение некоторого времени, и безопасно уберет ее, пока она снова не понадобится. Это позволяет освободить память для новых приложений и не превысить общий лимит.

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

Чтобы узнать, как ОС отслеживает и освобождает память, ознакомьтесь с руководством по архитектуре памяти.

Как объявить бюджеты в манифесте Android

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

Декларации <memory-budget> вступают в силу на устройствах с Android 17 QPR2 (уровень API 37.2) и более поздними версиями. В более ранних версиях Android синтаксический анализатор манифеста платформы игнорирует нераспознанные элементы XML, поэтому вы можете использовать <memory-budget>, не нарушая обратную совместимость.

Укажите исходный бюджет

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

Как менять бюджеты в зависимости от состояния процесса

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

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

Вы можете объявить несколько условий <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 не является обязательным. Если вы укажете только условия для отдельных штатов (например, android:state="background"), то для других штатов бюджет приложения не будет ограничен. Если условие не содержит android:state, оно действует по умолчанию для неуказанных состояний (например, для активного режима), а последующие более строгие условия переопределяют его, когда приложение переходит в состояние perceptible или background.

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

  • Выделяйте foreground бюджеты для плавной отрисовки кадров. Предоставьте интерфейсу переднего плана достаточно места для активного рабочего набора, чтобы синхронная рекламация не задерживала отрисовку кадров. Если в вашем приложении есть отдельный рабочий процесс, требующий большого объема памяти (например, редактор изображений или видео высокого разрешения), рекомендуем перенести его в выделенный процесс (см. раздел Многопроцессные приложения). Это позволит независимо распределять память и освобождать ее, когда пользователь выходит из приложения.
  • Тщательно планируйте бюджеты background для фоновых задач. Высокий показатель RSS в фоновом режиме, зафиксированный телеметрией, часто указывает на то, что память, выделенная для интерфейса, не освобождается после перехода в фоновый режим (см. разделы Размещение привязанной службы вместе с интерфейсом, требующим большого объема памяти и Освобождение памяти в ответ на события) или что во время фоновой индексации и синхронизации выполняются однократные операции чтения файлов. Поскольку у фоновой работы без графического интерфейса нет сроков для кадров, удаление одноразовых страниц кеша файлов и перенос неактивных страниц кучи в zRAM, когда фоновая синхронизация ненадолго превышает свой бюджет, не вызывает видимых пользователю задержек и предотвращает удаление активного приложения пользователя из-за фоновых пиков.

Как статусы бюджетов соотносятся со статусами процессов

Платформа сопоставляет состояния бюджета манифеста с состояниями процесса выполнения на основе RunningAppProcessInfo.importance, которое Activity Manager вычисляет на основе собственных активных компонентов процесса и любых входящих привязок сервисов или запросов поставщиков контента из других приложений:

  • foreground – активные, видимые взаимодействия с пользователем, например размещение возобновленного Activity, поддержание состояния верхнего приложения при выключении экрана или размещение сервиса или поставщика контента, активно связанного с верхним активным приложением или запрашиваемого им.
  • perceptible – рабочие нагрузки, которые заметны пользователю, но не имеют видимого окна. К ним относятся воспроизведение медиаконтента, пошаговая навигация, запись с камеры или микрофона, активные фоновые скачивания, активные службы синхронизации данных или службы, связанные с активным клиентом с помощью флагов, таких как BIND_NOT_FOREGROUND (см. раздел Управление наследованием с помощью флагов BIND). Хотя внутренние метрики платформы могут помечать некоторые из этих рабочих нагрузок как PROCESS_STATE_IMPORTANT_FOREGROUND, эта внутренняя константа не означает видимое окно и регулируется бюджетом perceptible.
  • background – работа, которая не видна пользователю, например фоновые задачи, запланированные операции, широковещательные приемники или предыдущие Activity после того, как пользователь перешел на другую страницу.

В таблице ниже показано, как уровни важности во время выполнения сопоставляются с состояниями манифеста.

Манифест android:state Важность среды выполнения (RunningAppProcessInfo) Типичные компоненты
foreground IMPORTANCE_FOREGROUND
IMPORTANCE_TOP_SLEEPING
Возобновленная видимая активность, верхнее приложение с заблокированным экраном, сервис или поставщик контента, связанный с верхним приложением.
perceptible IMPORTANCE_FOREGROUND_SERVICE
IMPORTANCE_VISIBLE
Активное воспроизведение медиаконтента, навигация, скачивание или синхронизация активных служб; служба, связанная с активной службой или видимыми флагами.
background IMPORTANCE_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
IMPORTANCE_SERVICE
Фоновые задачи, получатели, фоновая синхронизация, предыдущее действие или главное приложение в фоновом режиме

Подробнее о том, как привязки клиентов повышают приоритет процесса хоста сервиса или поставщика и как процесс возвращается к состоянию background или cached после того, как клиент отменяет привязку или уходит с экрана, рассказывается в разделе Привязки сервисов и состояния процессов.

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

Проверьте состояние процесса и бюджет приложения

Один из способов проверить состояние и важность активного процесса приложения во время разработки – отправить запрос в Activity Manager с помощью ADB:

adb shell dumpsys activity processes <package-name>

Ниже приведен сокращенный пример записи процесса и элементов управления OOM для приложения, в котором запущена активная служба.

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

В этом примере:

  • curProcState=4 и state: cur=FGS указывают на то, что процесс находится в состоянии активной службы. Если в приложении есть активные видимые действия, вы увидите значок TOP. Если в приложении выполняется важный компонент переднего плана (например, скачивание или синхронизация), оно отображается как IMPF.
  • В таблице управления нехваткой памяти для процессов prcp указывает, что процесс оценивается на уровне приоритета "Заметно", который соответствует бюджету perceptible.

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

adb shell dumpsys meminfo <package-name>

Начиная с Android 17 QPR2, в выходных данных есть раздел Memory Budget, в котором указаны верхний предел активного ограничения, источник ограничения (например, AndroidManifest или MemoryBudgetManager) и текущий объем резидентной памяти.

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

Если ваше приложение разделяет работу между несколькими процессами, настройте бюджеты для каждого из них с помощью тега <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" />

        <processes>
            <!-- Default application process (uses the package baseline budgets above) -->
            <process />

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

При объявлении блока <processes> в манифесте добавьте элемент <memory-budget> непосредственно под <application> и укажите запись <process> для каждого процесса, используемого компонентами в объединенном манифесте, включая <process /> для процесса приложения по умолчанию. Если вы хотите ограничить бюджет только для определенных подпроцессов, а для остальных оставить системный лимит, укажите высокое базовое значение для пакета, например <memory-budget android:maxMb="1000000" /> в <application>.

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

Как масштабировать бюджеты для дисплеев с высокой плотностью пикселей

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

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

    <!-- Baseline 180MB + 16MB multiplied by (density ratio)^2 -->
    <memory-budget
        android:maxMb="180"
        android:additionalMbPerDensity="16" />
    

    На экране mdpi (1.0x) бюджет составляет 180 + 16 × 12 = 196 МБ. На экране xxhdpi (3.0x) бюджет увеличивается до 180 + 16 × 32 = 324 МБ.

  • Масштабирование по физическому разрешению экрана (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 &approx; 2,59 млн пикселей) это добавляет &approx; 41,4 МБ к базовому бюджету. На экране с разрешением 1440p (1440 × 3120 &approx; 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 cgroup (который исключает общую память, например 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 среды выполнения в качестве дополнительного варианта.

Runtime API позволяет:

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

Android SDK API (MemoryBudgetManager)

Системный сервис MemoryBudgetManager доступен для приложений, написанных на Kotlin и Java, начиная с Android 17 QPR2 (минорный выпуск SDK, уровень API 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2).

Получение сервиса

Прежде чем использовать MemoryBudgetManager, убедитесь, что на устройстве установлена версия Android 17 QPR2 или более поздняя. Для этого используйте SDK_INT_FULL:

if (Build.VERSION.SDK_INT_FULL >= 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 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()

В отличие от пунктов манифеста <memory-budget>, которые автоматически переключают бюджеты при переходе процесса между состояниями foreground, perceptible и background, динамический бюджет, заданный с помощью MemoryBudgetManager, применяет один потолок, пока ваш код не обновит или не очистит его. Если вы используете API среды выполнения для проведения полевых экспериментов или управления ограничениями, зависящими от состояния, обновляйте или очищайте динамический бюджет при переходах жизненного цикла (например, с помощью ProcessLifecycleOwner или обратных вызовов жизненного цикла объекта activity, как показано в примере адаптивного редактора изображений).

Производительность и идемпотентность

При работе с MemoryBudgetManager во время выполнения учитывайте следующие характеристики:

  • Идемпотентность. Все API для изменения бюджета идемпотентны. Повторный вызов clearProcessBudget() или clearPackageBudget(), а также их вызов при отсутствии активного динамического бюджета не приведет к ошибке. Аналогично, последовательное присвоение setProcessBudgetBytes() или setPackageBudgetBytes() одного и того же значения не приведет к лишним изменениям конфигурации системы.
  • Частота вызовов. При настройке, обновлении или очистке бюджета выполняется межпроцессный вызов системной службы, чтобы обновить контроллер памяти операционной системы для процесса. Хотя он и легковесный, он не полностью бесплатный. Вызывайте эти API во время отдельных переходов жизненного цикла и этапов выполнения задач (например, при входе в действие или выходе из него, запуске или завершении фонового процесса или обработке запросов на речь). Не вызывайте их в узких циклах, потоках обработки аудио или подпрограммах отрисовки для каждого кадра.
  • Частота запросов. Свойства запросов processCurrentUsageBytes и packageCurrentUsageBytes получают данные об использовании памяти в реальном времени от системного сервиса. Несмотря на высокую скорость, запросы следует вызывать по требованию (например, во время переходов между задачами или внутри обратных вызовов OnOverBudgetListener), а не опрашивать в непрерывных циклах.

Как получать обратные вызовы о превышении бюджета

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

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)

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

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

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

Начиная с Android 17 QPR2 (уровень API 37.2), нативные приложения могут использовать C NDK API, предоставляемый 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 (or unconstrained baseline)
AMemoryBudgetManager_clearProcessBudget();

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

NDK позволяет отслеживать события памяти двумя способами:

  1. High-Level Watcher (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);

Примеры Runtime API

Ниже приведены примеры реализации API среды выполнения с помощью Android SDK API (написан на Kotlin) и Native NDK API (написан на C++).

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

В примере ниже показано приложение для редактирования изображений (com.example.imageeditor), в манифесте которого указан лимит в 256 МБ, чтобы обеспечить работу многослойного холста:

<manifest ... >
    <application ... >
        <!-- Manifest ceiling accommodates the heaviest editing workload -->
        <memory-budget android:maxMb="256" />
    </application>
</manifest>

Когда пользователь просматривает галерею уменьшенных изображений, приложение использует Android SDK API в Kotlin, чтобы динамически уменьшить бюджет процесса до 96 МБ. Когда пользователь открывает многослойный холст редактирования, приложение очищает динамический бюджет, чтобы восстановить полный потолок манифеста в 256 МБ. Он также регистрирует 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)
        // Constrain memory during lightweight gallery browsing
        applyGalleryBudget()
    }

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

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

Пример NDK C++: нативный 3D-движок

В этом примере показано, как нативный игровой движок C++ управляет бюджетом памяти на основе активного уровня качества графики. Предполагается, что в манифесте приложения указан лимит в 512 МБ для графики высокого качества (android:maxMb="512"). Движок динамически сокращает бюджет для настроек более низкого качества и использует AMemoryBudgetManager_Watcher_create в ALooper, чтобы выгрузить MIP-текстуры, когда бюджет превышен.

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