Несмотря на то, что физический объём оперативной памяти современных мобильных устройств продолжает расти, потребление памяти играми опережает этот рост из-за высокого разрешения игровых ресурсов и сложных конвейеров рендеринга.
Основные проблемы, вызванные чрезмерным использованием памяти.
- Завершение работы программ с низкой загрузкой памяти (LMK) : ОС принудительно закрывает фоновые или даже запущенные приложения для решения проблемы нехватки памяти в масштабах всей системы.
- Падение кадров и рывки (заикание) : частые всплески сборки мусора (GC) в управляемой куче или подкачка памяти на уровне операционной системы создают узкие места в обработке данных.
- Перегрев и разрядка батареи : Непрерывное выделение, освобождение и сохранение страниц памяти приводят к значительной нагрузке на процессор, что вызывает повышение температуры и более быстрый разряд батареи.
Обновления управления памятью Android
- Android 17: Введение
MemoryLimiter: В Android 17 представленMemoryLimiter, который активно отслеживает потребление памяти приложениями в соответствии с пороговыми значениями, установленными для конкретного устройства. Приложения, превышающие лимит памяти, немедленно завершают работу на системном уровне. Поскольку этот механизм более строгий, чем традиционный LMK, управление пиковым использованием памяти стало более важным, чем когда-либо.
Снижение потребления памяти в Unity
В Unity, как только движок расширяет свои внутренние пулы памяти ( нативные распределители блоков и управляемую кучу ) для размещения высокой пиковой нагрузки, он сохраняет эти страницы памяти, а не немедленно возвращает их операционной системе. Следовательно, даже после выгрузки ресурсоемких файлов базовый объем резидентной памяти остается увеличенным, что делает приложение крайне уязвимым для завершения процессов операционной системы Android (таких как LMK или MemoryLimiter ).
Чтобы предотвратить принудительное управление памятью на уровне операционной системы, оптимизацию памяти следует проводить, опираясь на три ключевых принципа:
- Категория А: Снижение пикового использования памяти.
- Категория B: Устранение ненужных активов и системных ресурсов.
- Категория C: Исключение ненужных выделений GC.
Категория А: Снижение пикового использования памяти.
В Unity распределители памяти сохраняют освобожденные страницы для повторного использования, вместо того чтобы сразу возвращать их операционной системе, поэтому базовый уровень использования памяти, как правило, отражает максимально достигнутый пик, а не текущее использование. Поэтому предотвращение скачков в первую очередь более эффективно, чем полагание на очистку после их возникновения.
1. Избегайте слишком больших пакетов активов (AssetBundles).
Из-за механизма загрузки пакетов Unity, огромные AssetBundles приводят к значительному раздуванию памяти и сбоям из-за нехватки памяти. Для эффективного управления ресурсами следует использовать модульную структуру пакетов.
Ключевые вопросы
- Высокий уровень потребления памяти : запрос одного крошечного ресурса заставляет Unity загружать весь файл пакета (включая заголовки, метаданные и буферы потоковой передачи) в оперативную память.
- Ловушка выгрузки : если какой-либо ресурс в пакете активно используется, весь пакет не может быть выгружен , в результате чего неиспользуемые данные остаются в оперативной памяти.
Передовые методы
- Поддерживайте модульность пакетов : группируйте ресурсы логически по сценам или жизненному циклу.
- Совет для Unity 6.6+ : используйте каталоги контента , чтобы предотвратить непреднамеренные зависимости между пакетами.
2. Оптимизация ссылок на ресурсы в ScriptableObjects
Прямая сериализация полей UnityEngine.Object в ScriptableObject создает прямые жесткие ссылки, заставляя все ссылочные ресурсы загружаться в оперативную память сразу после загрузки или создания экземпляра самого ScriptableObject .
// BEFORE: Loading SceneRequiredAssets forces _worldAsset and _spawnSettings into RAM immediately
public class SceneRequiredAssets : ScriptableObject
{
public string sceneName;
public Object _worldAsset;
public Object _spawnSettings;
}
// AFTER: Use AssetReference to enable asynchronous, on-demand loading using Addressables
public class SceneRequiredAssets : ScriptableObject
{
public string sceneName;
public AssetReference _worldAsset;
public AssetReference _spawnSettings;
}
3. Настройка типов загрузки аудиоклипов
Загрузка всех аудиоклипов в несжатом виде непосредственно в память приводит к большим и постоянным пиковым нагрузкам на память. Параметры загрузки аудиофайлов следует настраивать в зависимости от профиля их использования:
| Категория аудио | Тип нагрузки | Причина |
|---|---|---|
| Фоновая музыка (BGM) | Стриминг | Аудиопоток передается с диска в небольших буферах для устранения скачков потребления памяти. |
| Длинные звуковые эффекты | Сжато в памяти | Обеспечивает низкий уровень нагрузки на оперативную память и выполняет декомпрессию звука на лету во время воспроизведения. |
| Короткие и частые звуковые эффекты | Декомпрессия при загрузке | При загрузке аудиофайл сжимается в оперативную память, чтобы избежать дополнительной нагрузки на процессор во время воспроизведения. |
4. Внедрить стратегии объединения и освобождения объектов.
Неосвобожденные экземпляры, остающиеся внутри пулов объектов после переходов между сценами, бесконечно удерживают зарезервированную память, неоправданно завышая базовый объем используемой памяти.
- Действие : Периодически очищайте или удаляйте неиспользуемые объекты из пула во время переходов между сценами или в периоды низкой активности, позволяя Unity вернуть или повторно использовать зарезервированное пространство для других задач.
Категория B: Исключение ненужного использования памяти.
Устранение избыточных графических ресурсов и рендеринг целевых буферов напрямую снижают базовый объем используемой памяти.
1. Оптимизировать текстуры рендеринга и глубину резкости камеры.
- Удалить буферы глубины/трафарета : установите параметр «Формат трафарета глубины» в значение «Нет» для текстур рендеринга, которым требуются только данные о цвете.
- Отключение текстур глубины для камер пользовательского интерфейса : Для камер пользовательского интерфейса, где данные о глубине не требуются, отключите генерацию текстур глубины в настройках камеры URP, чтобы исключить проход
CopyDepthи связанную с ним память текстур GPU.
2. Оптимизация текстур и моделей.
| Категория | Руководство по оптимизации |
|---|---|
| Сжатие текстур | Всегда используйте форматы сжатия целевой платформы (например, ASTC для Android). |
| Доступны режимы чтения/записи. | Оставляйте отключенным, если это не необходимо. Включение этой функции приводит к дублированию текстурной памяти как в ЦП, так и в ОЗУ видеокарты. |
| Мипмапы | Отключите мипмапы для текстур пользовательского интерфейса или объектов, зафиксированных на постоянном расстоянии от камеры, что позволит сэкономить примерно 33% памяти, используемой для текстур. |
| Сложность сетки | Уменьшите количество ненужных полигонов и потоков вершин, чтобы снизить потребление памяти графическим процессором и собственной памятью. |
3. Удалить варианты шейдеров и оптимизировать память.
Супершейдеры (например, URP Lit Shader) инкапсулируют множество функций с помощью ключевых слов #multi_compile и shader_feature . Без оптимизации комбинаторный взрыв создает десятки тысяч уникальных вариантов шейдеров , что приводит к увеличению размеров сборки, огромному потреблению нативной памяти и проблемам с компиляцией драйверов GPU во время игры.
А. Механизмы накладных расходов на память вариантов шейдеров
- Комбинаторный взрыв : общее количество возможных вариантов экспоненциально возрастает с каждой добавленной группой ключевых слов.
- Архитектура распределения блоков : группы Unity компилируют бинарные варианты в сжатые блоки памяти, называемые блоками (по умолчанию: 4 МБ).
- Раздувание собственной памяти : Когда исполняемый код запрашивает даже один вариант внутри блока, весь блок размером 4 МБ декомпрессируется в оперативную память . Если оптимизация не выполнена, тысячи неиспользуемых вариантов, упакованных в эти блоки, навсегда занимают собственную память.
B. Встроенный в Unity многоступенчатый конвейер удаления ненужных вариантов: Unity автоматически удаляет ненужные варианты во время сборки на основе настроек графики целевой платформы и неиспользуемых функций движка (например, туман, карты освещения и настройки XR). Кроме того, варианты shader_feature автоматически отфильтровываются, если их ключевые слова не используются активно ни одним материалом в проекте, тогда как варианты #multi_compile принудительно включаются независимо от их использования.
C. Пользовательская архитектура автоматического удаления ключевых слов ( IPreprocessShaders ). Поскольку статический анализ не может обнаружить ключевые слова, динамически изменяемые с помощью скриптов C# во время выполнения ( Material.EnableKeyword ), стандартного удаления часто недостаточно. Чтобы гарантировать включение только фактически используемых вариантов, вы можете собирать варианты во время выполнения тестовых наборов QA с помощью Player.log (включив логирование компиляции шейдеров в настройках редактора) или трассировок профилировщика (маркеры Shader.CreateGPUProgram ). Затем реализуйте IPreprocessShaders.OnProcessShader в скрипте редактора, чтобы отфильтровать варианты, которые никогда не выполнялись во время выполнения, оставив в сборке только необходимые.
Категория C: Исключение ненужных выделений GC.
Выделение памяти сборщиком мусора (GC) в управляемой куче приводит к фрагментации памяти, расширению кучи, которое никогда не уменьшается, и серьезному падению частоты кадров во время пауз в работе сборщика мусора.
1. Предотвратить выделение ресурсов для закрытия лямбда-функций.
Когда лямбда-выражение захватывает внешние локальные переменные, C# генерирует неявный класс отображения в куче. Выполнение этого класса внутри Update выделяет экземпляры замыкания каждый кадр.
// Bad: Capturing local variable 'targetId' allocates a new closure object on the Heap every frame
void Update()
{
int targetId = 100;
Monster target = monsterList.Find(m => m.Id == targetId);
}
// Good 1: Replace with a standard 'for' loop (Recommended: 0 B allocation)
void Update()
{
int targetId = 100;
Monster target = null;
for (int i = 0; i < monsterList.Count; i++)
{
if (monsterList[i].Id == targetId)
{
target = monsterList[i];
break;
}
}
}
// Good 2: Use a static lambda (C# 9.0+) if no outer variables are captured
Monster target = monsterList.Find(static m => m.Id == 100);
2. Используйте stackalloc и Span.
Избегайте выделения памяти в куче для кратковременных временных массивов, используя стековую память.
// Before: Allocates an array on the Heap every call (GC Target)
Vector2[] pos = new Vector2[4];
// After: Utilizes Stack memory using System.Span (0 B Heap Allocation)
System.Span<Vector2> pos = stackalloc Vector2[4];
3. Оптимизация итераций сбора данных.
Избегайте упаковки объектов и выделения памяти в куче для перечислителей, вызванных расширениями LINQ или методами доступа ReadOnlyCollection.
// Before: LINQ Count() causes internal GetEnumerator() heap allocations
bool hasData = component != null && component.parameters.Count(parameter => parameter.overrideState) > 0;
// After: Replaced with indexer and direct loop iteration
bool hasData = HasDataOptimized(component);
private bool HasDataOptimized(TestComponent component)
{
if (component == null) return false;
var count = component.parameters.Count;
for (var i = 0; i < count; ++i)
{
if (component.parameters[i].overrideState)
return true;
}
return false;
}
4. Дополнительные правила предотвращения распределения GC
- Избегайте использования
Camera.allCameras, поскольку при каждом вызове в куче создается новый массивCamera[]. Вместо этого кэшируйте массив камер и передавайте его вCamera.GetAllCameras(_allCameras). - Избегайте использования
foreachдляIReadOnlyList<T>: итерация по интерфейсу приводит к упаковке перечислителя структуры, что вызывает выделение памяти сборщиком мусора. Вместо этого используйте стандартный циклfor. - Кэширование объектов сопрограмм : кэшируйте экземпляры
WaitForSecondsвместо многократного создания экземпляров с помощьюyield return new WaitForSeconds(time);; - Использование пользовательских структур в качестве ключей словаря : использование пользовательских структур в качестве ключей словаря вызывает метод
Equalsпо умолчанию, что приводит к упаковке объектов. Реализуйте классIEqualityComparer<T>и передайте его конструктору словаря.
public struct TypeKey
{
public int v1;
public int v2;
public class TypeKeyComparer : IEqualityComparer<TypeKey>
{
public bool Equals(TypeKey x, TypeKey y) => x.v1 == y.v1 && x.v2 == y.v2;
public int GetHashCode(TypeKey obj) => obj.v1.GetHashCode() ^ obj.v2.GetHashCode();
}
}
// Pass custom comparer during Dictionary initialization to prevent boxing
public readonly Dictionary<TypeKey, int> _typeKeyDictionary = new(new TypeKey.TypeKeyComparer());
5. Сведите к минимуму использование обобщенных типов и рефлексии.
Хотя обобщенные методы обеспечивают отличное повторное использование кода и удобство сопровождения, их чрезмерное использование может негативно сказаться на вашем проекте в контексте бэкенда Unity IL2CPP (Intermediate Language to C++).
- Раздувание кода IL2CPP : Для каждой уникальной комбинации обобщенных типов IL2CPP генерирует специализированную версию кода. Чрезмерное использование сложных обобщений может привести к «комбинаторному взрыву» сгенерированного кода C++, значительно увеличивая размер двоичного файла приложения и объем используемой памяти.
- Накладные расходы на рефлексию : Методы, использующие рефлексию, такие как API
System.Reflection, по своей природе медленны и часто приводят к выделению памяти в куче во время выполнения. - Рекомендации : используйте обобщения с умом — отдавайте им приоритет для архитектурной ясности, а не для широкого и неизбирательного применения. Там, где производительность имеет решающее значение, отдавайте предпочтение конкретным типам или полиморфизму на основе интерфейсов. Для рефлексии кэшируйте результаты, такие как
MethodInfoилиFieldInfoво время инициализации, а не запрашивайте их в цикле обновления.
6. Избегайте утечек управляемых оболочек.
Каждый UnityEngine.Object , такой как MonoBehaviour , Texture или GameObject , имеет оболочку C# "Managed Shell", которая взаимодействует с нативным движком C++.
- Проблема : если управляемая оболочка удерживается в памяти статической ссылкой, постоянной подпиской на события или неочищаемым замыканием, сборщик мусора не может освободить память. Даже если нативный объект уничтожен, управляемая оболочка сохраняется, что приводит к «фантомным» утечкам памяти, которые раздувают управляемую кучу.
- Решение : Всегда используйте надежные алгоритмы очистки. При уничтожении объектов или переходе между сценами явно отписывайтесь от событий с помощью оператора
-=и обнуляйте статические ссылки на типыUnityEngine.Object. Эта очистка гарантирует, что сборщик мусора сможет успешно удалить обертку после того, как нативный движок освободит свой дескриптор.