A闪的 BLOG 技术与人文
记录一个Unity内存泄漏的Tips。一个很小的点,但开发中极其容易被忽略。
当我们用C#创建一个“原生”对象时,Unity实际上会存在“两套”内存。一部分是我们所使用的C#托管代码内存(Managed Shell),另外一部分是Native层所创建的实际数据内存。
Texture2D tex;
void Start()
{
tex = new Texture2D(128, 128);
Destroy(tex);
}
上面这段代码中,tex
是托管代码内存,而Native层会有一份“原生”的 Texture2D
数据。当我们执行 Destroy(tex)
的时候,实际上是将Native层中的内存清除掉了。而托管层的 tex
并没有被清除。如果此时你打印 tex == null
,则会发现结果为 true
。你可能会感觉奇怪,命名是 null
,那么所谓的内存泄漏在哪里呢?
请看Unity针对 Object
对象的一段神奇代码。
public static bool operator ==(Object x, Object y) => Object.CompareBaseObjects(x, y);
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;
}
private static bool IsNativeObjectAlive(Object o)
{
if (o.GetCachedPtr() != IntPtr.Zero)
return true;
return !(o is MonoBehaviour) && !(o is ScriptableObject) && Object.DoesObjectWithInstanceIDExist(o.GetInstanceID());
}
好了,可以理解为当Native层的对象指针为空时,确实托管对象可以理解为 null
。
通过内存分析器中,我们可以通过查询来看到这些泄漏的托管对象
。如下图:
带有Leaked Managed Shell
标记的对象则属于上面这种情况的内存泄漏。你可能会觉得这些内存泄漏所占用的内存并不大,每个对象仅64B。但请不要忘记,这些泄漏的对象一旦存在其他“大体积”的对象引用,则会发生连锁反应。会造成更多的内存泄漏。
由于Leaked Managed Shell
类型的泄漏对象,对于C#的垃圾回收机而言属于活跃使用中的对象,所以并不会被识别为待回收对象。随着生命周期的推移,其结果可想而知。
Unity官方对于这种设计在14年是有一篇Blog(Custom == operator, should we keep it?)进行解释和记录的,但10年过去了,这种隐疾依然没有更好的解决办法。
避免这种内存泄漏的解决办法也非常简单,我们只需要在代码中显示的将托管对象设置为 null
即可。像这样,
void Start()
{
tex = new Texture2D(128, 128);
Destroy(tex);
tex = null;
}
最后,如果你想更多的了解C#的垃圾回收相关知识,推荐查阅垃圾回收的基本知识。