Unity文档总结(2)-Understanding Automatic Memory Management

当一个对象、字符串、数组被创建的时候,从中间池申请分配需要存储的内存称为堆。当该部分不在使用时,一度占用的内存被释放收回,用于别的事物。在过去,它通常由开发人员分配和释放这些堆内存块,明确相应的功能调用。如今像Unity的Mono引擎运行时系统自动为你管理内存。自动内存管理不需要进行明确的分配/释放的编码工作,并大大降低了潜在的内存泄漏(内存分配,但从未被释放的情况)。
·值和引用类型
当一个函数被调用时,把其参数的值复制到一个内存区域来保留为了特定的调用,只占用几个字节的数据类型可以被非常容易和快速地创建。然而常见的对象、字符串和数组要大得多,如果这些类型的数据需要定期复制的话效率会变得非常低。幸运的是这是没有必要的;一个大型项目的实际存储空间是从堆中分配并且只使用一个小小的"指针"值来记住它的位置。因此,只有指针需要在复制过程中进行参数传递。只要运行系统就可以找到指针确定的项目,可以视需要将经常使用的数据做个单一副本。直接存储和复制的参数传递过程中的类型被称为值类型。这些包括整型,浮点型,布尔型和Unity的结构类型(例如,Color和Vector3)。被分配在堆里然后通过指针访问的类型被称为引用类型,因为存储在变量中的值只是"指向"真实的数据。引用类型的例子包括:对象,字符串和数组。
·分配和垃圾回收
内存管理会保留堆中它知道未使用的区域。当一个新的内存块请求时(例如当一个对象被实例化),管理机制选择一个未使用的区域分配给内存块,然后把这块未使用空间分配的内存删除。后续请求都以同样的方式处理,直到有没有足够大的空闲面积来分配所需的块大小。从堆中分配的所有内存同时都在使用中是非常不可能的。在堆上的一个参考项目只要仍然能找到它的参考变量就可以访问到它。如果一个内存块的所有引用都消失了(即引用变量都被重新分配,或者他们是超出范围的局部变量),那么它所占用的内存,可以安全地重新分配。
要确定这堆块不再被使用,内存管理器会通过搜索所有当前活动的参考变量和标记他们称之为"活动"的块。在搜索结束时,内存管理器把任何活动的块和块之间的空间视为是空闲的,可以用于后续的分配。显而易见,定位和释放未使用的内存的过程中被称为垃圾收集(简称GC)。
·优化
垃圾回收对开发人员来说是自动的和不可见的,但实际上回收过程在后台需要占用CPU大量的时间。在正确使用时,自动内存管理一般等于或高于手动分配的整体性能。然而,重要的是为程序员避免失误,引发不必要的垃圾回收和执行停顿。
也有一些非主流的算法,能成为的GC的噩梦,即使他们看上去似乎是无辜的。重复字符串连接是一个典型的例子:
//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;
}
}
这里的关键细节是,新的块没有一个接一个添加到字符串的位置。实际情况是,每次循环的时候,以前的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;
}
}
每次进行更新时将分配新的字符串,并且垃圾像涓涓细流一样持续产生。大多数字符串可以在更新文本保存除了当score发生变化时: -
//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;
}
}
}
另一个潜在的问题发生在一个函数返回一个数组值
//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;
}
}
当创建一个新的数组值填充时,这种类型的函数是非常美观和方便的。然而,如果它被反复调用的话每次都要为它分配新的内存。由于数组可以是非常大的,所消耗完的空闲堆空间会迅速上升,导致频繁的垃圾收集。避免此问题的实际方法之一是使用的数组是一个引用类型。该函数内可以修改成一个函数作为参数传递的数组,并保留函数返回后的结果。像上面的函数常常可以被替换是这样的:
//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;
}
}
}
这只是简单的用新值取代现有的数组内容。虽然这需要在调用代码时就初始化分配好数组(这似乎有些不美观),该函数在调用时将不会产生任何新的垃圾。
请求回收

如上所述,最好尽量避免内存分配。然而分配内存是无法避免的,所以提供两种方法来让您尽量减少它们在游戏运行时的使用:
·如果堆比较小,则进行快速而频繁的垃圾回收
这一策略比较适合运行时间较长的游戏,其中帧率是否平滑过渡是主要考虑的因素,像这样的游戏通常会频繁地分配小块内存,但这些小块内存只是暂时地被使用,如果在iOS系统上使用该策略,那么一个典型的堆大小是大约 200 KB,这样在iPhone 3G设备上,垃圾回收操作将耗时大约 5毫秒。如果堆大小增加到1 MB时,该回收操作将耗时大约 7ms。因此,在普通帧的间隔期进行垃圾回收有时候是一个不错的选择。通常,这种做法会让回收操作执行的更加频繁(有些回收操作并不是严格必须进行的),但它们可以快速处理并且对游戏的影响很小:
if (Time.frameCount % 30 == 0)
{
System.GC.Collect();
}
但是,您应该小心地使用这种技术,并且通过检查Profiler来确保这种操作确实可以降低您游戏的垃圾回收时间。
·如果堆比较大,则进行缓慢且不频繁的垃圾回收
这一策略适合于那些内存分配 (和回收)相对不频繁,并且可以在游戏停顿期间进行处理的游戏。如果堆足够大,但还没有大到被系统关掉的话,这种方法是比较适用的。但是,Mono运行时会尽可能地避免堆的自动扩大。因此,您需要通过在启动过程中预分配一些空间来手动扩展堆(ie,你实例化一个纯粹影响内存管理器分配的"无用"对象):
//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;
}
}
游戏中的暂停是用来对堆内存进行回收,而一个足够大的堆应该不会在游戏的暂停与暂停之间被完全占满。所以,当这种游戏暂停发生时,您可以显式请求一次垃圾回收:
System.GC.Collect();
另外,您应该谨慎地使用这一策略并时刻关注Profiler的统计结果,而不是假定它已经达到了您想要的效果。
可重复使用的对象池
如果想避免垃圾回收,在很多情况下最简单的方法就是减少对象实例化和销毁的次数。在游戏中有一类游戏对象,例如子弹,他们会重复多次出现尽管只在游戏开始的一会。在这种情况下可以重用这些对象而不是销毁他们而创建新的对象。

转载于:https://www.cnblogs.com/gaoxu-1994/p/6526256.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值