目标 本篇博文的主要目的是讲解Unity项目如何进行内存管理。 当我们创建一个array(数组),string(字符串),object(对象),将会在一个叫做HEAP(堆)的中心池中分配内存。当这些东西很长时间没有使用,他们所占用的内存将被释放用以其他用处。以前,都是由程序员使用相关的函数调用明确地分配和释放堆上的内存。现在,像Unity的Mono开发引擎的内存管理机制自动地完成上述操作。自动内存管理机制比显示地分配/释放内存需要更少的代码,并且减少了内存泄露的风险。 当任何一个函数被调用,它的参数的值将会被拷贝到一段保留的内存区域。占有很少字节的一些数据类型可以很快且很容易地被拷贝。然而,我们知道数组,对象和字符串体积更大,经常拷贝这些数据是很低效率的。幸运的是,不是必须得这样的。我们在堆上分配了一个(体积小的)指针指向这个实际占用体积很大的数据,而堆上的这个值(指针)用来存储内存地址。所以在参数传递的过程中,仅仅需要拷贝这个指针就可以了。在实时系统运行时,被指针所指向的数据可以被定位到。函数每一次调用都可以使用数据的单向拷贝。 值类型是指那些在参数传递过程中直接拷贝和存储的类型。包括char,floats,integers,Booleans和Unity的结构类型(例如:Color和Vector3)。引用类型指的是那些数据存储在堆上并通过指针指向的类型。因为参数变量中的值仅仅是指向实际的数据。Strings,objects,arrays是引用类型的例子。 内存管理器持续地跟踪堆上那些没有使用的内存。当有新的内存请求,内存管理器会分配没有使用的空间。在后续的请求被处理被处理之前,这个过程会一直发生。堆中还在使用的内存不太可能被分配。只有当还有指针变量指向堆上的数据,这块数据才可用。如果所有指向堆数据的指针都被释放,那么这块堆的内存就可以安全地被再次分配使用了。 为了确定哪一块堆内存还没有被使用,内存管理器搜索所有激活的引用变量并标记他们为“live”。在搜索结束后,内存管理器把在那些标记为“live”的内存块之间的内存区域认为是空的并且可以为随后的内存分配所使用。这个定位和释放无用内存的过程被叫做垃圾回收(GC)。 垃圾回收不是手动的且对程序员是不可见的,但是垃圾回收的过程在后台是需要耗费大量的CPU时间。当准确地使用它时,在所有的性能方面,自动内存管理都能等于或者好于手动内存管理。然而,无论如何,开发者有必要避免一些错误,比如在执行过程中触发没必要的垃圾回收操作和暂停正在回收的操作。有一些第一眼看着没什么特别的算法其实是垃圾收集器的梦魇。 这里要知道的主要一点是新的部分不是被一个接一个地添加到字符串中。实际上,循环的每一次,myLine变量最后一个元素的内容销毁---一个完整的新的字符串被分配出来,包含了原始的部分并且在末尾加上了新的部分。因为随着变量i的增加,string体积也越来越大,这个值在堆上使用的空间也会增大,每次这个函数调用都很容易耗费几千个字节。为了连接字符串,可以用 System.Text.StringBuilder这个类。 无论如何,即使是重复的字符串连接也不会引起太大的麻烦,除非频繁地调用它。并且在Unity中这个操作通常在帧的更新函数中。 举例: 当Update函数被调用,每一次它都会分配一个新的字符串,并且会产生一定量的新的内存垃圾。其中这些垃圾的大部分可以被节省下来,仅仅当myScore改变的时,才会产生新的内存垃圾。 5.1 例子1 当函数返回一个数组时出现另一个问题: 当要给新的数组填充值时,这类函数是非常简洁和方便的。无论如何,如果它不断地被调用那么每次都会分配新的内存。因为这数组可以非常大。空闲的堆空间会因为重复的垃圾收集而被快速地耗尽。为了避免这个问题,我们利用数组是一个引用类型这一事实。当一个数组作为参数传入一个函数时可以在函数内被改变并且当函数返回时,结果会保留着。 5.2 例子2 这个函数简单地用最新的值替换数组中存在的值。虽然这要求在函数调用这段代码之前要给数组分配初始值的内存(这看起来比较简洁)。 垃圾回收正如前面提到的,应该尽可能地避免垃圾回收。虽然他们不能被完全消除,有2条策略可以在游戏中减少垃圾回收的执行。 这种策略适合那些操作性高的游戏,对于这些游戏来说,平滑的帧率是最重要的事情。这类游戏的典型特点是频繁分配小块内存,并且这些内存存在时间很短暂的。当在IOS设备上使用这个策略时,堆大小大约是200KB,在iPhone 3G上垃圾回收大约需要5ms的时间。如果堆大小增加到1MB,垃圾回收大约要7ms的时间。在一定的帧间隔请求一次垃圾回收是有好处的。这较之严格必须的垃圾回收更为频繁,但是这个过程会很快并且对于游戏是最小的代价。 无论如何,你使用这个技术,也要观察profiler统计数据,确保它真的有减少垃圾回收耗时。 5.4 缓慢且不频繁的垃圾回收(大的堆空间) 这个策略适合那些内存分配不频繁并且垃圾回收可以在暂停时被处理的游戏。堆空间竟可能地大是有用的,但是也不能太大而造成系统内存过低而被操作系统终止掉。无论如何,如果可能的话,Mono运行环境会自动地扩大堆空间。你可以在启动的时候预先分配一些占位符(例如,实例化一个无用的对象)来扩展堆空间,纯粹是为了在内存管理器分配对应的内存。 一个充分大的堆空间不应该在要调用垃圾回收的两个暂停间被填充满。当暂定时,你可以显式地调用一次垃圾回收: 再次强调,你应该注意当你使用这个策略时,要关注下profiler统计数据,而不是假想它会达到预期的效果。 有很多例子可以说明,我们可以通过减少对象的创建和销毁来简单地避免产生内存垃圾。一些游戏中的对象(比如子弹)可能会不断地出现虽然只有少部分的是有用的。像这样的例子,重用对象比销毁旧的再创建一个新的对象要好得多。 我希望这篇博文对你在Unity项目中开发内存管理这一块是有用的。如果你有问题,可以留言。我希望得到你的回应。 原文作者:Nikunj Popat |
|
来自: 阿修罗之狮猿授 > 《unity与Lua和C#通信》