介绍
当我们的游戏运行时,它使用内存来存储数据。当不再需要该数据时,存储该数据的内存将被释放,以便可以重用。垃圾是用来存储数据但不再使用的内存的术语。垃圾回收是该内存再次可用以进行重用的进程的名称。
Unity使用垃圾回收作为管理内存的一部分。如果垃圾回收发生得太频繁或者有太多工作要做,我们的游戏可能会表现不佳,这意味着垃圾回收是导致性能问题的常见原因。
在本文中,我们将了解垃圾回收如何工作的,什么时候发生垃圾回收,以及如何有效地使用内存,从而最小化垃圾回收对游戏的影响。
诊断垃圾回收的问题
垃圾回收导致的性能问题可以表现为帧率低、性能不稳定或间歇性冻结。然而,其他问题也会引起类似的症状。如果我们的游戏有这样的性能问题,我们应该做的第一件事就是使用Unity的Profiler窗口来确定我们看到的问题是否真的是由于垃圾回收造成的。
要了解如何使用Profiler窗口查找性能问题的原因,请查阅这一篇教程。
Unity内存管理简介
要理解垃圾回收是如何工作的,以及垃圾回收何时发生,我们必须首先理解Unity中内存的使用是如何工作的。首先,我们必须理解Unity在运行它自己的核心引擎代码和运行我们编写的代码时使用了不同的方法。
Unity在运行自己的核心Unity引擎代码时管理内存的方式叫做手动内存管理。这意味着核心引擎代码必须显式地声明如何使用内存。手动内存管理不使用垃圾回收,本文将不再深入讨论。
Unity在运行我们的代码时管理内存的方式叫做自动内存管理。这意味着我们的代码不需要显式地告诉Unity如何以一种详细的方式管理内存。Unity帮我们解决了这个问题。
在最基本的层面上,Unity中的自动内存管理是这样工作的:
- Unity可以访问两个内存池:栈(stack)和堆(heap,也称为托管堆)。栈用于短期存储小块数据,堆用于长期存储和大块数据。
- 当一个变量被创建时,Unity会从栈或堆中请求一个内存块。
- 只要变量在作用域内(仍然可由代码访问),分配给它的内存就会一直使用。我们说这个内存已经分配了。我们将栈内存中保存的变量描述为栈上的对象,将堆内存中保存的变量描述为堆上的对象。
- 当变量超出作用域时,内存不再需要,可以将其返回到它所来自的池中。当内存被返回到它的池时,我们说内存已被释放。栈中内存在它引用的变量超出作用域时立即释放。但是,堆中的内存此时没有释放,并且仍然处于已分配的状态,即使它引用的变量超出了作用域。
- 垃圾回收器标识和释放未使用的堆内存。垃圾回收器定期运行以清理堆。
现在我们已经了解了事件流,接下来我们进一步了解栈内存分配和释放与堆分配和释放的区别。
栈分配和释放期间发生了什么?
栈分配和释放是快速和简单的。这是因为栈仅用于短时间内存储小数据。分配和回收总是以可预测的顺序发生,并且具有可预测的大小。
栈的工作方式类似于栈数据类型:它是元素的简单集合,在本例中是内存块,其中元素只能按照严格的顺序添加和删除。这种简单性和严格性使得它如此快速:当一个变量存储在栈上时,它的内存只是从栈的“末端”分配的。当栈变量超出作用域时,用于存储该变量的内存将立即返回栈以供重用内存。
堆分配期间发生了什么?
堆分配比栈分配复杂得多。这是因为堆可以用来存储长期和短期数据,以及许多不同类型和大小的数据。分配和回收并不总是按照可预测的顺序发生,可能需要许多不同大小的内存块。
创建变量时,执行以下步骤:
- 首先,Unity必须检查堆中是否有足够的空闲内存。如果堆中有足够的空闲内存,则为变量分配内存。
- 如果堆中没有足够的空闲内存,Unity会触发垃圾回收器,试图释放未使用的堆内存。这可能是一个缓慢的操作。如果堆中现在有足够的空闲内存,则分配变量的内存。
- 如果垃圾回收后堆中没有足够的空闲内存,Unity会增加堆中的内存容量。这可能是一个缓慢的操作。然后分配变量的内存。
堆分配可能很慢,特别是在必须运行垃圾回收器和必须扩展堆的情况下。
垃圾回收期间发生了什么?
当堆变量超出作用域时,用于存储它的内存不会立即释放。未使用的堆内存仅在垃圾回收器运行时释放。
- 垃圾回收器检查堆上的每个对象。
- 垃圾回收器搜索所有当前对象引用,以确定堆上的对象是否仍在作用域内。
- 任何不再在作用域中的对象都被标记为删除。
- 删除标记的对象,并将分配给它们的内存返回堆中。
垃圾回收是一项耗性能的操作。堆上的对象越多,它必须做的工作就越多,代码中的对象引用越多,它必须做的工作就越多。
垃圾回收什么时候发生?
有三种情况会导致垃圾回收器运行:
- 每当请求使用堆中的空闲内存无法完成堆分配时,垃圾回收器就会运行。
- 垃圾回收器不时自动运行(尽管频率随平台而变化)。
- 垃圾回收器可以强制手动运行。
垃圾回收是一种常见的操作。每当无法从可用堆内存中完成堆分配时,就会触发垃圾回收器,这意味着频繁的堆分配和回收会导致频繁的垃圾回收。
垃圾回收的问题
现在我们已经了解了垃圾回收在Unity内存管理中的作用,我们可以考虑可能发生的问题的类型。
最明显的问题是垃圾回收器可能需要相当长的事件来运行。如果垃圾回收器在堆上有很多对象和/或有对象引用要检查,那么检查所有这些对象的过程可能会很慢。这可能导致我们的游戏卡顿或者缓慢。
另一个问题是垃圾回收器可能在不合适的时间运行。如果CPU已经在游戏的性能关键部分努力运行着,那么即使垃圾回收带来的少量额外开销也会导致帧率下降和性能显著变化。
另一个不太明显的问题是堆碎片。当从堆中分配内存时,根据必须存储的数据的大小,内存以不同大小的块从空闲空间中获取。当这些内存块被返回到堆中时,堆可以被分割成许多由分配的块分隔的小的空闲块。这意味着,尽管空闲内存的容量可能很高,但是我们无法在不运行垃圾回收器和/或扩展堆的情况下分配内存块,因为现有的块都不够大。
碎片堆有两个后果。第一,我们的游戏内存使用量将高于它所需要的水平,第二,垃圾回收器将运行得更频繁。有关堆碎片得更详细讨论,请参考这一篇文章。
发现堆分配
如果我们知道垃圾回收在我们的游戏中造成了问题,我们就需要知道代码的哪些部分正在生成垃圾。垃圾是在堆变量超出作用域时生成的,因此首先我们需要知道是什么原因导致在堆上分配变量。
在栈和堆上分配了什么?
在Unity中,值类型的局部变量被分配到栈上,其他的变量被分配到堆上。如果你不确定Unity中值类型和引用类型之间的区别,请参阅本教程。
下面的代码是栈分配的一个示例,因为变量localInt既是本地的又是值类型的。为该变量分配的内存将在该函数完成运行后立即从栈中释放。
void ExampleFunction() { int localInt = 5; }
下面的代码是堆分配的一个示例,因为变量localList是本地的,但是是引用类型。为该变量分配的内存将在垃圾回收器运行时回收。
void ExampleFunction() { List localList = new List(); }
使用Profiler窗口查找堆分配
我们可以在Profiler窗口中看到代码创建的堆分配。
在选择了CPU usage Profiler之后,我们可以在Profiler窗口的底部选择任何帧来查看关于该帧的CPU使用率数据。其中一系列数据称为GC alloc。这一列显示在该帧中进行的堆分配。如果我们选择列标题,我们可以通过这个统计数据对数据进行排序,这样就很容易看到游戏中哪些函数导致了最多的堆分配。一旦我们知道哪个函数导致堆分配,就可以检查这个函数。
一旦我们知道函数中的哪些代码导致生成垃圾,我们就可以决定如何解决这个问题并最小化生成垃圾数量。
减少垃圾回收的影响
一般来说,我们可以通过以下三种方式减少垃圾回收对游戏的影响:
- 我们可以减少垃圾回收器运行的时间。
- 我们可以减少垃圾回收器运行的频率。
- 我们可以故意触发垃圾回收器,使其在性能不重要的时候运行(例如在加载屏幕期间)。
考虑到这一点,有三种策略可以帮助我们:
- 我们可以阻止我们游戏,这样我们就有更少的堆分配和更少的对象引用。堆上的对象越少,要检查的引用越少,这意味着在触发垃圾回收时,运行垃圾回收所需的时间越少。
- 我们可以减少堆分配和回收的频率,特别是在性能关键时候。更少的分配和回收意味着触发垃圾回收的情况更少。这也降低了堆碎片的风险。
- 我们可以尝试计时垃圾回收和堆扩展,以便在可预测和方便的时间进行。这是一种更困难、更不可靠的方法,但是如果将其作为整体内存管理策略的一部分使用,则可以减少垃圾回收的影响。
减少垃圾的创建
让我们研究一些技术,它们将帮助我们减少代码生成的垃圾数量。
缓存
如果我们的代码重复调用导致堆分配的函数,然后丢弃结果,这会产生不必要的垃圾。相反,我们应该存储对这些对象的引用并重用它们。这种技术称为缓存。
在下面的示例中,每次调用代码时,代码都会导致堆分配。这是因为创建了一个新数组。
void OnTriggerEnter(Collider other) { Renderer[] allRenderers = FindObjectsOfType<Renderer>(); ExampleFunction(allRenderers); }
下面的代码只会导致一个堆分配,因为数组只会创建和填充一次,然后缓存。缓存的数组可以一次又一次地重用,而不会产生更多的垃圾。
private Renderer[] allRenderers; void Start() { allRenderers = FindObjectsOfType<Renderer>(); } void OnTriggerEnter(Collider other) { ExampleFunction(allRenderers); }
不要在频繁调用的函数中进行分配
如果我们必须在Monobehaviour分配堆内存,最糟糕的情况是在频繁运行的函数中。例如,Update()和LateUpdate()在每一帧中调用一次,因此如果我们的代码在这里生成垃圾,那么它会很快地累积起来。如果可能,我们应该考虑在Start()或Awake()中缓存对对象的引用,或者确保只在需要时才运行导致分配的代码。
void Update() { ExampleGarbageGeneratingFunction(transform.position.x); }
通过一个简单的更改,我们现在确保仅在transform.position.x的值已经改变的情况下才调用分配函数。我们现在只在需要的时候进行堆分配,而不是每一帧中进行。
private float previousTransformPositionX; void Update() { float transformPositionX = transform.position.x; if (transformPositionX != previousTransformPositionX) { ExampleGarbageGeneratingFunction(transformPositionX); previousTransformPositionX = transformPositionX; } }
减少Update()中生成的垃圾的另一种技术是使用计时器。这适用于当我们生成垃圾的代码必须定期运行,但不一定是每一帧。
在下面的示例代码中,生成垃圾的函数每帧运行一次:
void Update() { ExampleGarbageGeneratingFunction(); }
在下面的代码中,我们使用计时器来确保生成垃圾的函数每秒运行一次。
private float timeSinceLastCalled; private float delay = 1f; void Update() { timeSinceLastCalled += Time.deltaTime; if (timeSinceLastCalled > delay) { ExampleGarbageGeneratingFunction(); timeSinceLastCalled = 0f; } }
对频繁运行的代码进行这样的小更改时,可以大大减少生成的垃圾数量。
清除集合
创建新的集合会导致堆上的分配。如果我们发现在代码中不止一次地创建新集合,我们应该缓存对该集合的引用,并使用Clear()清空其内容,而不是重复调用new。
在下面的示例中,每次使用new时都会发生新的堆分配。
void Update() { List myList = new List(); PopulateList(myList); }
在下面的示例中,分配仅在创建集合或必须在幕后调整集合大小时发生。这大大减少了生成的垃圾数量。
private List myList = new List(); void Update() { myList.Clear(); PopulateList(myList); }
对象池
即使我们在代码中减少了分配,但是如果我们在运行时创建和销毁大量对象,我们仍然可能会遇到垃圾回收的问题。对象池是一种通过重用对象而不是重复创建和销毁 对象来减少分配和回收的技术。对象池在游戏中应用广泛,最适合我们频繁生成和销毁相似对象的情况;例如,从枪里射出子弹时。
关于对象池的完整说明超出了本文的范围,但它确实是一种有用的技术,值得学习。Unity学习网站上关于对象池的教程在这里。
不必要的堆分配的常见原因
我们知道,本地的、值类型的变量是在栈上分配的,其他的都是在堆上分配的。然而,在许多情况下,堆分配可能会让我们大吃一惊。让我们看看造成不必要堆分配的一些常见原因,并考虑如何最好地减少这些原因。
字符串(strings)
在C#中,字符串是引用类型而不是值类型,即使它们似乎保存了字符串的“值”。这意味着创建和丢弃字符串都会产生垃圾。由于字符串在很多代码中都是常用的,所以这些垃圾实际上是可以累加的。
C#中的字符串也是不可变的,这意味着它们的值在首次创建之后不能更改。每当我们操作一个字符串(例如,通过使用+运算符连接两个字符串),Unity就会创建一个保存更新值得新字符串并丢弃旧字符串。这就产生了垃圾。
我们可以遵循一些简单得规则来将字符串中得垃圾降到最低。让我们考虑这些规则,然后看一个如何应用它们的示例。
- 我们应该减少不必要的字符串创建。如果我们不止一次使用相同得字符串值,我们应该创建一个字符串并缓存该值。
- 我们应该减少不必要的字符串操作。例如,如果我们有一个经常更新的文本(Text)组件,并且包含一个连接的字符串,我们可以考虑将它分成两个文本组件。
- 如果我们必须在运行时构建字符串,我们应该使用StringBuilder类。StringBuilder类用于构建没有分配的字符串,并且可以节省在连接复杂字符串时产生的垃圾数量。
- 我们应该在调试不再需要Debug.Log()调用时立即删除它们。对Debug.Log()的调用仍然在游戏的所有构建中执行,即使它们没有输出任何内容。对Debug. Log()的调用至少会创建并处理一个字符串,因此如果游戏包含许多此类调用,那么垃圾就会堆积起来。
让我们看一个代码示例,它通过低效地使用字符串生成不必要的垃圾。在下面得代码中,我们通过将字符串“TIME:”与浮点计时器的值组合,在Update()中创建一个分数显示字符串。这会产生不必要的垃圾。
public Text timerText; private float timer; void Update() { timer += Time.deltaTime; timerText.text = "TIME:" + timer.ToString(); }
在下面的例子中,我们对此进行了相当大的改进。我们将单词“TIME:”放在一个单独的文本组件中,并在Start()中设置其值。这意味着在Update()中,我们不再需要组合字符串。这大大减少了生成的垃圾数量。
public Text timerHeaderText; public Text timerValueText; private float timer; void Start() { timerHeaderText.text = "TIME:"; } void Update() { timerValueText.text = timer.toString(); }
Unity的函数调用
最重要的是要意识到,无论什么时候我们调用我们还没有编写的代码,无论是在Unity本身还是在插件中,我们都可能在生成垃圾。一些Unity函数调用创建堆分配,因此应该小心使用,以避免产生不必要的垃圾。
没有我们应该避免不去用的函数列表。每个函数在有些情况下有用,而在其他情况下用处不大。和以往一样,最好仔细分析我们的游戏,确定垃圾在哪里被创建,并仔细考虑如何处理它。在某些情况下,缓存函数的结果可能是明智的做法;在其他情况下,最好重构代码以使用不同的函数。说了这么多,让我们看几个会导致堆分配的Unity函数常见例子,并考虑如何最好地处理它们。
每当我们访问一个返回数组的Unity函数时,就会创建一个新数组并将其作为返回值传递给我们。这种行为并不总是明显的或可预期的,特别时当函数是一个访问器(例如,Mesh.normals)。
在下面的代码中,将为循环的每个迭代创建一个新数组。
void ExampleFunction() { for (int i = 0; i < myMesh.normals.Length; i++) { Vector3 normal = myMesh.normals[i]; } }
在这种情况下很容易既可以减少分配:我们可以简单地缓存对数组的引用。当我们这样做时,只创建了一个数组,并相应地减少了创建的垃圾数量。
下面代码说明了这一点。在这种情况下,我们循环运行之前调用Mesh.normals,并缓存了引用,以便只创建一个数组。
void ExampleFunction() { Vector3[] meshNormals = myMesh.normals; for (int i = 0; i < meshNormals.Length; i++) { Vector3 normal = meshNormals[i]; } }
另一个不可预期的堆分配可以在函数GameObject.name或者GameObject.tag中出现。这两个都是访问新字符串的访问器,这意味着调用这些函数将生成垃圾。缓存这个值可能很有用,但是在这种情况下,我们可以使用一个相关的Unity函数去代替。为了在不产生垃圾的情况下检查GameObject的标签值,我们可以使用GameObject.CompareTag()。
在下面的示例代码中,调用GameObject.tag的时候创建垃圾。
private string playerTag = "Player"; void OnTriggerEnter(Collider other) { bool isPlayer = other.gameObject.tag == playerTag; }
如果我们使用GameObject.CompareTag(),这个函数将不再产生任何垃圾:
private string playerTag = "Player"; void OnTriggerEnter(Collider other) { bool isPlayer = other.gameObject.CompareTag(playerTag); }
GameObject.CompareTag不是唯一的例子:许多Unity函数调用都有不导致堆分配的替代版本。例如,我们可以使用Input.GetTouch()和Input.touchCount代替Input.touches,或者Physics.SphereCastNonAlloc()代替Physics.ShpereCastAll()。
装箱(boxing)
装箱是当使用值类型变量代替引用类型变量时发生的操作。装箱通常发生在我们将值类型变量(如int或float)传递给带有引用类型参数参数时。
例如,函数String.Format()接收一个字符串和一个对象参数。当我们传递一个字符串和一个int时,这个int必须装箱。因此,下面的代码包含了装箱的一个例子:
void ExampleFunction() { int cost = 5; string displayString = String.Format("Price: {0} gold", cost); }
由于幕后发生的事情,装箱会产生垃圾。当一个值类型变量被装箱时,Unity会在堆上临时创建一个包装了值类型的System.Object类型的变量,因此当这个临时对象被丢弃时,就会创建垃圾。
装箱是造成不必要堆分配的一个非常常见的原因。即使我们不直接在代码中对变量进行装箱,我们也可能使用了导致装箱的插件,或者在其他函数的幕后进行装箱。最佳实践建议是尽可能避免装箱,并删除导致装箱的任何函数调用。
协程(Cotoutines)
调用StartCoroutine()会创建少量垃圾,因为Unity必须创建实例来管理这个协程。考虑到这一点,在我们的游戏是交互的并且性能要求需要考虑时,应该限制对StartCoroutine()的调用。为了减少以这种方式创建的垃圾,必须在性能关键时刻运行的任何协程都应该提前启动,在使用嵌套的协程时,我们应该特别小心,因为它可能包含对StartCoroutine()的延迟调用。
协程中的yield语句本身不会创建堆分配:但是,使用yield语句传递的值可能会创建不必要的堆分配。例如,下面的代码会创建垃圾:
yield return 0;
这段代码创建了垃圾,因为值为0的int类型被装箱了。在这种情况下,如果我们希望简单地等待一个帧而不引起任何堆分配,那么最好的方法是使用以下代码:
yield return null;
协同程序的另一个常见失误是在多次使用相同值时使用new。例如,下面的代码将在每次循环迭代时创建并释放WaitForSeconds对象:
while (!isComplete) { yield return new WaitForSeconds(1f); }
如果缓存和重用WaitForSeconds对象,则创建的垃圾会少很多。下面的代码说明改进的用法:
WaitForSeconds delay = new WaitForSeconds(1f); while (!isComplete) { yield return delay; }
如果我们的代码由于协程而产生大量垃圾,我们可能会考虑重构代码以使用协程之外的其他方法。重构代码是一个复杂的主题,每个项目都是独特的,但是我们可能希望记住,协程有一个常见的替代方案。例如,如果我们主要使用协程来管理时间,我们可能希望在Update()函数中简单地跟踪时间。如果我们使用协程主要是为了控制游戏中事情发生的顺序,我们可能希望创建某种消息传递系统来允许对象进行通信。对于这一点,没有一种适合所有人的方法,但是请记住,在代码中实现同一目标的方法通常不止一种。
foreach循环
在5.5之前的Unity版本中,foreach循环在每次结束时,遍历除数组之外的任何东西都会产生垃圾。这是由于装箱只在幕后进行的。一个System.Object在循环开始时分配到堆上,在循环结束时释放。这个问题在Unity 5.5中修复了。
例如,在5.5之前的Unity版本中,下面代码中的循环会产生垃圾:
void ExampleFunction(List listOfInts) { foreach (int currentInt in listOfInts) { DoSomething(currentInt); } }
如果我们不能升级我们的Unity版本,有一个简单的解决方案。for和while循环不会在幕后导致装箱,因此不会生成任何垃圾。在遍历非数组的集合时,我们应该支持使用它们。
下面代码中的循环不会生成垃圾:
void ExampleFunction(List listOfInts) { for (int i = 0; i < listOfInts.Count; i ++) { int currentInt = listOfInts[i]; DoSomething(currentInt); } }
函数引用
对函数的引用,无论是引用匿名方法还是命名方法,在Unity中都是引用类型变量。它们将导致堆分配。将匿名方法转换为闭包(在闭包中,匿名方法在创建时可以访问作用域中的变量)会显著增加内存使用和堆分配的数量。
函数引用和闭包如何分配内存的精确细节取决于平台和编译器的设置,但是如果垃圾回收是一个需要考虑的问题,那么最好在游戏过程中尽量减少函数引用和闭包的使用。这里有一篇文章更详细地描述了有关这个这方面内容的技术细节。
LINQ和正则表达式
LINQ和正则表达式都会因为幕后的装箱而生成垃圾。最好避免在需要考虑性能的情况下使用它们。同样,这里也有一篇关于这个主题的更多细节描述的文章。
构造代码以达到最小化垃圾回收的影响
我们的代码的结构方式会影响垃圾回收。即使我们的代码没有创建堆分配,它也会增加垃圾回收器的工作负担。
我们的代码不必要地增加垃圾回收器的工作负担的一种方式是要求它检查它不应该检查的东西。结构体是值类型变量,但是如果我们有一个包含引用类型的结构体,那么垃圾回收器必须检查整个结构体。如果我们有大量这样的结构体,那么这会为垃圾回收器增加大量额外的工作。
在这个例子中,结构体包含一个引用类型的字符串。垃圾回收器在运行时必须检查整个结构体数组。
public struct ItemData { public string name; public int cost; public Vector3 position; }
private ItemData[] itemData;
在下面这个例子中,我们将数据存储在不同的数组中。当垃圾回收器运行时,它只需要检查字符串数组,并可以忽略其他数组。这减少了垃圾回收器必须要做的工作。
private string[] itemNames; private int[] itemCosts; private Vector3[] itemPositions;
代码不必要地在增加垃圾回收器工作负担的另一种方式是使用不必要的对象引用。当垃圾回收器搜索堆上对象的引用时,它必须检查代码中的每个当前对象的引用。代码中对象引用的减少意味着它要做的工作更少,即使我们不减少堆上对象的总数。
在这个例子中,我们有一个弹出对话框的类。当用户查看该对话框时,将显示另一个对话框。我们的代码包含对下一个应该显示的DialogData实例的引用,这意味着垃圾回收器必须将该引用作为其操作的一部分进行检查。
public class DialogData { private DialogData nextDialog; public DialogData GetNextDialog() { return nextDialog; } }
在这里,我们重新构造了代码,使其返回一个标识符,用于查找DialogData的下一个实例,而不是实例本身。这不是一个对象引用,因此它不会增加垃圾回收器所花费的时间。
public class DialogData { private int nextDialogID; public int GetNextDialogID() { return nextDialogID; } }
就其本身而言,这个示例相当简单。但是,如果我们的游戏包含大量对象,这些对象包含对其他对象的引用,那么我们可以通过以这种方式重构代码来大大降低堆的复杂性。
手动强制垃圾回收
最后,我们可能希望自己触发垃圾回收。如果我们知道堆内存被分配,但不再使用(例如,假如我i们的代码在加载资源的时候生成垃圾),并且我们知道垃圾回收冻结也不会影响玩家(例如,加载屏幕仍然显示),我们可以使用下面的代码请求垃圾回收:
System.GC.Collect();
这将强制垃圾回收器运行,在我们方便的时候释放未使用的内存。
总结
我们已经学习了Unity中的垃圾回收是如何工作的,为什么垃圾回收会导致性能问题,以及如何最小化垃圾回收对游戏的影响。利用这些知识和我们的分析工具,我们可以修复与垃圾回收相关的性能问题,并构建游戏的结构,从而有效地管理内存。
下面的链接提供了关于本文主题的进一步阐述。
延伸阅读
Unity中的内存管理和垃圾回收
- Unity Manual: Understanding Optimization in Unity
- Unity Manual: Understanding Automatic Memory Management
- Gamasutra: C# Memory Management for Unity Developers by Wendelin Reich
- Gamasutra: C# memory and performance tips for Unity by Robert Zubek
- Gamasutra: Reducing memory allocations to avoid Garbage Collection on Unity by Grhyll JDD
- Gamasutra: Unity Garbage Collection Tips and Tricks by Megan Hughes
装箱