前言
本文将对Unity介绍的性能改进的文章进行部分翻译,原文地址:https://unity3d.com/jp/learn/tutorials/topics/performance-optimization/optimizing-garbage-collection-unity-games
缓存
void OnTriggerEnter(Collider other){
var allRenderers = FindObjectsOfType<Renderer>();
ExampleFunction(allRenderers);
}
在上面的示例中,每次调用代码时都会创建一个新数组,导致堆内存增加。
private Renderer[] allRenderers;
void Start(){
allRenderers = FindObjectOfType<Renderer>();
}
void OnTriggerEnter(Collider other){
ExampleFunction(allRenderers);
}
在上面的示例中,只有一个堆内存分配,因为创建的数组在Start中被缓存
缓存数组可以多次重复使用而不会产生垃圾
不要在频繁调用的函数中分配
由于Update和LateUpdate每帧都会被调用,所以一旦产生垃圾会迅速累加。如果可能的话,在Start或Awake中缓存对象的引用,仅在必要时再分配对象。
void Update(){
ExampleGarbageGeneratingFunction(transform.position.x);
}
例如,在上面的代码中,每次调用Update都会调用引发赋值的函数,并且会频繁的创建垃圾。
private float previousTransformPositionX;
void Update(){
var transformPositionX = transform.position.x;
if(transformPositionX == previousTransformPositionX)return;
ExampleGarbageGeneratingFunction(transformPositionX);
previousTransformPositionX = transformPositionX;
}
通过上面改进的方式,只有在transform.position.x的值被改变时才调用函数,即只有在必要时才会进行赋值。
另外,使用计时器也很有效。
void Update(){
ExampleGarbageGeneratingFunction();
}
上面代码每调用一次都会产生垃圾。
private float timeSinceLastCalled;
private float delay = 1f;
void Update(){
timeSinceLastCalled += Time.deltaTime;
if(timeSinceLastCalled<=delay)return;
ExampleGarbageGeneratingFunction();
timeSinceLastCalled = 0f;
}
在上面代码中,使用计时器,使方法每秒才执行一次。
通过对频繁调用的代码进行更改,可以大幅减少产生的垃圾数量
清除集合
当创建一个新的集合,将产生堆内存分配,如果要创建一个新的集合,缓存对集合的引用,使用Clear方法,而不是每次都new一个。
void Update(){
var myList = new List<int>();
PopulateList(myList);
}
在上面这个例子中,每次new都会产生新的堆内存分配。
private List<int> myList = new List<int>();
void Update(){
myList.Clear();
PopulateList(myList);
}
在上面例子中,只有在创建集合和集合需要调整大小时才进行赋值,这将减少生成的垃圾数量。
字符串
下面的创建字符串的方法会产生不必要的垃圾
public string timerText;
private float timer;
void Update(){
timer += Time.deltaTime;
timerText.text = "TIME:"+timer.ToString();
}
在上面代码中,字符串”TIME:”与浮点timer组合,会创建新的字符串,而产生了不必要的垃圾。
public string timerHeaderText;
public string timerValueText;
private float timer;
void Start(){
timerHeaderText.text = "TIME:";
}
void Update(){
timerValueText.text = timer.ToString();
}
上面例子,文本”TIME:”被设置为另一个Text组件,不需要再连接字符串,因此大大减少了垃圾
调用Unity API
访问返回值是数组的Unity内置方法或属性时,可能会创建一个新数组返回,因此每次调用Unity API时都会发生堆分配
void ExampleFunction(){
for(var i = 0;i<myMesh.normals.Length;i++){
var normal = myMesh.normals[i];
}
}
如上:每次在循环中访问Mesh.normals时,都会创建一个新的数组。可以通过将引用缓存到数组中来减少分配
void ExampleFunction(){
var meshNormals = myMesh.normals;
for(var i = 0;i<meshNormals.Length;i++){
var nromal = meshNormals[i];
}
}
上面代码在循环之前缓存Mesh.normals,这样仅发生了一次拷贝
访问GameObject.tag时也会发生堆分配
private string playerTag = "玩家";
void OnTriggerEnter(Collider other){
var isPlayer = other.gameObject.tag == playerTag;
}
上面代码垃圾是通过调用GameObject.tag生成的。
private string playerTag = "玩家";
void OnTriggerEnter(Collider other){
var isPlayer = other.gameObject.CompareTag(playerTag);
}
通过使用GameObject.CompareTag替换直接比较的方式可以防止产生垃圾
此外还有很多Unity API可以通过类似方式避免堆内存的分配.
如:使用Input.GetTouch和Input.touchCount代替Input.Touches,
或Physics.SphereCastNonAlloc()代替Physics.SphereCastAll().
协同程序
在协同中传递给yield的值,可能会发生不必要的堆分配。
yield return 0;
如上述代码,将会对int类似的0,进行装箱操作,而产生不必要的堆分配。
yield return null;
如果只是想等一帧,建议使用上述代码
另一个常见的协同错误是在yield中使用new
while(!isComplete){
yield return new WaitForSeconds(1f);
}
上面这段代码每次重复一个循环都会创建并销毁一个WaitForSeconds对象
var delay = new WaitForSeconds(1f);
while(!isComplete){
yield return delay;
}
通过缓存WaitForSeconds对象,可以防止垃圾发生
foreach循环
如果Unity的版本是5.5或更低版本,则foreach会产生垃圾,
在Unity5.5中已修复此问题
void ExampleFunction(List<int> listOfInts){
foreach(var currentInt in listOfInts){
DoSomething(currentInt);
}
}
如果不太方便升级Unity版本,则可以通过将foreach替换为for来避免创建垃圾
void ExampleFunction(List<int> listOfInts){
for(var i = 0;i<listOfInts.Count;i++){
var currentInt = listOfInts[i];
DoSomething(currentInt);
}
}
结构体中的数据
虽然结构体是值类型,但如果包含引用类型的变量,则会被GC检查.
public struct ItemData{
public string name;
public int cost;
public Vector3 position;
}
private ItemData[] itemData;
上面代码中,由于结构体中包含一个引用类型的字符串,所以整个结构体数组都会被GC检查。
private string[] itemNames;
private int[] itemCosts;
private Vector3[] itemPositions;
将每个字段分别转换成数组后,只有字符串数组受GC检查,其它将被忽略。可以降低GC负载
public class DialogData{
private DialogData nextDialog;
public DialogData GetNextDialog(){
return nextDialog;
}
}
在上面这个例子中,存储了另一个对话框的引用,GC也会检查此引用
public class DialogData{
private int nextDialogID;
public int GetNextDialogID(){
return nextDialogID;
}
}
如果修改成保存搜索实例的标识符,则不会进行GC
如果在游戏中持有大量对象的引用,可以改成实例标识符来降低堆的分配