UnityEngine.Object == null
Мне не так давно написал один из разработчиков на проекте, который столкнулся с проблемой зомби объектов.
Зомби объект — жив потому что GC не может его собрать, так как в стэке есть ссылка на него, но сам объект уже Destroyed.
И дело тут не в том, что
Такую ситуацию очень легко получить:
🔹Берем любой класс наследник MonoBehaviour
🔹 Реализуем в нем любой интерфейс
🔹 Instantiate'им объект, сохраняем в переменную
🔹 Во вторую переменную cast'им наш объект к интерфейсу
🔹 Уничтожаем MonoBehaviour
Через
🔹 Пытаемся вызвать поле или метод интерфейса
Вопросы:
🔸 Будет ли MonoBehaviour == null?
🔸 Будет ли переменная интерфейса == null?
🔸 Будет ли ошибка при вызове любого member'а интерфейса?
Подсказка
Ответы:
👍 Да
👎 Нет
❓ И да и нет.
Если вызваемый мембер не часть MonoBehavior имплементации - ошибки не будет
В остальных случаях будет MissingReferenceException
Почему так?
Потому что любой
🔹Одна на стороне unity (написанная на C++)
🔹Вторая на стороне CLR (Common Language Runtime) — класс/структура C#
Это значит:
🔸 CLRObject может смотреть уже на уничтоженный UnityObject ровно столько, сколько мы храним указатель на него
🔸 Если UnityObject уничтожен, мы все еще можем обратиться к его CLR части и взять оттуда любые данные, которые не является частью UnityObject
🔸 Момент сборки данного объекта GC может быть отложен на сколько угодно по времени
Это ведет к проблемам:
♦️ Утечки памяти по всему проекту.
UnityObject уничтожен и нам нужно удалить объект из коллекции, а мы не можем, потому что interface != null
4 возможных решения:
1️⃣ Проверять все экземпляры типа интерфейса методом:
2️⃣ Для абстракции ВСЕХ монобехов использовать только abstract классы
3️⃣ Наследовать все интерфейсы от
4️⃣ (самый быстрый, самый дерзкий)
Через
Вот как это примерно может выглядеть
Почему именно так:
🔸UnityObject содержит перегрузку оператора == и != которая проверят что нативная (C++ часть) "жива"
🔻Но в CLR части == транслируется в операцию
Или проще говоря в обычное сравнение 2ух указателей.
Такой проверки в случае реализации интерфейса в UnityObject не достаточно.
Потому мы получаем false-negative результат при сравнение на null, который потенциально ведет к утечкам памяти
Я создал Gist в котором расписал подробно TestCase'ы и решение.
Не стесняйтесь его дополнить или предложить свой вариант в комментариях😎
#будни@UniArchitect
Мне не так давно написал один из разработчиков на проекте, который столкнулся с проблемой зомби объектов.
Зомби объект — жив потому что 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