了解自己主动内存管理
当创建对象、 字符串或数组时。从中央池中称为堆分配存储它所需的内存。该项目时不再使用。它一次占用的内存能够回收,并用于别的东西。在过去,它一般是由程序猿来分配和释放这些块堆内存使用适当的函数调用显式。现在,像统一的单引擎的执行时系统会自己主动为您管理内存。
自己主动内存管理须要少比显式分配/释放的编码工作,极大地降低了潜在的内存泄漏 (情况在哪里内存分配。但永远不会随后释放)。
值和引用类型
当一个函数被调用时。其參数的值拷贝到为这一详细要求保留的内存区域。能够复制占用仅仅有几个字节的数据类型。非常迅速和easy。然而,这是常见的对象、字符串和数组要大得多。假设这些类型的数据被复制在定期的基础上,它将会非常低效。
幸运的是。这不是必要的 。从堆分配一个大的项目的实际存储空间和一个小的"指针"值,用来记住它的位置。从那时起,仅仅有指针须要复制期间传递的參数。仅仅要执行时系统能够找到由指针标识的项。能够作为必要时常常使用数据的单个副本。
直接存储和复制期间參数传递的类型称为值类型。这些包含整数、 浮点数、 布尔值和统一的结构类型(比如,颜色和Vector3)。
在堆上分配,然后通过指针訪问的类型称为引用类型。由于仅仅是存储在变量中的值"是指"真实的数据。
引用类型的样例包含对象、 字符串和数组。
分配和垃圾回收
内存管理器跟踪的领域它明知是未使用的堆中。当一座新的内存请求时 (说当一个对象被实例化)时。经理选择要从中分配块未使用的区域,然后从已知未使用的空间中删除已分配的内存。
兴许请求的处理方式同样。直到没有自由的范围不够大。无法分配所需的块大小。在这一点上是极不可能从堆中分配的全部内存都都仍在使用。仅仅能訪问堆上的參考项目。仅仅要仍有能够找到它的引用变量。
假设指向的内存块的全部引用都都不见了(即,引用变量已被又一次分配或它们都是都现已超出范围的本地变量) 然后它占用的内存能够安全地又一次分配。
要确定哪堆块不再使用,内存管理器搜索全部当前活动的引用变量。并标志着他们称为"活着"的块。
在搜索结束后。不论什么活块之间的空间被觉得是空的内存管理器,能够用于兴许分配。原因非常明显,定位和释放未使用的内存的过程被称为垃圾收集(或简称 GC)。
优化
垃圾收集是自己主动与不可见的程序猿,但收集过程实际上须要大量的 CPU时间。在幕后。假设运用得当。自己主动内存管理通常将等于或击败手动分配,以总体的性能。然而。至关重要的是对于程序猿来说。避免错误。将触发比必要更常常收集器并介绍在执行暂停。
有一些臭名昭著的算法,能够是 GC的噩梦。虽然他们看起来无辜乍一看。反复字符串连接是一个经典的样例:-
function ConcatExample(intArray: int[]) {
??? var line = intArray[0].ToString();
???
????for (i = 1; i < intArray.Length; i++) {
??????? line += ", " + intArray[i].ToString();
??? }
???
????return line;
}
?
?
这里关键的细节是新片不会加入到地方中的字符串、 一个接一个。
究竟发生了什么是周围循环的每次行变量上以前的内容变得死寂了 — —一个全新的字符串分配包含原片加末尾的新部分。
由于字符串获取与添加值的我更长的时间,所用的堆空间正在消耗也添加,所以它是easy使用了数百个字节的可用堆空间的每次调用此函数。
假设您须要将很多字符串连接在一起更好的选择是单声道库System.Text.StringBuilder类。
然而,即使反复的串联不会造成太多的麻烦。除非它叫做频繁,并在通常意味着该框架的统一更新。就像:-
var scoreBoard: GUIText;
var score: int;
?
function Update() {
??? var scoreText: String = "Score: " + score.ToString();
??? scoreBoard.text = scoreText;
}
?
?
你何时分配新的字符串调用 Update时每次和生成新的垃圾不断淌出。大多数是能够通过更新文本,仅当比分更改时保存:-
var scoreBoard: GUIText;
var scoreText: String;
var score: int;
var oldScore: int;
?
function Update() {
??? if (score != oldScore) {
??????? scoreText = "Score: " + score.ToString();
??????? scoreBoard.text = scoreText;
??????? oldScore = score;
??? }
}
?
?
当一个函数返回数组值时发生的还有一个潜在的问题:-
function RandomList(numElements: int) {
??? var result = new float[numElements];
???
????for (i = 0; i < numElements; i++) {
??????? result[i] = Random.value;
??? }
???
????return result;
}
?
?
这样的类型是函数的非常优雅,交通便捷。当创建一个新数组,用值填充。
然而。假设它反复调用然后新奇内存将分配每次。由于数组能够是非常大,可用堆空间能够得到使用迅速上升。导致频繁的垃圾回收。若要避免此问题的一种方法是要使用的数组是一个引用类型的事实。能够在这个函数中改动成一个函数作为參数传递的数组和结果不会在函数返回后。
像上面常常被替换之类的功能:-
function RandomList(arrayToFill: float[]) {
??? for (i = 0; i < arrayToFill.Length; i++) {
??????? arrayToFill[i] = Random.value;
??? }
}
?
?
这仅仅是用新值替换现有数组的内容。虽然这须要初始分配的数组必须在调用代码中 (这看起来有点不雅)。该函数将不会产生不论什么新的垃圾。当它被调用时。
请求集合
如上文所述,它最好尽量避免分配。
只是,既然他们不能全然消除,但也有两个基本的策略,你能够使用尽量降低它们侵入游戏:-
具有高速和频繁的垃圾回收的小堆
这样的策略往往是游戏的最好的有长时间在哪里光滑的帧速率是游戏的主要关注的游戏。
这样的比赛一般会频繁地分配小块,但这些块将仅仅简要地被使用。在 ios上使用这样的策略时的典型堆大小是大约 200 KB,垃圾回收会约5ms的 iPhone 3g。
假设堆添加到 1 MB时,该集合将约 7ms。因此。它能够有利于有时要求普通帧间隔的垃圾回收。这一般会使集合比严格必需更常常发生。但他们将处理速度快、 影响最小的游戏:-
if (Time.frameCount % 30 == 0)
{
?? System.GC.Collect]();
}
?
?
然而,你应该慎重使用这样的技术,检查事件探查器统计信息,以确保它真的降低收集时间为你的游戏。
大堆与缓慢但非常少发生垃圾回收
这一战略适合的游戏拨款 (和集合) 是相对较少。能够在游戏暂停期间处理。它是用于堆是一样大的而不是如此之大,让您的应用程序由于系统内存不足 OS被杀害。然而。单声道的执行时避免扩大堆自己主动假设可能的话。您能够通过在启动过程中预一些占位符空间手动扩展堆 (即您实例化一个纯粹的影响,内存管理器分配的"无用"对象):-
function Start() {
??? var tmp = new System.Object[1024];
?
??? // make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocks
??????? for (var i : int = 0; i < 1024; i++)
??????? tmp[i] = new byte[1024];
?
??? // release reference
??????? tmp = null;
}
?
?
?
一个足够大的堆应该不得到全然填满那些暂停游戏。能够容纳一个集合之间。这样的停顿时,您能够显式请求集合:-
System.GC.Collect();
?
?
再次。你应该照应使用此策略时,注意到探查器统计信息而不仅仅在假设它有预期的效果。
可重用的对象池
有非常多情况下。在那里您能够避免生成垃圾仅通过降低创建和销毁的对象的数目。有某些类型的对象在游戏中,如子弹头。可能会遇到几遍,即使仅仅有一小部分以前将播放一次。在这样的情况下,非常有可能要重用的对象,而不是摧毁旧的并替换为新的。
进一步的信息
内存管理是微妙和复杂须大量的学术努力一直致力。假设你有兴趣学习很多其它有关它memorymanagement.org是一种优秀的资源,列出了很多出版物和在线文章。在Sourcemaking.com上维基百科的页面。能够找到有关对象池的进一步信息.
?