Unity. Scriptable Object и память
Захотелось поделиться неожиданной для меня находкой, связанной с тем как выделяется память и как легко столкнуться с крупной утечкой, которую без перезапуска не почистить.
Как столкнулся, в нашем проекте есть заготовленные куски уровней в виде prefab, в них и меши и логика и эффекты. При генерации уровень собирается из нескольких блоков. Так же есть Scriptable Object для хранения описания игрового уровня, какие блоки, что за уровень и.т.п., на менеджере всего этого дела висел список этих описаний уровней. В определенный момент столкнулся с тем что свободной оперативной памяти 0.
Для примера.
Проблема первая.
В чем собственно проблема? Как только Unity видит переменную со ссылкой на scriptable object из проекта, он тянет его в память, а потом и все на что он ссылается, в том числе префабы блоков. А префабы блоков тянут конечно весь тот контент, что содержат. Для меня лично было не очевидно что ссылки на контейнеры данных, scriptable object, потянут за собой по цепочке пол проекта в память :) А у меня бюджет памяти не очень большой тем временем.
Решение. Можно загружать описания уровня по имени из Resources, разбить их вообще на два файла, в одном текстовая информация, для выбора уровней, а в другом связи с ассетами. Второй грузить только когда они нужны на сцене. Можно еще и с относительно новыми Adressables повозиться.
Проблема вторая.
Есть ещё одна менее очевидная проблема, если загрузить Scriptable Object в память при помощи:
preset = Resources.Load("ScriptablePreset") as ScriptableOne;
И потом удалить со сцены объект, который это делал (или вообще все объекты и даже сцену другую загрузить), то загруженный Scriptable Object будет висеть мертвым грузом в памяти и вместе с ним все на что ссылается он и далее по цепочке. Можно звать Garbage Collector, Resources.UnloadUnusedAssets, грузить другие сцены, все будет бестолку и сколько угодно времени, в памяти будет висеть то, на что никто уже не ссылается. Получалось что уровень я создал, потом удалил, но в памяти весь контент висит дальше, получается большой утечка памяти и через сколько то запусков на устройстве с сильно ограниченной памятью, типа Switch/Mobile игра крашнется.
Решение. Но как же быть? Выход то простой, нужно на OnDestruction явно обнулить переменную, где мы хранили указатель на Scriptable Object, GC сочтет это за руководство к действию.
private void OnDestroy(){ preset = null;}
Вот так, старые добрые деструкторы пиши. На всякий случай даже уточнил на форуме Unity не баг ли это, нет, "так и задумано". В profiler / profile analyzer | memory profiler такие ситуации отследить не очень то просто, у ассетов будет просто не указано кто его удерживает в памяти, т.к. обьект удален, и становится непонятно почему его сборщик мусора не ловит.
p.s. Все тестирование использования памяти было выполнено в development билдах с подключением профайлера, как на ПК, так и на консоли.
Интересно, только я такое проморгал в своем проекте?)