1、减少GC 的运行次数
2、减少单次GC的运行时间
3、将GC的运行时间延迟,避免在关键时候触发,比如可以在场景加载的时候调用GC
例如
void OnTriggerEnter(Collider other)
{
Renderer[] allRenderers = FindObjectsOfType();
ExampleFunction(allRenderers);
}
private Renderer[] allRenderers;
///应该这样
void Start()
{
allRenderers = FindObjectsOfType();
}
void OnTriggerEnter(Collider other)
{
ExampleFunction(allRenderers);
}
例如:不要再update、LateUpdate中进行内存分配,可以在Start 或者Awake中进行。
对象池:空间换时间的折中方案
对象池的优缺点
1、提升获取对象的响应速度
2、运用对象池技术,可以显著提升性能。
3、一定程度上减少了GC的压力。对于实时性要求比较高的程序
有很大的帮助。比如说Http连接的对象池,Redis对象池等等都是使用了对象池。
1、脏对象的问题
所谓的脏对象就是指的是当对象被放回对象池后,还保留着刚刚被客户端调用时生成的数据。
脏对象可能带来两个问题:
1).脏对象持有上次使用的引用,导致内存泄漏等问题。
2). 脏对象如果下一次使用时没有做清理,可能影响程序的处理数据。
2、生命周期的问题
处于对象池中的对象生命周期要比普通的对象要长久。维持大量的对象也是比较占用内存空间的。
(1)减少字符串的创建以及加减运算
(2)使用StringBuilder
(3)移除Debug.Log
(4)可以把固定的字符串与变化的字符串分开。
//这种会进行一个string的操作,造成不必要的内存垃圾
public Text timerText;
private float timer;
void Update()
{
timer += Time.deltaTime;
timerText.text = "Time:"+ timer.ToString();
}
//转换后
public Text timerHeaderText;
public Text timerValueText;
private float timer;
void Start()
{
timerHeaderText.text = "TIME:";
}
void Update()
{
timerValueText.text = timer.ToString();
}
private StringBuilder _stringBuilder = new StringBuilder();
private List _list = new List();
void Start()
{
for (int i=0;i<100;i++)
{
_list.Add(i.ToString());
}
}
void Update()
{
// string s = "";
_stringBuilder.Clear();
for (int i=0;i<_list.Count;i++)
{
//s += _list[i];
_stringBuilder.Append(_list[i]);
}
}
void ExampleFunction()
{
for(int i=0; i < myMesh.normals.Length;i++)
{
Vector3 normal = myMesh.normals[i];
}
}
//第二
void ExampleFunction()
{
Vector3[] meshNormals = myMesh.normals;
for(int i=0; i < meshNormals.Length;i++)
{
Vector3 normal = meshNormals[i];
}
}
理解一下上面代码,通俗讲,第一个函数,每次循环都会从堆中获取数据。而第二个方法,直接把值先拷贝一份放到数组中,第二中方法会更快一些。
通过这些细小的改变,我们可以使得代码运行的更快同时减少内存垃圾的产生。
-----举个例子-----
void Update()
{
ExampleGarbageGenerationFunction(transform.position.x);
}
这种我们可以采用下面这种方式优化
private float previousTransformPositionX;//声明一个变量
void Update()
{
float transformPositionX = transform.position.x;
//每次update的时候,判断一下值是否相等,才能执行if语句里的代码。
if(transfromPositionX != previousTransformPositionX)
{
ExampleGarbageGenerationFunction(transformPositionX);
previousTransformPositionX = trasnformPositionX;
}
}
还有一种,添加计时器。一秒执行一次
private float timeSinceLastCalled;
private float delay = 1f;
void Update()
{
timSinceLastCalled += Time.deltaTime;
if(timeSinceLastCalled > delay)
{
ExampleGarbageGenerationFunction();
timeSinceLastCalled = 0f;
}
}
不要忽略这一个方法,很多对于固定时间的时间回调函数中,如果每次都分配新的缓存,但是在操作完后并不释放,这样就造成大量的内存垃圾,对于这样的缓存
//改进前
void Update()
{
List myList = new List();
PopulateList(myList);
}
//改进后
private List myList = new List();
void Update()
{
myList.Clear();
PopulateList(myList);
}
改进后,该链表只能在第一次创建或者该链表必须重新设置的时候才能进行堆内存分配,大大减少内存垃圾的产生。
装箱操作是非常普遍的一种产生内存垃圾的行为。
调用StartCoroutine()会产生少量的内存垃圾,
yield在协程中不会产生堆内存分配,但是如果yield带有参数返回,则会造成不必要的内存垃圾
例如:
yield return 0;//由于返回0,引发了装箱操作
yield rerurn null;//可以这么写
第二种对协程的错误使用:每次返回的时候都new同一个变量,例如
while(!isComplete)
{
yield return new WaitForSeconds(1f);
}
WaitForSeconds delay = new WaiForSeconds(1f);
while(!isComplete)
{
yield return delay;
}
如果用协程来控制游戏中的事件的发生顺序,最好对于不同事件之间有一定的信息通讯方式。
还有其他的协程优化方法,后续再整理。
Unity5.5以上,对foreach进行了改进,不会再产生内存垃圾。
由于LINQ和常量表达式以装箱的方式实现,所以在使用的时候最好进行性能测试。
即使我们减少了代码再堆内存上的分配操作,代码也会增加GC的工作量。
例如,Struct本身是值类型,但是里面有string变量,那么整个Struct都必须在
GC中被检查。
例子
public struct ItemData
{
public string name;
public int cost;
public Vector3 position;
}
private ItemData[] itemData;
不过我们可以把struct拆分成多个数组形式
private string[] itemNames;
private int[] itemCosts;
private Vector3[] itemPositions;
减少不必要的Object引用
//例如
public class DialogData
{
private DialogData nextDialog;
public DialogData GetNextDialog()
{
return nextDialog;
}
}
//通过重构代码,我们可以返回下一个对话框实体的标记,而不是对话框本身,这样就可以解决多余的object引用了。
public class DialogData
{
private int nextDialogID;
public int GetNextDialogID()
{
return nextDialogID;
}
}
我们可以在不影响游戏体验的时候主动调用GC(例如在场景切换的时候)
System.GC.Collect()