После того, как вы разберетесь с управлением памятью в Android и настроите инструменты для измерения использования памяти вашей игрой, следующим шагом будет активное сокращение и оптимизация этого процесса. Соблюдение строгих ограничений Android помогает предотвратить закрытие игры системой, сокращает время запуска и обеспечивает корректную работу игры на всех устройствах.
В этом руководстве представлены практические методы по сокращению потребления памяти вашей игрой, с особым акцентом на оптимизацию на уровне ресурсов, конфигурации, специфичные для движка, и лучшие практики управления памятью.
Сокращение потребления памяти в Unity
Из-за архитектурных особенностей Unity, после расширения собственных блочных распределителей и управляемой кучи, движок, как правило, сохраняет эти страницы памяти для повторного использования, а не немедленно возвращает их операционной системе (ОС), даже после освобождения ресурсов. В частности, виртуальное адресное пространство (зарезервированная память) остается зарезервированным на протяжении всего времени работы процесса, а физическая память (RSS) не освобождается сразу, пока не произойдет несколько циклов сборки мусора (GC) и обрезки. Следовательно, временный пик использования памяти может привести к тому, что резидентная память останется раздутой в течение длительного времени даже после снижения фактического использования. Такое поведение увеличивает риск сбоев из-за нехватки памяти (OOM) на устройствах низкого класса и ухудшает общую стабильность во время выполнения.
Следовательно, оптимизация памяти в Unity должна осуществляться с помощью трех основных подходов, адаптированных к особенностям работы движка:
- Контролируйте активное использование памяти, чтобы предотвратить пиковые нагрузки на память.
- Управляйте форматами текстур и вариантами шейдеров, чтобы избежать создания ненужных ресурсов и собственных ресурсов.
- Необходимо провести рефакторинг структуры кода во время выполнения, чтобы исключить ненужные выделения памяти в управляемой куче и минимизировать частоту сборки мусора и расширение кучи.
Для получения более подробной информации см. раздел «Оптимизация памяти в Unity» .
Многопроцессная оптимизация
Использование памяти кэшированными процессами не учитывается при расчете лимитов памяти, поскольку не влияет на активные приложения. Запуск службы в отдельном изолированном процессе помогает основному процессу как можно быстрее перейти в кэшированное состояние, тем самым улучшая работоспособность вашей игры.
Для получения дополнительной информации см. разделы «Как отслеживать состояние процесса и память» , «Как изолировать процесс службы с помощью Unity» и «Как изолировать процесс службы с помощью Unreal» .
Сокращение использования памяти в сервисах, воспринимаемых пользователями.
В вашей игре может потребоваться выполнение логики в рамках сервиса, воспринимаемого пользователем, например, для завершения загрузки большого файла или для фоновых систем голосового чата. Эти стратегии помогут вам управлять использованием памяти и сокращать её потребление в подобных сценариях.
Стратегии для загрузки больших файлов
Эти стратегии могут быть применимы к большим файлам, которые вы хотите загрузить и продолжить загрузку даже после того, как пользователь свернет игру.
1. Изолируйте процесс загрузки.
Что: Убедитесь, что ОС может немедленно освободить память, которую ваше приложение не использует, выполнив загрузку в отдельном процессе, поскольку возможно, что распределитель памяти может удерживать страницы пула памяти и искусственно завышать объем памяти Anonymous RSS даже после освобождения массивов. Когда вы явно завершаете или выходите из службы и процесса, память возвращается в пул ОС, и ваш основной процесс останется неизменным.
В Unity: перенесите загрузку в нативный Android-
Serviceобъявленный с помощью процесса, например,android:process=":downloader", в пользовательском Manifest и вызовите его с помощью JNIAndroidJavaClassUnity. Убедитесь, что процесс завершен после окончания загрузки. Более подробные инструкции см. в разделе «Запуск видимого сервиса в отдельном процессе с Unity» .В Unreal Engine: Объявите пользовательскую
ServiceAndroid с процессом, например,android:process=":downloader", используя язык плагинов Unreal Engine, и запустите её с помощью C++ JNI. Убедитесь, что процесс завершается после окончания загрузки. Более подробные инструкции см. в разделе «Запуск видимой службы в отдельном процессе с Unreal Engine» .Для нативной Android: объявите
ServiceвAndroidManifestс процессом, например,android:process=":downloader". Запустите загрузку в этом изолированном процессе и вызовитеProcess.killProcess(Process.myPid())после завершения загрузки.
Как это помогает: Сокращает время, в течение которого память удерживается, и позволяет продолжить загрузку, освобождая при этом память, используемую основным процессом.
2. Прямая потоковая загрузка файлов на диск.
Что: Передача данных непосредственно из сетевого сокета на диск с использованием небольшого буфера фиксированного размера, который можно использовать повторно, вместо накопления сетевых ответов в большом массиве перед записью.
В Unity: Избегайте использования
DownloadHandlerBufferдля пакетов ресурсов или больших файлов, поскольку он выделяет собственный буфер памяти, эквивалентный размеру файла (анонимная память RSS). Вместо этого используйтеDownloadHandlerFileдля потоковой передачи байтов на диск в фоновом потоке.В Unreal Engine: передавайте входящие фрагменты данных непосредственно в
FArchive(архив на основе файлов, использующий файловый менеджер Unreal Engine) с помощьюSetResponseBodyReceiveStream(), а не добавляйте данные изIHttpRequestвTArray<uint8>.Для нативных приложений Android: перенаправляйте
InputStreamвFileOutputStream, используя пул буферов, вместо вызова методов.readBytes()или.string()для HTTP-ответа.
Чем это помогает: Снижает пиковое потребление памяти.
3. Декомпрессия загруженного файла в потоковом режиме.
Что делать: если загружаемый файл сжат, лучше обернуть сетевой входной поток в потоковый декомпрессор, например ZipInputStream вместо того, чтобы скачивать файл, загружать его в оперативную память, а затем распаковывать.
Чем это помогает: Снижает пиковое потребление памяти.
4. Делегировать задачи операционной системе.
Что: Чтобы полностью избежать управления фоновой памятью, делегируйте эту работу собственным API Android.
WorkManager— это современная рекомендуемая оболочка дляJobSchedulerна уровне операционной системы. Для Android 14 и вышеWorkManagerавтоматически обрабатывает загрузки, инициированные пользователем, как задание передачи данных, инициированной пользователем (UIDT). Это выполняется внутри процесса вашего приложения, поэтому вам все равно необходимо передавать данные непосредственно на диск, чтобы минимизировать использование памяти. UIDT защищает ваше приложение от сбоев, связанных с нехваткой памяти, позволяя операционной системе корректно приостанавливать и возобновлять загрузку, если системные ресурсы становятся ограниченными.DownloadManagerработает в отдельном системном процессе и не выделяет вашему приложению память для загрузки. Ваше приложение получит широковещательное уведомление, когда файл будет загружен и готов к загрузке.
Чем это помогает: WorkManager помогает справляться с нехваткой памяти, а DownloadManager снижает потребление памяти вашим приложением.
Стратегии вспомогательных служб
Эти стратегии могут применяться к вспомогательным сервисам, которые игра может захотеть запускать параллельно с основным игровым процессом, например, к фоновому голосовому чату.
1. Изолируйте процесс.
Что: Отделите эту функцию, например, ваше решение для голосового чата, от основного игрового движка. Например, вы можете запускать захват микрофона и потоковую передачу по сети внутри фоновой службы Android, назначенной отдельному процессу (объявленному в Manifest, например, с помощью android:process=":voice" ).
Как это помогает: Когда приложение свернуто, основной, ресурсоемкий игровой процесс может перейти в кэшированное состояние с более низким приоритетом, в то время как более легковесная вспомогательная служба продолжает работу в состоянии, воспринимаемом пользователем как служба.
2. Очистить неиспользуемую память внутри процесса.
Что: Если вспомогательная служба слишком глубоко интегрирована в игровой движок, чтобы её можно было отделить, постарайтесь максимально разгрузить этот процесс сразу после перевода игры в фоновый режим/свертывания. Рассмотрите возможность очистки кэша текстур, выгрузки несущественных сцен, снижения частоты обновления и рендеринга движка до нуля и явного вызова сборки мусора.
В Unity: выполняйте эту обрезку при срабатывании
OnApplicationPause().Resources.UnloadUnusedAssets()может оказаться полезным.В Unreal Engine: привяжите логику обрезки к делегату
ApplicationWillEnterBackgroundDelegate.Для нативных приложений Android: выполняйте обрезку памяти в
onPause()илиonStop()в зависимости от ситуации. Операционная система может попытаться вызвать вашу реализацию методаonTrimMemory()прежде чем прибегать к завершению процессов, использующих больше памяти.
Чем это помогает: Снижает потребление памяти, которая не требуется, когда игра не запущена в фоновом режиме.