0%

Unity中空判断与Destroy实现

以往我在做空判断的时候,都是这样写的:

1
if (gameobject != null)

或者是

1
if (gameobject == null) return;

最近换了Rider, 他给我来了句提示与 'null' 的比较开销较大,奇奇怪怪,我换成了 if(gameobject) 就没有这些提示了,那么这两种写法到底有什么区别呢?

要讨论这两个问题,还真不是很简单,我们首先要了解Object,这里的ObjectSystem.Object,也是UnityEngine.Object

System.Object大家都很好理解,我们来看看UnityEngine.Object吧。

UnityEngine.Object继承自system.Object,是Unity所涉及所有物体的基类。

但是他们也有区别,不得不谈的就是销毁空判断

这是最常见的问题,在UnityEngine中我们销毁一个对象时,使用“==”操作符与null比较结果为true,但是可以发现,我们还是可以引用这个变量,这主要是由于UnityEngine的==操作符在销毁后即将Object置位null,但是对象本身需要被GC后才能达到真正意义上的销毁,也就是System.Object中的null;

这里涉及到了托管的问题,如果不了解也不要紧,我可以告诉你从 UnityEngine.Object 继承的对象,包括托管和非托管两部分,当调用 Destroy 时,销毁的只是非托管部分,托管部分只能通过 C# 的垃圾回收器进行回收。

什么意思呢,我们直接来看Destroy会更清晰一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// <para>Removes a GameObject, component or asset.</para>
/// </summary>
/// <param name="obj">The object to destroy.</param>
/// <param name="t">The optional amount of time to delay before destroying the object.</param>
[NativeMethod(Name = "Scripting::DestroyObjectFromScripting", IsFreeFunction = true, ThrowsException = true)]
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void Destroy(Object obj, [DefaultValue("0.0F")] float t);

/// <summary>
/// <para>Removes a GameObject, component or asset.</para>
/// </summary>
/// <param name="obj">The object to destroy.</param>
/// <param name="t">The optional amount of time to delay before destroying the object.</param>
[ExcludeFromDocs]
public static void Destroy(Object obj)
{
float t = 0.0f;
Object.Destroy(obj, t);
}

Unity 自己的文档讲的很清楚了, 我们来看“了解托管堆”这一节

垃圾回收器定期运行(__注意:__具体运行时间视平台而定)。这时将扫描堆上的所有对象,将任何不再引用的对象标记为删除。然后会删除未引用的对象,从而释放内存。

Unity 用了一种很讨巧的方法,他们把 destroy 掉的内存标记为 'null',事实上,这些内存并没有被回收,而是在等待 C# 的 GC 把他们回收掉。

我们再来看看Unity对于==的重载

1
2
3
public static bool operator ==(Object x, Object y) => Object.CompareBaseObjects(x, y);

public static bool operator !=(Object x, Object y) => !Object.CompareBaseObjects(x, y);

还有其对布尔类型的重载

1
public static implicit operator bool(Object exists) => !Object.CompareBaseObjects(exists, (Object) null);

可以发现CompareBaseObjects方法的实现也是绕不开的,我们来看看

1
2
3
4
5
6
7
8
9
10
private static bool CompareBaseObjects(Object lhs, Object rhs)
{
bool flag1 = (object) lhs == null;
bool flag2 = (object) rhs == null;
if (flag2 & flag1)
return true;
if (flag2)
return !Object.IsNativeObjectAlive(lhs);
return flag1 ? !Object.IsNativeObjectAlive(rhs) : lhs.m_InstanceID == rhs.m_InstanceID;
}

看到这里,其实我们已经对之前的问题有了回答了

UnityEngine.Object中的布尔类型重载和==运算符重载,他们都是通过CompareBaseObjects来实现的
他们俩本质上都是采用了 Unity 层面上的检查。

对于这点,我们也可以证明

请看方法IsNativeObjectAlive

1
2
3
4
5
6
private static bool IsNativeObjectAlive(Object o)
{
if (o.GetCachedPtr() != IntPtr.Zero)
return true;
return !(o is MonoBehaviour) && !(o is ScriptableObject) && Object.DoesObjectWithInstanceIDExist(o.GetInstanceID());
}

Unity 先调用o.GetCachedPtr() 方法检查目标对象的本机指针是否还存在, 要知道 IntPtr 并非 Unity 自己鼓捣出来的玩意, 他是.NET Framework.NET Core中的一个特殊的结构体,用于表示目标对象的本地指针. 如果本地指针不存在,那么就会调用Object.DoesObjectWithInstanceIDExist方法,这个方法会检查目标对象的实例ID是否存在,如果存在具有给定实例ID的对象,则返回 true.

通过这种方法实现的空判断,实质上开销是特别大的,有些人会使用System.Object.ReferenceEquals,比如这样

1
2
3
4
if(System.Object.ReferenceEquals(gameobject, null))
{
// TODO something
}

这样做,在性能上确实是有很大提升的,但是读到这里的朋友估计已经猜到我要讲什么了.
在执行destroy方法后,目标对象实质上并没有被回收,他只是被打上了一个'tag'的标签,而变量仍然在指向着堆对象.为什么==可以呢,因为Unity已经把他重载了.


欢迎关注我的其它发布渠道