UnityEngine.Object == null



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



Зомби объект — жив потому что GC не может его собрать, так как в стэке есть ссылка на него, но сам объект уже Destroyed.



И дело тут не в том, что GC.Collect еще не вызвался, а в том что мы храним указатель на объект, который уже уничтожен.



Такую ситуацию очень легко получить:

🔹Берем любой класс наследник MonoBehaviour

🔹 Реализуем в нем любой интерфейс

🔹 Instantiate'им объект, сохраняем в переменную

🔹 Во вторую переменную cast'им наш объект к интерфейсу

🔹 Уничтожаем MonoBehaviour

Через Object.Destroy или Object.DestroyImmediate

🔹 Пытаемся вызвать поле или метод интерфейса



Вопросы:

🔸 Будет ли MonoBehaviour == null?

🔸 Будет ли переменная интерфейса == null?

🔸 Будет ли ошибка при вызове любого member'а интерфейса?



Подсказка



Ответы:

👍 Да

👎 Нет

И да и нет.

Если вызваемый мембер не часть MonoBehavior имплементации - ошибки не будет

В остальных случаях будет MissingReferenceException




Почему так?

Потому что любой 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 UnityEngine.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'ы и решение.

Не стесняйтесь его дополнить или предложить свой вариант в комментариях 😎



#будни@UniArchitect