Unity内存管理

Unity的内存区域

Unity中内存主要分两块:Managed Space(Mono)和Native Space(Unity)

以下分别展开介绍

Managed Memory Space

即内存中由Mono管理的部分。这部分内存可以再细分成两块:栈和堆。

在讨论栈和堆之前,必须明确一个概念——变量的Value Type和Reference Type。

C#中变量分为值类型(Value Type)和引用类型(Reference Type)分别对应栈空间和堆空间上的变量。

以下这些属于值类型变量:

而这些属于引用类型变量:

值类型变量的特点:

相对的,引用类型变量的特点:

Mono Heap和GC机制

Mono Heap上的变量不会被自动释放,释放其内存的方法只有一个那就是GC。 GC释放的原理是引用计数。 即如果一块内存没有被任何地方引用,则会在下一次GC中被释放掉。 因此对于引用类型变量,将所有的引用设为null不会导致内存的立即释放,需要等到下一次GC。 而GC触发的时机是不规律的,经常是系统感觉内存不够用了才会触发,或者调用了包含System.GC.Collect()。 而一次GC因为要扫描堆上的所有对象,并且计算引用计数,所以会非常慢。 因此在Unity中触发GC的时机非常的有讲究。 频繁GC会导致卡顿、帧速下降等等问题。内存占用过高则比较容易导致游戏Crash。 因此对于Mono内存的优化主要在于控制GC的实际和频度。

Native Memory Space

即Unity保有的内存,一般包括:

简单来说,自己写的代码之外的占用的内存基本都是Unity来替我们管理的。 这部分内存不能够直接调用GC进行回收,但是Unity提供了一些方法让我们可以销毁掉不用的Native内存:

这些函数都包含回收Unity内存的部分。 但是注意它们的应用的场合不尽相同,在使用时需要仔细甄别。

Unity的内存管理技巧

针对Unity两部分内存的性质,大致有如下的内存管理技巧。

控制GC的调用时机

控制GC的时机主要是指防止GC被调用的过于频繁。一般来说,GC被过于频繁的触发都是因为Mono Heap上出现了过于频繁的创建对象的操作。 对此,有一些经典的策略可供参考。

对象池

对象池是指用SetActive操作代替Instantiate/Destory。即在对象“销毁”的时候并不调用Destory函数,而只是用SetActive将其设置为disable。 在“创建”的时候用SetActive再将其置为enable。这样对于大量存在创建-销毁对象的游戏会有不错的优化效果。

用struct代替class

对于反复创建、销毁的对象,还有一个方法就是使用struct代替class。因为struct使用的是栈内存,离开作用域之后立刻就会被回收,不会对GC产生任何影响。 Unity中的Vector3、Color等其实都是struct实现的。

在适当的时机主动触发GC

主动触发System.GC.Collect在大多数时候都不是一个好主意,但是也有一些例外的情况。 在不影响用户体验的前提下,主动触发GC可以防止GC发生在预期之外的地方。

防止Memory Spike

Unity内存管理中的另外一个话题是突然的内存增加(Memory Spike)导致游戏Crash。 这方面可用的策略比较灵活,这里简单介绍几个。

先弃后取

这个策略最经典的应用是在Scene Loading上边。 Unity的Scene Loading是先将Scene的内容加载进来,然后再释放掉之前Scene上的资源。 因此如果直接使用Application.LoadLevel之类的函数有可能会导致两个Scene的资源同时出现在内存中,此时非常容易Crash。 很多游戏在Scene切换的时候会引入一个Loading Scene,然后再加载下一个Scene,这样可以防止两个充满了资源的Scene的所有内容被同时加载到内存中。

化整为零

一般来说,加载巨大的单个资源比较容易出问题。 因此将单个的资源进行细分,根据当前的内存状况进行加载会使问题得到很大缓解。

适当复用

和对象池类似,频繁的对象创建有可能导致游戏在GC被触发之前就Crash,因此对于对内存的一定程度上的复用可以避免这类问题。 注意除了对象可以复用之外,数组也是可以复用的。