当建立对象、字符串或数组时,存储它所需的内存将从称为堆的中央池中分配。当项目再也不使用时,它曾经占用的内存能够被回收并用于别的东西。在过去,一般由程序员经过适当的函数调用明确地分配和释放这些堆内存块。现在,像Unity的Mono引擎这样的运行时系统会自动为您管理内存。自动内存管理须要比显式分配/释放更少的编码工做,并大大下降内存泄漏(内存被分配但从未随后释放的状况)的可能性。html
当调用一个函数时,它的参数值将被复制到一个保留特定调用的内存区域。只占用几个字节的数据类型能够很是快速方便地复制。然而,对象、字符串和数组要大得多,若是这些类型的数据被按期复制,那将是很是低效的。幸运的是,这是没必要要的;大项目的实际存储空间是从堆中分配的,一个小的“指针”值用来记住它的位置。从那时起,只有指针在参数传递过程当中须要被复制。只要运行时系统可以定位指针标识的项,就能够常用数据的一个副本。
在参数传递期间直接存储和复制的类型称为值类型。这些包括整数,浮点数,布尔和Unity的结构类型(例如Color和Vector3)。分配在堆上而后经过指针访问的类型称为引用类型,由于存储在变量中的值仅仅是“引用”到真实数据。引用类型的示例包括对象,字符串和数组。git
内存管理器跟踪它知道未被使用的堆中的区域。当请求一个新的内存块时(例如当一个对象被实例化时),管理器选择一个未使用的区域,从中分配该块,而后从已知的未使用的空间中移除分配的内存。后续请求以相同的方式处理,直到没有足够大的空闲区域分配所需的块大小。在这一点上,从堆中分配的全部内存仍然在使用中是很是不可能的。只要还存在能够找到它的引用变量,就只能访问堆上的引用项。若是对内存块的全部引用都消失了(即,引用变量已被从新分配,或者它们是如今超出范围的局部变量),则它占用的内存能够安全地从新分配。
为了肯定哪些堆块再也不被使用,内存管理器会搜索全部当前活动的引用变量,并将它们所指的块标记为live
。在搜索结束时,内存管理器认为这些live
块之间的任何空间都是空的,而且可用于后续分配。因为显而易见的缘由,定位和释放未使用的内存的过程被称为垃圾回收(或简称GC)。程序员
垃圾收集对程序员来讲是自动的、不可见的,可是收集过程实际上须要大量的CPU时间。若是正确使用,自动内存管理一般会等于或击败手动分配的总体性能。可是,对于程序员来讲,重要的是要避免那些比实际须要触发更屡次收集器和在执行中引入暂停的错误。有一些臭名昭著的算法,多是GC噩梦,尽管他们乍一看是无辜的。重复字符串链接是一个典型的例子:github
//C# script example using UnityEngine; using System.Collections; public class ExampleScript : MonoBehaviour { void ConcatExample(int[] intArray) { string line = intArray[0].ToString(); for (i = 1; i < intArray.Length; i++) { line += ", " + intArray[i].ToString(); } return line; } } //JS script example function ConcatExample(intArray: int[]) { var line = intArray[0].ToString(); for (i = 1; i < intArray.Length; i++) { line += ", " + intArray[i].ToString(); } return line; }
这里的关键细节是,新的部分不会被一个接一个地添加到字符串中。实际状况是,每次循环line
变量的前一个内容都会变死——一个完整的新字符串被分配到包含原来的部分,再在最后加上新的部分。因为字符串随着i
值的增长而变得更长,因此所消耗的堆空间数量也增长了,所以每次调用这个函数时都很容易消耗数百字节的空闲堆空间。若是你须要链接多个字符串,那么一个更好的选择是Mono库的System.Text.StringBuilder类。然而,即便反复链接也不会引发太多麻烦,除非它被频繁调用,而在Unity中一般意味着帧更新。就像是:算法
//C# script example using UnityEngine; using System.Collections; public class ExampleScript : MonoBehaviour { public GUIText scoreBoard; public int score; void Update() { string scoreText = "Score: " + score.ToString(); scoreBoard.text = scoreText; } } //JS script example var scoreBoard: GUIText; var score: int; function Update() { var scoreText: String = "Score: " + score.ToString(); scoreBoard.text = scoreText; }
...每次调用Update将分配新字符串,并不断生成的新垃圾。大多数状况下,只有当分数变化时才更新文本:数组
//C# script example using UnityEngine; using System.Collections; public class ExampleScript : MonoBehaviour { public GUIText scoreBoard; public string scoreText; public int score; public int oldScore; void Update() { if (score != oldScore) { scoreText = "Score: " + score.ToString(); scoreBoard.text = scoreText; oldScore = score; } } } //JS script example 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; } }
当函数返回数组值时,会发生另外一个潜在的问题:安全
//C# script example using UnityEngine; using System.Collections; public class ExampleScript : MonoBehaviour { float[] RandomList(int numElements) { var result = new float[numElements]; for (int i = 0; i < numElements; i++) { result[i] = Random.value; } return result; } } //JS script example function RandomList(numElements: int) { var result = new float[numElements]; for (i = 0; i < numElements; i++) { result[i] = Random.value; } return result; }
当建立一个充满值的新数组时,这种函数很是优雅和方便。可是,若是反复调用,那么每次都会分配新的内存。因为数组可能很是大,可用空间可能会迅速消耗,从而致使垃圾收集频繁。避免这个问题的一个方法是利用数组是引用类型的事实。做为参数传递给函数的数组能够在该函数内修改,结果将在函数返回后保留。
像上面这样的功能一般能够被替换成:dom
//C# script example using UnityEngine; using System.Collections; public class ExampleScript : MonoBehaviour { void RandomList(float[] arrayToFill) { for (int i = 0; i < arrayToFill.Length; i++) { arrayToFill[i] = Random.value; } } } //JS script example function RandomList(arrayToFill: float[]) { for (i = 0; i < arrayToFill.Length; i++) { arrayToFill[i] = Random.value; } }
这只是用新值替换数组的现有内容。虽然这须要在调用代码中完成数组的初始分配(这彷佛有些不雅),可是在调用该函数时不会产生任何新的垃圾。函数
如上所述,最好尽可能避免分配。然而,鉴于它们不能被彻底消除,您可使用两种主要策略来最大限度地减小其入侵游戏:性能
这个策略一般最适合长期游戏的游戏,其中平滑的帧速率是主要的关注点。这样的游戏一般会频繁地分配小块,但这些块将仅在短期内使用。在iOS上使用此策略时,典型的堆大小约为200KB,iPhone 3G上的垃圾收集大约须要5ms。若是堆增长到1MB,则收集大约须要7ms。所以,有时候能够以规则的帧间隔请求垃圾回收。这一般会使垃圾收集发生的次数比严格的须要的更多,可是它们将被快速处理,对游戏的影响最小:
if (Time.frameCount % 30 == 0) { System.GC.Collect(); }
可是,您应该谨慎使用此技术,并检查profiler统计信息,以确保它真正减小了游戏的收集时间。
这个策略对于分配(和所以收集)相对不频繁并能够在游戏暂停期间处理的游戏最适用。对于堆来讲,尽量大,而不是由于系统内存太少而致使操做系统杀死你的应用程序。可是,若是可能,Mono运行时会自动避免扩展堆。您能够经过在启动期间预先分配一些占位符空间来手动扩展堆(即,您实例化一个纯粹用于对内存管理器产生影响的“无用”对象):
//C# script example using UnityEngine; using System.Collections; public class ExampleScript : MonoBehaviour { void 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 (int i = 0; i < 1024; i++) tmp[i] = new byte[1024]; // release reference tmp = null; } } //JS script example 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();
一样,在使用此策略时应该当心,并注意Profiler统计数据,而不是仅仅假定它具备所指望的效果。
不少状况下,只要减小建立和销毁对象的数量,就能够避免生成垃圾。游戏中存在着某些类型的物体,如抛射体,尽管一次只会有少许的物体在游戏中,但它们可能会被反复地遇到。在这种状况下,经常能够重用对象,而不是破坏旧对象,并用新的对象替换它们。
内存管理是一个微妙而复杂的课题,它已经投入了大量的学术研究。若是您有兴趣了解更多信息,那么memorymanagement.org是一个很好的资源,列出了许多出版物和在线文章。有关对象池的更多信息能够在维基百科页面和Sourcemaking.com上找到。
本文做者: Sheh伟伟
本文连接: http://davidsheh.github.io/2017/07/13/「翻译」理解Unity的自动内存管理/
版权声明: 本博客全部文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!