Бесплатная консультация
← Все статьи

Бюджет памяти на мобильных: как не собирать вылеты по ночам

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

Графический ускоритель для игрового компьютера

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

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

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

Так же как команда заранее определяет целевой FPS, размер сборки или ограничения на draw calls, стоит заранее определить и допустимое потребление памяти. Особенно это важно при разработке игр для Android и разработке игр для iOS, где проект должен стабильно работать на линейке устройств с разными характеристиками, а не только на телефоне, который лежит у разработчика на столе.

Бюджет задаётся до контента, а не после

Если лимит памяти не назван в начале производства, каждая часть команды начинает использовать столько ресурсов, сколько удобно ей.

Художник импортирует текстуры в большем разрешении, потому что визуально так лучше. Разработчик держит несколько систем в памяти, потому что их быстрее не выгружать. UI получает отдельные большие атласы. Звук добавляется без общего ограничения. Затем подключаются аналитика, реклама, платежи и другие SDK.

Каждое решение может быть оправданным.

Проблема становится заметной только тогда, когда всё это начинает работать одновременно.

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

Первый шаг — определить минимальное поддерживаемое устройство.

Не самое популярное. Не устройство руководителя проекта. И не новый флагман.

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

После этого общий memory budget стоит разделить хотя бы на несколько крупных категорий:

  • текстуры;
  • меши;
  • анимации;
  • аудио;
  • код и managed heap;
  • UI;
  • runtime-данные;
  • кеши;
  • служебные буферы;
  • сторонние SDK;
  • запас на пиковые состояния.

Главный принцип: не распределять 100% доступной памяти.

У проекта должен оставаться запас. Новая версия SDK, дополнительная локализация, временный буфер загрузки или системная операция могут увеличить потребление даже тогда, когда сама сцена визуально не изменилась.

Считайте пик, а не среднее

Средняя память — удобная, но опасная метрика.

Модули оперативной памяти: бюджет памяти проекта

Игра может большую часть времени использовать условно безопасный объём памяти и всё равно регулярно вылетать.

Причина в том, что приложение падает не в среднем состоянии, а в конкретный пиковый момент.

Классический пример — переход между сценами.

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

Именно несколько секунд перехода могут оказаться самым дорогим состоянием всей игры.

Поэтому профилировать нужно не только стабильную gameplay-сцену.

Отдельно проверяйте:

  • холодный запуск;
  • первую загрузку тяжёлой сцены;
  • переход между двумя большими локациями;
  • возврат в предыдущую локацию;
  • несколько переходов подряд;
  • открытие тяжёлого интерфейса поверх gameplay;
  • загрузку магазина или инвентаря;
  • возврат приложения из background;
  • повторный вход в игровой сеанс.

Особенно полезен простой тест: пройти один и тот же цикл 10–20 раз и сравнить память в начале и в конце.

Если после каждого цикла baseline постепенно растёт, проект, скорее всего, что-то удерживает.

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

Минимальное устройство важнее флагмана

Оптимизировать память на мощном телефоне удобно, но почти бесполезно для определения реального нижнего порога.

Проект может идеально работать на современном устройстве и нестабильно вести себя на более старой модели, которая формально входит в список поддерживаемых.

При этом для Android недостаточно выбрать просто «старый Samsung» и «новый Xiaomi».

Устройства отличаются не только объёмом RAM, но и GPU, архитектурой, версией ОС и поведением драйверов. Поэтому вместо огромного парка случайных телефонов лучше собрать осмысленную матрицу Android-устройств и покрыть несколько классов риска.

Полезно иметь хотя бы:

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

На iOS парк предсказуемее, но проверка на старших и младших поддерживаемых поколениях всё равно обязательна.

Memory budget должен соответствовать нижнему уровню поддержки, а не среднему телефону вашей аудитории.

Текстуры обычно съедают больше всего

В большинстве мобильных игр текстуры — одна из крупнейших статей потребления памяти.

Комплектующие игрового компьютера

При этом размер PNG или JPEG в репозитории почти ничего не говорит о том, сколько ресурс займёт после загрузки.

Гораздо важнее:

  • разрешение;
  • runtime-формат;
  • компрессия под конкретную платформу;
  • наличие mip maps;
  • количество каналов;
  • способ загрузки;
  • необходимость держать ресурс в памяти прямо сейчас.

Поэтому начинать стоит с самых дорогих текстур, а не с попытки вручную ужать каждую маленькую иконку.

Проверьте большие environment-текстуры, персонажей, UI-атласы, splash-экраны и ресурсы, используемые только в отдельных режимах.

Отдельная проблема — атласы.

Сам факт использования atlas не гарантирует экономию памяти. Если большой атлас заполнен на четверть, оставшаяся площадь всё равно создаёт стоимость.

У интерфейса стоит проверить и mip levels. Для многих экранных элементов они просто не нужны, но при неправильных настройках продолжают занимать память.

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

Не все ассеты должны жить одновременно

После текстур стоит посмотреть на более фундаментальный вопрос:

почему этот ресурс вообще находится в памяти сейчас?

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

Например, если игрок находится в боевой сцене, действительно ли ему одновременно нужны:

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

аудио другого игрового режима?

Если ответ «нет», проблема может быть не в размере ресурсов, а в организации загрузки.

Для Unity-проектов особенно важно проверить Resources, Addressables, AssetBundles, ссылки между префабами и сцены-монолиты. При аудите Unity-проекта структура ассетов часто оказывается не менее важной, чем качество самого кода.

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

Поэтому хороший порядок оптимизации такой:

Определить, что реально находится в памяти.

Понять, что из этого не требуется текущему состоянию.

Убрать лишнее.

Только после этого уменьшать оставшиеся ресурсы.

Так обычно получается намного больший эффект при меньшей потере качества.

Не забывайте про managed memory и GC

Когда говорят о памяти мобильной игры, разговор быстро сводится к текстурам.

Но проблемы могут возникать и в коде.

Постоянные allocations создают нагрузку на managed heap и сборщик мусора. Даже если игра не достигает критического лимита памяти, регулярные пики GC могут создавать заметные фризы.

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

  • LINQ в горячих участках;
  • временные коллекции;
  • строки;
  • создание и уничтожение большого количества объектов;
  • неконтролируемые object pools;
  • сериализация;
  • UI, который постоянно перестраивает данные.

Здесь снова важно не оптимизировать вслепую.

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

Аудио, SDK и служебные системы тоже входят в бюджет

Память редко заканчивается исключительно из-за игрового контента.

Игровая станция с подсветкой

В проект постепенно добавляются:

  • аналитика;
  • crash reporting;
  • реклама;
  • attribution;
  • платежи;
  • social SDK;
  • voice chat;
  • push notifications;
  • дополнительные native-библиотеки.

Каждая интеграция может казаться небольшой, но общий baseline приложения постепенно растёт.

То же относится к аудио.

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

Поэтому после подключения крупного SDK или новой подсистемы полезно сравнивать memory profile до и после изменения.

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

Автоматическая проверка лучше ручной оптимизации

Разово привести проект к бюджету недостаточно.

Через месяц команда добавит новые уровни, персонажей, интерфейсы и события — и память снова начнёт расти.

Поэтому memory budget должен стать частью обычного production process.

В идеале pipeline сохраняет данные по сборкам, чтобы команда видела динамику.

Например:

Build 210 — peak 680 MB

Build 225 — peak 695 MB

Build 241 — peak 790 MB

Такой скачок сразу вызывает вопрос: что изменилось между 225 и 241?

Без истории команда обнаружит проблему только тогда, когда QA получит вылет на слабом устройстве.

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

Это не означает, что CI должен точно воспроизводить реальную память всех устройств. Его задача — ловить регрессии.

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

QA должен проверять память до content lock

Проблемы памяти дорого исправлять в самом конце production.

Если вся система загрузки уровней уже построена, сотни ассетов готовы, а release candidate начал вылетать, оптимизация может потребовать изменений сразу в нескольких направлениях.

Поэтому производительность и память должны входить в раннее тестирование.

В материале про QA до content lock мы уже разбирали, почему системные риски нужно проверять до того, как финальный контент заполнит проект.

Для памяти особенно полезны повторяемые сценарии:

  • несколько переходов между тяжёлыми сценами;
  • долгий игровой сеанс;
  • многократное открытие тяжёлых экранов;
  • suspend/resume;
  • смена режима игры;
  • загрузка нескольких уровней подряд.

Если тест выполняется одинаково на каждой заметной сборке, рост становится виден намного раньше.

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

Что делать, если бюджет уже превышен

Главное — не начинать с хаотического уменьшения всех ресурсов подряд.

Видеокарты крупным планом: производительность железа

Сначала снимите профиль на проблемном устройстве и найдите крупнейшие категории расходов.

Дальше действуйте по стоимости результата.

1. Уберите то, что вообще не должно быть в памяти

Это самый выгодный класс оптимизации.

Проверьте старые сцены, preload, кеши, зависимости, загруженные Addressables и объекты, переживающие смену состояния.

2. Найдите самые дорогие ассеты

Отсортируйте текстуры, меши, аудио и другие ресурсы по фактической стоимости.

Иногда несколько крупных ресурсов дают значительную часть всего превышения.

3. Проверьте пиковые переходы

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

4. Проверьте код и временные allocations

Особенно если память постепенно растёт во время долгого сеанса.

5. Только после этого снижайте визуальное качество

Уменьшение разрешения всех текстур — простой, но дорогой для продукта способ.

Если сначала убрать ненужное и исправить lifecycle ресурсов, иногда до массового ухудшения контента вообще не приходится доходить.

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

Простой чек-лист memory budget

Перед следующим большим этапом production проверьте:

  • определено ли минимальное поддерживаемое устройство;
  • есть ли конкретный memory budget;
  • разделён ли он между основными категориями;
  • оставлен ли запас;
  • известна ли самая тяжёлая сцена;
  • измеряются ли переходы между сценами;
  • проводится ли тест нескольких переходов подряд;
  • известны ли самые дорогие текстуры;
  • проверены ли mip maps и compression settings;
  • измеряется ли managed heap;
  • отслеживаются ли allocations;
  • проверяются ли сторонние SDK;
  • есть ли история потребления памяти между сборками;
  • тестирует ли QA минимальные устройства;
  • существует ли порог, после которого рост памяти считается регрессией.

Если на половину вопросов ответ «нет», проблема не в том, что игра использует слишком много памяти.

Проблема в том, что команда пока не управляет этим ресурсом.

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

Хороший memory budget не заставляет художников постоянно думать о мегабайтах и не превращает каждого разработчика в специалиста по профилированию.

Наоборот, он упрощает производство.

Команда заранее понимает рамки. Art знает технические ограничения ассетов. Разработчики понимают правила загрузки. QA знает, какие сценарии проверять. Lead видит рост до того, как он превращается в релизный блокер.

И тогда оптимизация перестаёт быть ночной операцией перед публикацией.

NoPanic Studio подключается к мобильным проектам на разных стадиях: от технического аудита и настройки бюджетов до оптимизации уже работающей игры. Работаем с Android-проектами, играми для iOS и Unity-проектами, где необходимо снизить пиковую память, найти причины вылетов или стабилизировать игру на минимальном классе устройств.

Если мобильная игра начинает вылетать на части устройств, память растёт от сборки к сборке или команда не понимает, какой лимит вообще считать безопасным — пришлите билд и описание целевых устройств. Разберём memory profile, найдём основные источники расхода и составим план оптимизации без случайного ухудшения всего контента.

Следующая статьяДиагностика рассинхрона в сетевой игре
→