Об управлении памятью

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

Важность оптимизации памяти

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

  • Предотвращение холодного запуска (удобство для пользователей и удержание). Когда игрок временно переключается с игры на другое приложение, например чтобы ответить на уведомление или проверить сообщение, операционная система переводит процесс игры в фоновый режим. Если игра в фоновом режиме потребляет слишком много памяти, система Low Memory Killer (LMK) в первую очередь завершает ее процесс, чтобы освободить ОЗУ для активных задач. В следующий раз вместо быстрого возобновления игры пользователю придется ждать, пока она запустится заново. При этом будут загружаться тяжелые графические объекты, аудио и двоичные файлы игрового движка. Если вы будете следить за тем, чтобы использование памяти в фоновом режиме оставалось низким, вы сможете предотвратить такие завершения работы, сохранить состояние пользователя и обеспечить игрокам возможность немедленно возобновить сеанс. Подробную информацию о работе LMK можно найти в руководстве Android Vitals – Low memory killers.
  • Стабильность экосистемы и устройств. Неэффективное использование памяти и ее утечки ухудшают работу системы в целом. Когда системной памяти не хватает, система сталкивается с серьезной нагрузкой, что приводит к падению частоты кадров, задержкам в интерфейсе и сбоям в звуке. Если нехватка памяти слишком велика, системный механизм Low Memory Killer (LMK) агрессивно завершает фоновые процессы, заставляя другие приложения медленно запускаться и терять пользовательские данные, когда игроки переключаются между задачами.
  • Завершение процессов на уровне платформы. Начиная с Android 17 (уровень API 37) система более активно завершает процессы, которые используют слишком много памяти. Если игра занимает слишком много памяти, ОС может внезапно завершить ее процесс, не создавая стандартную трассировку стека.
  • Совместимость с устройствами. Флагманские устройства оснащены 12–16 ГБ ОЗУ, но у большинства геймеров в мире устройства с 4 или 6 ГБ ОЗУ. Правильное управление памятью гарантирует, что ваша игра останется доступной и отзывчивой на всех уровнях оборудования без необходимости использования сложных отдельных пакетов ресурсов.

Как работает память в Android

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

Основные понятия, связанные с памятью Android

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

  • Общие сведения о памяти. Для управления ОЗУ в Android используется разбиение на страницы и сопоставление памяти (mmap). Она не поддерживает традиционный файл подкачки на диске, а использует сжатие страниц (с помощью zRAM) и их утилизацию, чтобы освободить физическую память.
  • Распределение памяти между процессами. Android распределяет ОЗУ по всей системе. Она назначает определенные кучи для выполнения виртуальной машины Dalvik или ART, позволяя нативным средам разработки (например, игровым движкам C++) запрашивать память из нативной системной кучи.
  • Управление памятью приложений. Android работает по многопроцессной модели, поэтому приложения должны динамически отслеживать состояние своего жизненного цикла и добровольно освобождать ненужные ресурсы (например, некешированную графику и растровые изображения), чтобы поддерживать работоспособность системы.
  • Обзор процессов и потоков. Система распределяет процессы по иерархии в зависимости от их видимости и важности для пользователя. Это позволяет определить, какие процессы нужно сохранить, а какие – завершить в первую очередь при нехватке памяти.

Показатель общего объема потребляемой памяти

Ограничитель памяти Android 17 на уровне платформы оценивает потребление ресурсов процессом, используя общий объем потребляемой памяти, а не общий размер резидентной памяти (RSS) или размер виртуальной памяти.

Общий объем потребляемой памяти = анонимный RSS (RssAnon) + несжатый файл подкачки (VmSwap)

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

Ограничения памяти

Чтобы поддерживать стабильность системы и не допускать чрезмерного потребления ресурсов приложениями, платформа Android управляет ограничениями памяти для запущенных процессов.

Ограничитель памяти в Android 17 и более поздних версиях

В Android 17 (уровень API 37) и более поздних версиях для каждого приложения действуют строгие ограничения на использование памяти. Они задаются с помощью Linux cgroup v2 и не позволяют отдельным приложениям вызывать нестабильность системы. Дополнительную информацию о технической реализации можно найти в руководстве по ограничению памяти AOSP и статье Приоритет эффективности памяти: важные шаги для Android 17.

  • Механизм. Ограничитель памяти отслеживает все процессы приложения и динамически назначает ограничения в зависимости от состояния жизненного цикла процесса:
    • Видимые процессы (активные). Процессы приложений, у которых в данный момент отображается пользовательский интерфейс, могут использовать больше ресурсов, поэтому для них установлен более высокий лимит.
    • Невидимые процессы (фоновые или сервисные). Для процессов, которые выполняются без отображения пользовательского интерфейса, выделяется меньше ресурсов.
  • Атрибуты ядра. Сервис использует два основных атрибута:
    • memory.high – мягкое ограничение. Если этот лимит превышен, ядро ограничивает процесс и пытается агрессивно освободить память. Это может привести к снижению производительности игры.
    • memory.swap.max управляет жестким ограничением на пространство подкачки или zRAM, которое может использовать процесс.
  • Поведение при завершении. Если процесс продолжает выделять анонимную память сверх memory.high и исчерпывает емкость подкачки, выделение памяти завершается сбоем и ОС автоматически завершает процесс. Это завершение регистрируется с помощью ApplicationExitInfo с причиной выхода Memory Limiter (доступно начиная с Android 17, 26Q4).

Новые ограничения по памяти в Android Vitals в Play Console

Чтобы разработчики могли заранее выявлять проблемы с памятью, Google Play добавил новые показатели в Android Vitals в Play Console. Чтобы выявить экстремальные выбросы, Play Console отслеживает 90-й процентиль (P90) анонимного RSS и файла подкачки для игровых сеансов.

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

Подробные рекомендации и ограничения по объему памяти можно найти в статье Android Vitals: пороги соответствия.

Видимые пользователю сервисы

Заметные сервисы – это критически важные фоновые процессы, которые система Android считает заметными для пользователя. Это состояние охватывает все запущенные процессы:

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

Поскольку заметные сервисы предназначены для критически важных и длительных фоновых задач, они очень уязвимы к накопительным утечкам жизненного цикла. При этом платформа Android не считает, что игра работает в активном режиме. Это значит, что если игра работает в фоновом режиме, но в ней продолжает выполняться заметная служба, то на нее распространяются более строгие ограничения по объему памяти, указанные в Справочном центре Google Play. При переходе из активного режима в фоновый режим Perceptible Service игры должны активно удалять ненужные объекты.

Требования к R8

Чтобы минимизировать размер байт-кода и снизить нагрузку на базовый процесс Java, Google Play Console оценивает оптимизацию кода в рамках рекомендаций по обеспечению качества приложений. Подробную информацию о требованиях и ограничениях, связанных с оптимизацией игр, можно найти в разделе Оптимизация DEX-кода документации Android Vitals.

Чтобы настроить R8 в стандартном проекте для Android, следуйте инструкциям в руководстве Как оптимизировать приложения. Чтобы включить расширенные настройки сжатия и оптимизации, ознакомьтесь с разделом Использование R8 в полном режиме. Чтобы определить, какие правила мешают R8 обфусцировать классы или удалять неиспользуемый код, воспользуйтесь анализатором конфигурации R8.

Если вы используете игровой движок, поведение R8 по умолчанию зависит от движка и его версии:

Требования к битовым картам

Битовые карты занимают значительную часть памяти в современных играх с высоким качеством графики. Поскольку пиксельные данные Bitmap хранятся непосредственно в неуправляемой нативной куче в Android 8.0 (уровень API 26) и более поздних версиях, неоптимизированная загрузка изображений может привести к превышению пороговых значений памяти платформы. Рекомендации по масштабированию и кешированию изображений приведены в статье Оптимизация использования изображений.

Как отслеживать использование памяти

Чтобы эффективно оптимизировать использование памяти в игре, сначала нужно понять, как платформа Android измеряет объем занимаемой памяти. В Android 17 показатель использования памяти будет отслеживать сумму анонимного RSS (RssAnon) и несжатого файла подкачки (VmSwap), исключая память, поддерживаемую файлами, и частную память графического процессора. В этом руководстве подробно рассказывается, как использовать системные инструменты, такие как Perfetto и meminfo, внедрять API диагностики, например ProfilingManager и onTrimMemory, и извлекать точные данные о распределении памяти в Unity и Unreal Engine. Узнайте, как точно профилировать игру и избегать задержек, связанных с традиционным опросом памяти во время выполнения.

Подробнее об отслеживании использования памяти…

Стратегии снижения использования памяти

Игровые движки упрощают кросс-платформенную разработку, но из-за того, как они по умолчанию управляют памятью, могут срабатывать ограничения на уровне ОС. На этой странице подробно описаны практические шаги по оптимизации, специально разработанные для Unity и Unreal Engine. Узнайте, почему использование onTrimMemory на основе Java может привести к взаимоблокировкам в Unity и как вместо этого использовать нативные обратные вызовы жизненного цикла. Вы также узнаете, как оптимизировать объекты, например использовать сжатие текстур ASTC 8x8 и настраивать выгрузку объектов, чтобы игра работала без сбоев на устройствах любого уровня.

Подробнее о том, как уменьшить использование памяти…