Unity3D实验室之优化GC

作者: koo叔 分类: Unity3D 发布时间: 2018-04-10 06:57 编辑

前言

本文将对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中被缓存<br/>
缓存数组可以多次重复使用而不会产生垃圾

不要在频繁调用的函数中分配

由于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的值被改变时才调用函数,即只有在必要时才会进行赋值。<br/>
另外,使用计时器也很有效。

void Update(){
    ExampleGarbageGeneratingFunction();
}

上面代码每调用一次都会产生垃圾。

private float timeSinceLastCalled;
private float delay = 1f;
void Update(){
    timeSinceLastCalled += Time.deltaTime;
    if(timeSinceLastCalled<=delay)return;
    ExampleGarbageGeneratingFunction();
    timeSinceLastCalled = 0f;
}

在上面代码中,使用计时器,使方法每秒才执行一次。<br/>
通过对频繁调用的代码进行更改,可以大幅减少产生的垃圾数量

清除集合

当创建一个新的集合,将产生堆内存分配,如果要创建一个新的集合,缓存对集合的引用,使用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,这样仅发生了一次拷贝<br/>
访问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替换直接比较的方式可以防止产生垃圾<br/>
此外还有很多Unity API可以通过类似方式避免堆内存的分配.
如:使用Input.GetTouch和Input.touchCount代替Input.Touches,
或Physics.SphereCastNonAlloc()代替Physics.SphereCastAll().

协同程序

在协同中传递给yield的值,可能会发生不必要的堆分配。

yield return 0;

如上述代码,将会对int类似的0,进行装箱操作,而产生不必要的堆分配。<br/>

yield return null;

如果只是想等一帧,建议使用上述代码<br/>
另一个常见的协同错误是在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
如果在游戏中持有大量对象的引用,可以改成实例标识符来降低堆的分配

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表评论

你的email不会被公开。必填项已用*标注

更多阅读
标签云