Unity3D實驗室之優化GC

前言

本文將對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
如果在遊戲中持有大量對象的引用,可以改成實例標識符來降低堆的分配

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章