当我们来创建一个对象、字符串或数组时,我们需要从称为堆的中央池中为其分配内存来存储它。当它不再被使用时,我们又需要来释放这块内存便于重复使用。在以前这个过程通常需要我们通过适当的函数调用显式地分配和释放块内存来实现。但现在,运行时系统如Unity的mono引擎将自动地为我们管理内存。自动内存管理比显式分配/释放需要更少的编码工作,大大减少了内存泄漏的可能性(内存被分配但从来没有随后被释放的情况)。
对此首先要理解两个概念:值类型和引用类型
在方法调用的时候传递的参数,是直接复制到特定的内存区域,以便于方法中使用。如此一来不同的数据类型占用字节不同将很明显的影响到效率。为了避免这种情况的发生,故而出现两种传递方式。值类型通过值传递,引用类型通过引用传递。
在参数传递过程中直接存储和复制的类型称为值类型。包括整型、浮点型、布尔值和结构类型。在堆上分配的,然后通过指针访问的类型称为引用类型,因为存储在变量中的值仅仅是“引用”到实际数据。引用类型的示例包括对象、字符串和数组。
内存分配和垃圾回收
内存管理器跟踪堆中未使用的堆中的区域。当请求一个新的内存块(比如当一个对象实例化时),管理器选择一个未使用的区域来分配块,然后从已知的未使用空间中移除分配的内存。后续请求以相同的方式处理,直到没有足够大的空闲区域分配所需的块大小。不可能出现堆中分配的所有内存都在使用中的情况。只要还存在可以找到它的引用变量,就能访问堆上的引用项。如果所有引用的内存块都消失了(即参考变量被重新分配,或是超出了局部变量的范围)那么它所占用的内存可以被重新分配。
为了确定哪些堆块不再使用,内存管理器搜索所有当前活动的引用变量,并将它们称为“活”的块标记出来。在搜索结束时,内存管理器认为“活”的块之间的任何空间都是空的,可用于后续分配。查找和释放未使用内存的过程称为垃圾收集(简称GC)。
垃圾收集对我们来说是自动的、不可见的,但是收集过程实际上需要大量的CPU时间。如果正确使用,自动内存管理通常会等于或大于手动分配的整体性能。所以我们必须避免引起不比要的gc
例如
1.字符串的连接操作:
string line = intArray[0].ToString();
for (i = 1; i < intArray.Length; i++)
{
line += “, ” + intArray[i].ToString();
}
在每一次循环里都会重新分配内存空间,保存相加之后的字符串,如此一来,循环次数越多内存消耗就越大。
所以当有此需求时应尽量避免“+”操作来连接字符串,可以使用System.Text.StringBuilder类来实现。
更危险的是在unity的update里做字符串连接操作,因为update是每帧更新,所以内存消耗比for循环更大
2.方法返回数组的情况:
float[] RandomList(int numElements)
{
var result = new float[numElements];
for (int i = 0; i < numElements; i++)
{
result[i] = Random.value;
}
return result;
}
这样的方法来生成一个已赋值的数组是很常见,很方便的,但是如果重复调用这样的方法,每次调用都会重新分配新的内存。一旦数组很大,调用频繁的话,空闲的堆内存空间会立马耗尽,从而导致频繁的gc。为了避免这种情况的发生,我们可以利用数组是引用类型的特性,通过传递数组类型的一个参数,然后在方法里改变该数组的值,从而达到目的。修改后的方法如下:
void RandomList(float[] arrayToFill)
{
for (int i = 0; i < arrayToFill.Length; i++)
{
arrayToFill[i] = Random.value;
}
}
如上所述,我们要尽量避免新内存空间的分配,但是无论如何这是不可能完全消除的。我们可以通过两种方式来最小化内存分配带来的消耗
1.如果堆比较小,则进行快速而频繁的垃圾回收
这种方式适用于需要长时间运行,并且帧率稳定的游戏。会频繁的分配小块内存,短暂使用之后快速回收的情况下
可以使用下面的代码,虽然会导致gc的次数变多,但是每次占用的内存都只是小块,
所以垃圾回收也是消耗很小,对游戏的影响不大
if (Time.frameCount % 30 == 0)
{
System.GC.Collect();
}
2.如果堆比较大,则进行缓慢且不频繁的垃圾回收
这种方式适用于不需要频繁的分配和回收内存,并且可以在游戏停顿时处理的游戏。Mono运行时会尽可能地避免堆的自动扩大,所以可以在游戏一开始就分配一块大的内存预存。然后在游戏暂停的时候显示的调用System.GC.Collect()进行
void Start()
{
var tmp = new System.Object[1024];
for (int i = 0; i < 1024; i++)
tmp[i] = new byte[1024];
tmp = null;
}
注意:这两种做法都要慎用的,可以通过查看profiler确定这样做是否真的减少了垃圾收集的时间消耗
3.可重用的对象池
尽可能的减少对象的生成和销毁来避免垃圾生成。例如poolmanager
4.避免装箱操作
装箱:装箱就是隐式的将一个值型转换为引用型对象
int i=0;
Syste.Object obj=i;
装箱操作是非常普遍的一种产生内存垃圾的行为,应尽量避免
5.协程
yield在协程中不会产生堆内存分配,但是如果yield带有参数返回,则会造成不必要的内存垃圾
例如:
yield return 0;
可以用:
yield return null;
代替。
另外一种对协程的错误使用是每次返回的时候都new同一个变量
例如:
yield return new WaitForSeconds(1f);
可以用:
WaitForSeconds delay = new WaiForSeconds(1f);
yield return delay;
代替。
本文分享自微信公众号 - 游戏人的开发分享(No_2SeeYou)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。