Бюджеты памяти позволяют приложениям объявлять свой бюджет памяти, что сообщает системе о необходимости сократить использование памяти, когда приложение использует больше установленного бюджета. Это особенно полезно для системных и встроенных приложений, а также для приложений, предназначенных для устройств с ограниченным объемом памяти, когда разработчик знает ожидаемый рабочий набор памяти (память, которая активно требуется приложению для того, что пользователь делает в данный момент) и хочет убедиться, что его приложение не использует слишком много общих ресурсов ОЗУ системы.
Баланс бюджета поддерживается за счет вытеснения памяти и подкачки, которые удаляют страницы памяти, не использовавшиеся в последнее время, и фокусируют объем потребляемой памяти приложения на его текущем рабочем наборе. Если приложение превышает заявленный бюджет, операционная система нацеливает возврат именно на это приложение:
- Чистые страницы, поддерживаемые файлами (например, неактивный код и сопоставленные объекты), вытесняются первыми, поскольку при необходимости их можно повторно прочитать из хранилища.
- Измененные страницы, связанные с файлами, записываются обратно в хранилище и вытесняются.
- Анонимные страницы памяти (например, память, выделенная для кучи) сжимаются и выгружаются в zRAM.
Пока рабочий набор не превышает бюджет, приложение будет работать хорошо, используя не больше памяти, чем установлено в бюджете. Операционная система вытесняет неиспользуемую память и сжимает неактивные страницы кучи, чтобы распределение памяти оставалось ограниченным без завершения процесса.
Что происходит, когда приложение превышает бюджет
Превышение лимита памяти не приводит к завершению работы или сбою приложения. Вместо этого, когда приложение использует больше памяти, чем позволяет его лимит, ОС находит память, которую приложение не использовало в течение некоторого времени, и безопасно убирает ее до тех пор, пока она снова не понадобится. Это позволяет освободить память для новых приложений и не превысить общий лимит.
Пока между объемом памяти, который требуется приложению в любой момент времени (его рабочим набором), и установленным бюджетом есть запас, приложение продолжает работать в обычном режиме. Если бюджет меньше рабочего набора, производительность приложения во время выполнения может снизиться.
Чтобы узнать, как ОС отслеживает и освобождает память, ознакомьтесь с руководством по архитектуре памяти.
- Освобождение памяти и подкачка. В этом разделе объясняется, как ОС распределяет частные анонимные страницы кучи и страницы, связанные с файлами, между приложениями и как она освобождает холодные страницы, когда превышен лимит.
- Основные понятия. Объясняется, почему общая память Zygote исключается из бюджета приложения и как бюджеты резидентной памяти связаны с анонимным RSS + swap в телеметрии.
- Битовые карты и память. Узнайте, как аппаратные битовые карты на базе графического процессора и выделения DMA-BUF учитываются за пределами кучи и бюджета памяти приложения.
Объявление бюджетов в манифесте 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– работа, которая не видна пользователю сразу, например фоновые задачи, будильники и широковещательные приемники.
В таблице ниже показано, как уровни важности во время выполнения сопоставляются с состояниями манифеста.
Манифест android:state |
Важность среды выполнения (RunningAppProcessInfo) |
Типичные компоненты |
|---|---|---|
foreground |
IMPORTANCE_FOREGROUNDIMPORTANCE_TOP_SLEEPING |
Возобновленная видимая активность, верхнее приложение с заблокированным экраном, сервис или поставщик контента, связанный с верхним приложением. |
perceptible |
IMPORTANCE_FOREGROUND_SERVICEIMPORTANCE_VISIBLE |
Активные службы воспроизведения медиаконтента, навигации, скачивания или синхронизации; служба, связанная с активной службой или видимыми пометками. |
background |
IMPORTANCE_PERCEPTIBLEIMPORTANCE_CANT_SAVE_STATEIMPORTANCE_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>
Многопроцессные приложения
Если ваше приложение разделяет работу между несколькими процессами, настройте бюджеты для каждого из них с помощью тега <process> внутри <processes>.
Рассмотрим пример с приложением для потоковой передачи музыки (com.example.radio):
- Основной процесс. Содержит видимый интерфейс и движок воспроизведения аудио (
MediaSessionServiceс активной службойmediaPlayback). Когда процесс виден, он работает в рамках бюджета активного режима, равного 180 МБ. Когда пользователь выходит из приложения, а музыка продолжает играть, процесс переходит в состояниеperceptible, в котором 64 МБ достаточно для работы механизма воспроизведения и аудиобуфера. - Процесс синхронизации (
: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>
<!-- 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> в манифесте перечислите записи <process> только для процессов, объявляющих <memory-budget>. Если вы хотите задать бюджет только для определенных подпроцессов, а для остальных оставить системное ограничение, не указывайте элементы <memory-budget> непосредственно в <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 ≈ 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 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. |
Измерьте рабочий набор и откалибруйте бюджеты
Хороший бюджет должен быть чуть выше рабочего набора приложения для заданного состояния, а не на его неограниченном пике. Чтобы найти эту точку, запустите реалистичный путь пользователя, постепенно снижайте бюджет и следите за моментом, когда ваше приложение начнет за это платить.
Разница между рабочим набором и резидентной памятью
Если бюджет не задан, резидентный объем памяти приложения (memory.current) включает два типа памяти:
- Рабочий набор. Куча Java, Kotlin и нативная куча, а также часто используемые страницы, связанные с файлами (код DEX, нативные библиотеки и объекты), которые постоянно задействуются в текущем пути пользователя.
- Холодный резерв. Кеши растровых изображений с предыдущих экранов, одноразовые буферы запуска, резерв кучи, оставшийся после всплеска выделения памяти, и страницы файлов, которые были прочитаны один раз и больше не использовались.
Бюджет, установленный на уровне пикового значения, никогда не компенсирует неиспользованные средства. Если бюджет меньше рабочего набора, ядро выгружает страницы кучи и удаляет страницы кода, которые приложение сразу же запрашивает обратно. Такие повторные ошибки рабочего набора требуют процессорного времени и заряда батареи, а если они возникают в потоке UI, то и кадров.
Вам нужно найти колено – наименьший бюджет, при котором удаляется только холодный резерв, а повторные сбои, процессорное время и время формирования кадра остаются на том же уровне, что и без бюджета.
Подготовьте тестовую среду
Настройте повторяемый тест, который ведет себя как рабочая версия:
- Калибруйте ту же оптимизированную с помощью R8 конечную сборку, которую вы поставляете. Отладочные сборки используют больше памяти, поэтому бюджет, настроенный для них, слишком велик для релизов.
- Протестируйте приложение на устройстве или эмуляторе с Android 17 QPR2 (уровень API 37.2) или более поздней версией. Лучше всего использовать сборку
userdebugили образ эмулятора без Google Play, поскольку на некоторых устройствах файлы cgroup, описанные в следующих разделах, можно прочитать только после выполнения командыadb root. - Напишите скрипт для пути пользователя продолжительностью 30–60 секунд с помощью Jetpack Macrobenchmark, UI Automator или
adb shell input. Сделайте это по одному разу для каждого состояния, для которого вы планируете бюджет. Простаивающее приложение ничего не говорит о своем рабочем наборе.foreground– прокрутка плотных списков и сеток медиафайлов, перемещение между основными разделами, открытие меню и диалоговых окон, запуск анимации.perceptible: закройте видимое окно, пока выполняется заметная работа (например, воспроизведение аудио или скачивание), подождите 10–15 секунд и вернитесь в приложение.background– запуск синхронизации, задачиWorkManager(например, с помощьюadb shell cmd jobscheduler run -f <package-name> <job-id>), сервиса или широковещательного приемника, когда у приложения нет видимого интерфейса.
Измерьте бюджет background во время выполнения фоновой работы. Когда процесс кешируется, платформа освобождает его бюджет (см. раздел Как состояния бюджета сопоставляются с состояниями процесса), поэтому бездействующий кешированный процесс не дает информации о бюджете background.
Определите базовые показатели
Начните с запусков без бюджета, чтобы понять, как выглядит нормальная работа.
Используйте сборку без объявлений <memory-budget> и запускайте каждый прогон из нового процесса (adb shell am force-stop <package-name>).
У каждого процесса приложения есть собственный контроллер памяти cgroup v2. Найдите путь к нему в строке 0:: файла /proc/<PID>/cgroup, а не создавайте его самостоятельно, поскольку макет cgroup может отличаться на разных устройствах. Передавайте каждую команду устройства в adb shell как одну строку в кавычках, чтобы оболочка устройства видела кавычки:
PID=$(adb shell pidof -s <package-name>)CG=/sys/fs/cgroup$(adb shell "sed -n 's/^0:://p' /proc/$PID/cgroup")adb shell "cat $CG/memory.current $CG/memory.swap.current"adb shell "grep -E '^(anon|file|workingset_refault_.*) ' $CG/memory.stat"
Чтение этих файлов не требует больших ресурсов и не мешает работе приложения, поэтому вы можете считывать их каждую секунду во время выполнения:
memory.current– резидентная память, выделенная процессу, в байтах. Это число, которое ограничивает<memory-budget>.memory.swap.current– память, сжатая в файл подкачки zRAM, в байтах.anonиfileвmemory.stat– резидентная анонимная память и память, связанная с файлами, в байтах.workingset_refault_anonиworkingset_refault_fileвmemory.stat– текущее количество выгруженных или удаленных страниц, которые снова понадобились приложению. Эти счетчики – основной признак того, что бюджет слишком мал.
Если появится ошибка, связанная с разрешениями, выполните команду adb root и повторите попытку. Если у вас нет root-доступа, adb shell dumpsys meminfo <package-name> предоставит вам менее подробные данные. В App Summary Java Heap, Native Heap и Stack – это в основном анонимная память, а Code – память, связанная с файлами. Программные растровые изображения учитываются в Native Heap и вашем бюджете, а аппаратные растровые изображения и Graphics – нет. Не запускайте dumpsys meminfo в цикле во время измерения времени формирования кадра. Он принудительно запускает сборку мусора в приложении и проходит по всем сопоставлениям памяти, что влияет на результаты измерений.
Проверка бюджетов кандидатов
Вы можете попробовать разные бюджеты, не пересобирая приложение. Чтобы задать бюджет для запущенного процесса, используйте am memory-limiter manual, а чтобы удалить – none.
adb shell am memory-limiter manual <PID> 120MBadb shell am memory-limiter manual <PID> none
Ручная переопределенная настройка действует в течение всего времени работы экземпляра процесса, включая изменения состояния процесса, поэтому очистите ее или перезапустите процесс, прежде чем продолжить.
Он также устанавливает жесткое ограничение процесса (memory.max) на то же значение, поэтому бюджет кандидата ниже рабочего набора вашего приложения может привести к завершению процесса, а не просто к его замедлению. Это считается невыполнением бюджета.
Вы также можете выполнить зачистку из тестовой сборки, прочитав значение-кандидат из аргумента инструментирования и установив MemoryBudgetManager.processBudgetBytes. Бюджет времени выполнения может только ужесточить бюджет манифеста, поэтому выполните проверку сборки без объявлений <memory-budget>. Оно также остается в силе при изменении состояния процесса, пока вы не вызовете clearProcessBudget.
Начните с самого высокого значения memory.current и уменьшайте его на 10–15 МБ за раз. Для каждого варианта бюджета:
- Запустите новый процесс, доведите его до целевого состояния и примените бюджет.
- Запустите путь пользователя один раз для подготовки. Применение бюджета приводит к однократному вытеснению страниц холодного запуска, а вам нужно измерить устойчивое состояние, а не переход.
Сбросьте статистику кадров и запишите начальные значения счетчиков повторных ошибок и процессорного времени (
utimeиstimeв тактах):adb shell dumpsys gfxinfo <package-name> resetadb shell "grep -E '^workingset_refault_(anon|file) ' $CG/memory.stat"adb shell "cut -d ' ' -f 14,15 /proc/$PID/stat"Запустите измерение пути пользователя.
Снова запишите значения
memory.current,anon,file, счетчиков ошибок страниц и процессорного времени, а также соберите статистику кадров.dumpsys gfxinfoможет выводить одну и ту же статистику несколько раз, поэтому возьмите первое совпадение, которое охватывает все приложение:adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 'Janky frames:'adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 '95th percentile'
Повторите каждый вариант не менее трех раз, чередуя их, а не запуская все повторения одного варианта подряд. Сравнивайте каждый результат с диапазоном контрольных значений, а не с одним значением.
Прочитайте результаты
По мере уменьшения бюджета приложение проходит через три режима:
- Выше колена.
memory.currentснижает потребление памяти, удаляя неиспользуемые страницыfileи неактивную кучу.anonостается плоским, повторные ошибки остаются вблизи базовой линии, а процессорное время и время формирования кадра не меняются. Это то, к чему вы стремитесь. - Ниже колена. Холодные страницы закончились, поэтому ядро начинает выгружать страницы кучи, которые все ещё используются приложением. Количество повторных ошибок увеличивается на сотни или тысячи страниц за запуск, а процессорное время растет. Время формирования кадра может почти не измениться на тестовом устройстве, которое в остальном бездействует, но теперь ваше приложение тратит процессорное время и заряд батареи на сжатие и распаковку собственного рабочего набора.
- Значительно ниже точки перегиба. Ошибки возврата и подкачки попадают в поток UI и
RenderThread. Время формирования кадра и время теплого возобновления увеличиваются, а также возникают задержки.
По сравнению с неограниченным базовым бюджетом, кандидат проходит, если он соответствует всем следующим условиям, которые являются разумной отправной точкой:
- Процесс продолжает выполняться после завершения запуска.
- Разница по умолчанию близка к базовой
- Процессорное время процесса увеличивается не более чем на 5 %.
- Доля кадров с задержкой увеличилась не более чем на 1 процентный пункт.
- Время формирования кадра P95 увеличивается не более чем на 3 мс.
- Время теплого возобновления (
TotalTimeизadb shell am start -W) увеличивается не более чем на 15 % для бюджетовperceptibleиbackground.
Точка перегиба – это наименьший бюджет, который проходит проверку. Например, вот как выглядит сканирование в активном режиме для гипотетического приложения с пиковым значением 138 МБ:
| Предлагаемый бюджет | memory.current |
anon |
file |
Ошибки страниц (страницы) | Время формирования кадра (95-й процентиль) | Режим |
|---|---|---|---|---|---|---|
| Нет | 138 МБ | 75 МБ | 63 МБ | 0 | 10,5 мс | Базовая температура |
| 125 МБ | 121 МБ | 74 МБ | 47 МБ | 10 | 10,5 мс | Выше колена |
| 110 МБ | 110 МБ | 75 МБ | 35 МБ | 15 | 10,8 мс | Колено |
| 95 МБ | 95 МБ | 74 МБ | 21 МБ | 176 | 11,2 мс | Ниже колена |
| 85 МБ | 85 МБ | 68 МБ | 17 МБ | 2050 | 12,8 мс | Ниже колена |
| 70 МБ | 70 МБ | 54 МБ | 16 МБ | 8771 | 16,5 мс | Намного ниже колена |
Выберите бюджет
Установите значение android:maxMb на 10–20 % выше критического, чтобы различия в пользовательских данных, регионах и всплесках выделения памяти не привели к сбою приложения. В предыдущем примере изгиб находится на уровне 110 МБ, поэтому 125 МБ – хороший выбор.
Затем установите конечную сборку с новыми объявлениями <memory-budget>, снова запустите путь пользователя и убедитесь, что критерии успешного прохождения по-прежнему выполняются. В разделе Memory Budget (Бюджет памяти) dumpsys
meminfo показывает активный бюджет и его источник.
Проверка бюджетов, не связанных с активным режимом, с помощью теплого возврата
Ограниченный бюджет perceptible или background может казаться достаточным, пока приложение находится в таком состоянии, но когда пользователь вернется, вы можете понести убытки. Проверьте и обратный путь:
- Запустите процесс в активном режиме, переведите приложение в целевое состояние и примените бюджет-кандидат с помощью
am memory-limiter manual. - Подождите 10–15 секунд, пока выполняется фоновая или видимая задача.
- Отмените переопределение (
adb shell am memory-limiter manual <PID> none), а затем сразу же верните приложение с помощьюadb shell am start -Wи запишитеTotalTime. Если вы задаете кандидата черезMemoryBudgetManager, вызовитеclearProcessBudget, прежде чем приложение вернет результат. - Запустите короткое взаимодействие на переднем плане и проверьте повторные ошибки и статистику кадров на соответствие критериям.
Выберите наименьший бюджет, который проходит обе проверки, когда приложение находится в этом состоянии и при теплом возврате, и добавьте те же 10–20 % запаса.
Калибровка масштаба экрана
Если вы используете android:additionalBytesPerDisplayPixel, найдите точку перегиба переднего плана на двух экранах с разным разрешением, например 1080 × 2400 (примерно 2,59 млн пикселей) и 1440 × 3120 (примерно 4,49 млн пикселей).
Разделите разницу между двумя точками перегиба в байтах на разницу в количестве пикселей, чтобы получить android:additionalBytesPerDisplayPixel. Вычтите это значение из точки перегиба для меньшего экрана, чтобы получить базовое значение, не зависящее от разрешения, и добавьте запас, чтобы получить android:maxMb.
API среды выполнения (вторичный динамический вариант)
Статическое объявление бюджетов в файле AndroidManifest.xml – это предпочтительное решение почти для всех приложений. Однако для приложений с динамическими рабочими нагрузками или для экспериментов со средой выполнения Android предоставляет API SDK и NDK среды выполнения в качестве дополнительного варианта.
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. Если в вашем приложении minSdk ниже 36, сначала проверьте SDK_INT, чтобы на устройствах с Android 15 (уровень API 35) или более ранней версией не возникала ошибка NoSuchFieldError при чтении 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)
}
Использование запросов и бюджеты
// 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 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)
Рекомендации по работе с обратными вызовами при превышении бюджета:
- Используйте фон
Looper. По возможности передавайте фонHandlerThreadLooperвregisterProcessOverBudgetListenerилиregisterPackageOverBudgetListener. Очистка кеша или файлов в фоновом потоке позволяет избежать конфликтов с отрисовкой кадров в потоке UI, когда процесс испытывает нехватку памяти. ЗарезервируйтеLooper.getMainLooperдля слушателей, которые затрагивают только состояние пользовательского интерфейса, связанное с основным потоком. - Ожидайте повторных обратных вызовов. Обратные вызовы при превышении бюджета не являются однократными уведомлениями. Когда использование превышает бюджет, операционная система сначала возвращает холодные страницы, а если давление продолжается, система продолжает возвращать память и вызывать ваш прослушиватель не чаще одного раза в секунду. Сделайте обратный вызов идемпотентным. Если вашему приложению нужно только одно уведомление или более длительный период ожидания между очистками, вызовите
unregisterProcessOverBudgetListener(илиunregisterPackageOverBudgetListener) внутри обратного вызова или защитите очистку с помощью временной метки ожидания илиAtomicBoolean. - Регистрируйте слушатель до уменьшения бюджета. Если текущее использование памяти уже превышает новый бюджет, когда вы вызываете
setProcessBudgetBytesилиsetPackageBudgetBytes, событие превышения бюджета активируется, как только применяется новый бюджет. Зарегистрируйте прослушиватель до того, как зададите бюджет, чтобы не пропустить первое уведомление о его превышении. - Быстрота. Операции по освобождению памяти должны давать немедленный результат. Сложные вычисления при высокой нагрузке снижают производительность.
- Избегайте выделения ресурсов. Не выделяйте ресурсы для новых объектов и не запускайте новые потоки внутри обратного вызова, так как это может привести к немедленному прямому возврату ресурсов операционной системой.
- Сосредоточьтесь на высокоэффективных целях. Удаление больших растровых изображений, буферов рендеринга или закрытие файлов, сопоставленных с памятью, гораздо эффективнее, чем освобождение множества мелких объектов.
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 позволяет отслеживать события памяти двумя способами:
- High-Level Watcher (
AMemoryBudgetManager_Watcher_create). Отслеживает события наALooperс автоматическим устранением дребезга. - Низкоуровневый дескриптор файла.
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 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);
Привяжите наблюдатель к ALooper фонового рабочего потока, а не к основному потоку пользовательского интерфейса или рендеринга, чтобы удаление кеша не мешало рендерингу кадров. Создайте наблюдателя до того, как ограничите бюджет с помощью AMemoryBudgetManager_setProcessBudget или AMemoryBudgetManager_setPackageBudget, чтобы приложение не пропустило первое событие превышения бюджета.
Пока процесс или пакет превышает бюджет, а операционная система продолжает освобождать память, наблюдатель повторно вызывает обратный вызов (не чаще одного раза за интервал debounce_ms). Сделайте обратный вызов идемпотентным или вызовите AMemoryBudgetManager_Watcher_destroy (это безопасно делать из обратного вызова), если вашему приложению нужно только однократное уведомление.
Примеры 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 в фоновом HandlerThread
Looper, прежде чем применить бюджет галереи, чтобы удалить кэшированные битовые карты предварительного просмотра под нагрузкой, не блокируя поток 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"
}
}
Пример 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;
}
// 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;
};