tgoop.com/unsafecsharp/215
Last Update:
UnityEngine.Object == null
Мне не так давно написал один из разработчиков на проекте, который застрял на проблеме зомби объектов.
Зомби объект — жив потому что GC не может его собрать, так как в стэке есть ссылка на него, но сам объект уже Destroyed.
И дело тут не в том, что GC.Collect еще не вызвался, а в том что мы храним указатель на объект, который уже уничтожен.
Такую ситуацию очень легко получить:
🔹Берем любой класс наследник MonoBehaviour
🔹 Реализуем в нем любой интерфейс
🔹 Instantiate'им объект, сохраняем в переменную
🔹 Во вторую переменную cast'им наш объект к интерфейсу
🔹 Уничтожаем MonoBehaviour
Через Object.Destroy или Object.DestroyImmediate
🔹 Пытаемся вызвать поле или метод интерфейса
Вопросы:
🔸 Будет ли MonoBehaviour == null?
🔸 Будет ли переменная интерфейса == null?
🔸 Будет ли ошибка при вызове любого member'а интерфейса?
Подсказ
Ответы:
👍 Да
👎 Нет
❓ И да и нет.
Если вызваемый мембер не часть MonoBehavior имплементации - ошибки не будет
В остальных случаях будет MissingRefere
Почему так?
Потому что любой UnityEngine.Object имеет 2 runtime части:
🔹Одна на стороне unity (написанная на C++)
🔹Вторая на стороне CLR (Common Language Runtime) — класс/структура C#
Это значит:
🔸 CLRObject может смотреть уже на уничтоженный UnityObject ровно столько, сколько мы храним указатель на него
🔸 Если UnityObject уничтожен, мы все еще можем обратиться к его CLR части и взять оттуда любые данные, которые не является частью UnityObject
🔸 Момент сборки данного объекта GC может быть отложен на сколько угодно по времени
Это ведет к проблемам:
♦️ Утечки памяти по всему проекту.
UnityObject уничтожен и нам нужно удалить объект из коллекции, а мы не можем, потому что interface != null
♦️MissingReferenceException, в неожиданных местах со вкусом фрустрации и сложной отладки
4 возможных решения:
1️⃣ Проверять все экземпляры типа интерфейса методом:
bool IsNullUniversal<T>(T instance)
{
if (instance is Object unityObject)
return unityObject == null;
return instance == null;
}
2️⃣ Для абстракции ВСЕХ монобехов использовать только abstract классы
3️⃣ Наследовать все интерфейсы от IEquatable<T> и использовать везде метод Equals вместо ==
4️⃣ (самый быстрый, самый дерзкий)
Через UnsafeUtility читать m_CachedPtr и m_InstanceID и через рефлексию дергать метод DoesObjectWithInstanceIDExist
Почему именно так:
🔸UnityObject содержит перегрузку оператора == и != которая проверят что нативная (C++ часть) "жива"
🔻Но в CLR части == транслируется в операцию seq, которая в после IL2CPP будет выглядеть так:
((((RuntimeObject*)instance) == ((RuntimeObject*)NULL))? 1 : 0)
Или проще говоря в обычное сравнение 2ух указателей.
Такой проверки в случае реализации интерфейса в UnityObject не достаточно.
Потому мы получается false-negative результат при сравнение на null, который потенциально ведет к утечкам памяти
Я создал Gist в котором расписал подробно TestCase'ы и решение.
Не стесняйтесь его дополнить или предложить свой вариант в комментариях 😎
Об архитектуре проектов на unity я пишу в своем канале, подписывайся❤️🔥
Специально для @unsafecsharp
BY Unity: Всё, что вы не знали о разработке
Share with your friend now:
tgoop.com/unsafecsharp/215