1. 程式人生 > >Unity優化方向——優化Unity遊戲中的垃圾回收

Unity優化方向——優化Unity遊戲中的垃圾回收

傳遞 必須 compare 消息傳遞 適用於 間歇性 連接 ria dir

介紹

當我們的遊戲運行時,它使用內存來存儲數據。當不再需要該數據時,存儲該數據的內存將被釋放,以便可以重用。垃圾是用來存儲數據但不再使用的內存的術語。垃圾回收是該內存再次可用以進行重用的進程的名稱。 Unity使用垃圾回收作為管理內存的一部分。如果垃圾回收發生得太頻繁或者有太多工作要做,我們的遊戲可能會表現不佳,這意味著垃圾回收是導致性能問題的常見原因。 在本文中,我們將了解垃圾回收如何工作的,什麽時候發生垃圾回收,以及如何有效地使用內存,從而最小化垃圾回收對遊戲的影響。

診斷垃圾回收的問題

垃圾回收導致的性能問題可以表現為幀率低、性能不穩定或間歇性凍結。然而,其他問題也會引起類似的癥狀。如果我們的遊戲有這樣的性能問題,我們應該做的第一件事就是使用Unity的Profiler窗口來確定我們看到的問題是否真的是由於垃圾回收造成的。 要了解如何使用Profiler窗口查找性能問題的原因,請查閱這一篇教程。

Unity內存管理簡介

要理解垃圾回收是如何工作的,以及垃圾回收何時發生,我們必須首先理解Unity中內存的使用是如何工作的。首先,我們必須理解Unity在運行它自己的核心引擎代碼和運行我們編寫的代碼時使用了不同的方法。 Unity在運行自己的核心Unity引擎代碼時管理內存的方式叫做手動內存管理。這意味著核心引擎代碼必須顯式地聲明如何使用內存。手動內存管理不使用垃圾回收,本文將不再深入討論。 Unity在運行我們的代碼時管理內存的方式叫做自動內存管理。這意味著我們的代碼不需要顯式地告訴Unity如何以一種詳細的方式管理內存。Unity幫我們解決了這個問題。 在最基本的層面上,Unity中的自動內存管理是這樣工作的:
  • Unity可以訪問兩個內存池:棧(stack)和堆(heap,也稱為托管堆)。棧用於短期存儲小塊數據,堆用於長期存儲和大塊數據。
  • 當一個變量被創建時,Unity會從棧或堆中請求一個內存塊。
  • 只要變量在作用域內(仍然可由代碼訪問),分配給它的內存就會一直使用。我們說這個內存已經分配了。我們將棧內存中保存的變量描述為棧上的對象,將堆內存中保存的變量描述為堆上的對象。
  • 當變量超出作用域時,內存不再需要,可以將其返回到它所來自的池中。當內存被返回到它的池時,我們說內存已被釋放。棧中內存在它引用的變量超出作用域時立即釋放。但是,堆中的內存此時沒有釋放,並且仍然處於已分配的狀態,即使它引用的變量超出了作用域。
  • 垃圾回收器標識和釋放未使用的堆內存。垃圾回收器定期運行以清理堆。
現在我們已經了解了事件流,接下來我們進一步了解棧內存分配和釋放與堆分配和釋放的區別。

棧分配和釋放期間發生了什麽?

棧分配和釋放是快速和簡單的。這是因為棧僅用於短時間內存儲小數據。分配和回收總是以可預測的順序發生,並且具有可預測的大小。 棧的工作方式類似於棧數據類型:它是元素的簡單集合,在本例中是內存塊,其中元素只能按照嚴格的順序添加和刪除。這種簡單性和嚴格性使得它如此快速:當一個變量存儲在棧上時,它的內存只是從棧的“末端”分配的。當棧變量超出作用域時,用於存儲該變量的內存將立即返回棧以供重用內存。

堆分配期間發生了什麽?

堆分配比棧分配復雜得多。這是因為堆可以用來存儲長期和短期數據,以及許多不同類型和大小的數據。分配和回收並不總是按照可預測的順序發生,可能需要許多不同大小的內存塊。 創建變量時,執行以下步驟:
  • 首先,Unity必須檢查堆中是否有足夠的空閑內存。如果堆中有足夠的空閑內存,則為變量分配內存。
  • 如果堆中沒有足夠的空閑內存,Unity會觸發垃圾回收器,試圖釋放未使用的堆內存。這可能是一個緩慢的操作。如果堆中現在有足夠的空閑內存,則分配變量的內存。
  • 如果垃圾回收後堆中沒有足夠的空閑內存,Unity會增加堆中的內存容量。這可能是一個緩慢的操作。然後分配變量的內存。
堆分配可能很慢,特別是在必須運行垃圾回收器和必須擴展堆的情況下。

垃圾回收期間發生了什麽?

當堆變量超出作用域時,用於存儲它的內存不會立即釋放。未使用的堆內存僅在垃圾回收器運行時釋放。
  • 垃圾回收器檢查堆上的每個對象。
  • 垃圾回收器搜索所有當前對象引用,以確定堆上的對象是否仍在作用域內。
  • 任何不再在作用域中的對象都被標記為刪除。
  • 刪除標記的對象,並將分配給它們的內存返回堆中。
垃圾回收是一項耗性能的操作。堆上的對象越多,它必須做的工作就越多,代碼中的對象引用越多,它必須做的工作就越多。 垃圾回收什麽時候發生? 有三種情況會導致垃圾回收器運行:
  • 每當請求使用堆中的空閑內存無法完成堆分配時,垃圾回收器就會運行。
  • 垃圾回收器不時自動運行(盡管頻率隨平臺而變化)。
  • 垃圾回收器可以強制手動運行。
垃圾回收是一種常見的操作。每當無法從可用堆內存中完成堆分配時,就會觸發垃圾回收器,這意味著頻繁的堆分配和回收會導致頻繁的垃圾回收。

垃圾回收的問題

現在我們已經了解了垃圾回收在Unity內存管理中的作用,我們可以考慮可能發生的問題的類型。 最明顯的問題是垃圾回收器可能需要相當長的事件來運行。如果垃圾回收器在堆上有很多對象和/或有對象引用要檢查,那麽檢查所有這些對象的過程可能會很慢。這可能導致我們的遊戲卡頓或者緩慢。 另一個問題是垃圾回收器可能在不合適的時間運行。如果CPU已經在遊戲的性能關鍵部分努力運行著,那麽即使垃圾回收帶來的少量額外開銷也會導致幀率下降和性能顯著變化。 另一個不太明顯的問題是堆碎片。當從堆中分配內存時,根據必須存儲的數據的大小,內存以不同大小的塊從空閑空間中獲取。當這些內存塊被返回到堆中時,堆可以被分割成許多由分配的塊分隔的小的空閑塊。這意味著,盡管空閑內存的容量可能很高,但是我們無法在不運行垃圾回收器和/或擴展堆的情況下分配內存塊,因為現有的塊都不夠大。 碎片堆有兩個後果。第一,我們的遊戲內存使用量將高於它所需要的水平,第二,垃圾回收器將運行得更頻繁。有關堆碎片得更詳細討論,請參考這一篇文章。

發現堆分配

如果我們知道垃圾回收在我們的遊戲中造成了問題,我們就需要知道代碼的哪些部分正在生成垃圾。垃圾是在堆變量超出作用域時生成的,因此首先我們需要知道是什麽原因導致在堆上分配變量。

在棧和堆上分配了什麽?

在Unity中,值類型的局部變量被分配到棧上,其他的變量被分配到堆上。如果你不確定Unity中值類型和引用類型之間的區別,請參閱本教程。 下面的代碼是棧分配的一個示例,因為變量localInt既是本地的又是值類型的。為該變量分配的內存將在該函數完成運行後立即從棧中釋放。
void ExampleFunction()
{
int localInt = 5;
}

下面的代碼是堆分配的一個示例,因為變量localList是本地的,但是是引用類型。為該變量分配的內存將在垃圾回收器運行時回收。
void ExampleFunction()
{
List localList = new List();
}

使用Profiler窗口查找堆分配 我們可以在Profiler窗口中看到代碼創建的堆分配。 在選擇了CPU usage Profiler之後,我們可以在Profiler窗口的底部選擇任何幀來查看關於該幀的CPU使用率數據。其中一系列數據稱為GC alloc。這一列顯示在該幀中進行的堆分配。如果我們選擇列標題,我們可以通過這個統計數據對數據進行排序,這樣就很容易看到遊戲中哪些函數導致了最多的堆分配。一旦我們知道哪個函數導致堆分配,就可以檢查這個函數。 一旦我們知道函數中的哪些代碼導致生成垃圾,我們就可以決定如何解決這個問題並最小化生成垃圾數量。

減少垃圾回收的影響

一般來說,我們可以通過以下三種方式減少垃圾回收對遊戲的影響:
  • 我們可以減少垃圾回收器運行的時間。
  • 我們可以減少垃圾回收器運行的頻率。
  • 我們可以故意觸發垃圾回收器,使其在性能不重要的時候運行(例如在加載屏幕期間)。
考慮到這一點,有三種策略可以幫助我們:
  • 我們可以阻止我們遊戲,這樣我們就有更少的堆分配和更少的對象引用。堆上的對象越少,要檢查的引用越少,這意味著在觸發垃圾回收時,運行垃圾回收所需的時間越少。
  • 我們可以減少堆分配和回收的頻率,特別是在性能關鍵時候。更少的分配和回收意味著觸發垃圾回收的情況更少。這也降低了堆碎片的風險。
  • 我們可以嘗試計時垃圾回收和堆擴展,以便在可預測和方便的時間進行。這是一種更困難、更不可靠的方法,但是如果將其作為整體內存管理策略的一部分使用,則可以減少垃圾回收的影響。

減少垃圾的創建

讓我們研究一些技術,它們將幫助我們減少代碼生成的垃圾數量。

緩存

如果我們的代碼重復調用導致堆分配的函數,然後丟棄結果,這會產生不必要的垃圾。相反,我們應該存儲對這些對象的引用並重用它們。這種技術稱為緩存。 在下面的示例中,每次調用代碼時,代碼都會導致堆分配。這是因為創建了一個新數組。
void OnTriggerEnter(Collider other)
{
Renderer[] allRenderers = FindObjectsOfType<Renderer>();
ExampleFunction(allRenderers);
}

下面的代碼只會導致一個堆分配,因為數組只會創建和填充一次,然後緩存。緩存的數組可以一次又一次地重用,而不會產生更多的垃圾。
private Renderer[] allRenderers;
 
void Start()
{
allRenderers = FindObjectsOfType<Renderer>();
}
 
 
void OnTriggerEnter(Collider other)
{
ExampleFunction(allRenderers);
}

不要在頻繁調用的函數中進行分配

如果我們必須在Monobehaviour分配堆內存,最糟糕的情況是在頻繁運行的函數中。例如,Update()和LateUpdate()在每一幀中調用一次,因此如果我們的代碼在這裏生成垃圾,那麽它會很快地累積起來。如果可能,我們應該考慮在Start()或Awake()中緩存對對象的引用,或者確保只在需要時才運行導致分配的代碼。
void Update()
{
ExampleGarbageGeneratingFunction(transform.position.x);
}

通過一個簡單的更改,我們現在確保僅在transform.position.x的值已經改變的情況下才調用分配函數。我們現在只在需要的時候進行堆分配,而不是每一幀中進行。
private float previousTransformPositionX;
 
void Update()
{
float transformPositionX = transform.position.x;
if (transformPositionX != previousTransformPositionX)
{
ExampleGarbageGeneratingFunction(transformPositionX);
previousTransformPositionX = transformPositionX;
}
}

減少Update()中生成的垃圾的另一種技術是使用計時器。這適用於當我們生成垃圾的代碼必須定期運行,但不一定是每一幀。 在下面的示例代碼中,生成垃圾的函數每幀運行一次:
void Update()
{
ExampleGarbageGeneratingFunction();
}

在下面的代碼中,我們使用計時器來確保生成垃圾的函數每秒運行一次。
private float timeSinceLastCalled;
 
private float delay = 1f;
 
void Update()
{
timeSinceLastCalled += Time.deltaTime;
if (timeSinceLastCalled > delay)
{
ExampleGarbageGeneratingFunction();
timeSinceLastCalled = 0f;
}
}

對頻繁運行的代碼進行這樣的小更改時,可以大大減少生成的垃圾數量。

清除集合

創建新的集合會導致堆上的分配。如果我們發現在代碼中不止一次地創建新集合,我們應該緩存對該集合的引用,並使用Clear()清空其內容,而不是重復調用new。 在下面的示例中,每次使用new時都會發生新的堆分配。
void Update()
{
List myList = new List();
PopulateList(myList);
}

在下面的示例中,分配僅在創建集合或必須在幕後調整集合大小時發生。這大大減少了生成的垃圾數量。
private List myList = new List();
void Update()
{
myList.Clear();
PopulateList(myList);
}

對象池

即使我們在代碼中減少了分配,但是如果我們在運行時創建和銷毀大量對象,我們仍然可能會遇到垃圾回收的問題。對象池是一種通過重用對象而不是重復創建和銷毀 對象來減少分配和回收的技術。對象池在遊戲中應用廣泛,最適合我們頻繁生成和銷毀相似對象的情況;例如,從槍裏射出子彈時。 關於對象池的完整說明超出了本文的範圍,但它確實是一種有用的技術,值得學習。Unity學習網站上關於對象池的教程在這裏。

不必要的堆分配的常見原因

我們知道,本地的、值類型的變量是在棧上分配的,其他的都是在堆上分配的。然而,在許多情況下,堆分配可能會讓我們大吃一驚。讓我們看看造成不必要堆分配的一些常見原因,並考慮如何最好地減少這些原因。

字符串(strings)

在C#中,字符串是引用類型而不是值類型,即使它們似乎保存了字符串的“值”。這意味著創建和丟棄字符串都會產生垃圾。由於字符串在很多代碼中都是常用的,所以這些垃圾實際上是可以累加的。 C#中的字符串也是不可變的,這意味著它們的值在首次創建之後不能更改。每當我們操作一個字符串(例如,通過使用+運算符連接兩個字符串),Unity就會創建一個保存更新值得新字符串並丟棄舊字符串。這就產生了垃圾。 我們可以遵循一些簡單得規則來將字符串中得垃圾降到最低。讓我們考慮這些規則,然後看一個如何應用它們的示例。
  • 我們應該減少不必要的字符串創建。如果我們不止一次使用相同得字符串值,我們應該創建一個字符串並緩存該值。
  • 我們應該減少不必要的字符串操作。例如,如果我們有一個經常更新的文本(Text)組件,並且包含一個連接的字符串,我們可以考慮將它分成兩個文本組件。
  • 如果我們必須在運行時構建字符串,我們應該使用StringBuilder類。StringBuilder類用於構建沒有分配的字符串,並且可以節省在連接復雜字符串時產生的垃圾數量。
  • 我們應該在調試不再需要Debug.Log()調用時立即刪除它們。對Debug.Log()的調用仍然在遊戲的所有構建中執行,即使它們沒有輸出任何內容。對Debug. Log()的調用至少會創建並處理一個字符串,因此如果遊戲包含許多此類調用,那麽垃圾就會堆積起來。
讓我們看一個代碼示例,它通過低效地使用字符串生成不必要的垃圾。在下面得代碼中,我們通過將字符串“TIME:”與浮點計時器的值組合,在Update()中創建一個分數顯示字符串。這會產生不必要的垃圾。
public Text timerText;
private float timer;
 
void Update()
{
timer += Time.deltaTime;
timerText.text = "TIME:" + timer.ToString();
}

在下面的例子中,我們對此進行了相當大的改進。我們將單詞“TIME:”放在一個單獨的文本組件中,並在Start()中設置其值。這意味著在Update()中,我們不再需要組合字符串。這大大減少了生成的垃圾數量。
public Text timerHeaderText;
public Text timerValueText;
private float timer;
 
void Start()
{
timerHeaderText.text = "TIME:";
}
 
void Update()
{
timerValueText.text = timer.toString();
}

Unity的函數調用

最重要的是要意識到,無論什麽時候我們調用我們還沒有編寫的代碼,無論是在Unity本身還是在插件中,我們都可能在生成垃圾。一些Unity函數調用創建堆分配,因此應該小心使用,以避免產生不必要的垃圾。 沒有我們應該避免不去用的函數列表。每個函數在有些情況下有用,而在其他情況下用處不大。和以往一樣,最好仔細分析我們的遊戲,確定垃圾在哪裏被創建,並仔細考慮如何處理它。在某些情況下,緩存函數的結果可能是明智的做法;在其他情況下,最好重構代碼以使用不同的函數。說了這麽多,讓我們看幾個會導致堆分配的Unity函數常見例子,並考慮如何最好地處理它們。 每當我們訪問一個返回數組的Unity函數時,就會創建一個新數組並將其作為返回值傳遞給我們。這種行為並不總是明顯的或可預期的,特別時當函數是一個訪問器(例如,Mesh.normals)。 在下面的代碼中,將為循環的每個叠代創建一個新數組。
void ExampleFunction()
{
for (int i = 0; i < myMesh.normals.Length; i++)
{
Vector3 normal = myMesh.normals[i];
}
}

在這種情況下很容易既可以減少分配:我們可以簡單地緩存對數組的引用。當我們這樣做時,只創建了一個數組,並相應地減少了創建的垃圾數量。 下面代碼說明了這一點。在這種情況下,我們循環運行之前調用Mesh.normals,並緩存了引用,以便只創建一個數組。
void ExampleFunction()
{
Vector3[] meshNormals = myMesh.normals;
for (int i = 0; i < meshNormals.Length; i++)
{
Vector3 normal = meshNormals[i];
}
}

另一個不可預期的堆分配可以在函數GameObject.name或者GameObject.tag中出現。這兩個都是訪問新字符串的訪問器,這意味著調用這些函數將生成垃圾。緩存這個值可能很有用,但是在這種情況下,我們可以使用一個相關的Unity函數去代替。為了在不產生垃圾的情況下檢查GameObject的標簽值,我們可以使用GameObject.CompareTag()。 在下面的示例代碼中,調用GameObject.tag的時候創建垃圾。
private string playerTag = "Player";
 
void OnTriggerEnter(Collider other)
{
bool isPlayer = other.gameObject.tag == playerTag;
}

如果我們使用GameObject.CompareTag(),這個函數將不再產生任何垃圾:
private string playerTag = "Player";
 
void OnTriggerEnter(Collider other)
{
bool isPlayer = other.gameObject.CompareTag(playerTag);
}

GameObject.CompareTag不是唯一的例子:許多Unity函數調用都有不導致堆分配的替代版本。例如,我們可以使用Input.GetTouch()和Input.touchCount代替Input.touches,或者Physics.SphereCastNonAlloc()代替Physics.ShpereCastAll()。

裝箱(boxing

裝箱是當使用值類型變量代替引用類型變量時發生的操作。裝箱通常發生在我們將值類型變量(如int或float)傳遞給帶有引用類型參數參數時。 例如,函數String.Format()接收一個字符串和一個對象參數。當我們傳遞一個字符串和一個int時,這個int必須裝箱。因此,下面的代碼包含了裝箱的一個例子:
void ExampleFunction()
{
int cost = 5;
string displayString = String.Format("Price: {0} gold", cost);
}

由於幕後發生的事情,裝箱會產生垃圾。當一個值類型變量被裝箱時,Unity會在堆上臨時創建一個包裝了值類型的System.Object類型的變量,因此當這個臨時對象被丟棄時,就會創建垃圾。 裝箱是造成不必要堆分配的一個非常常見的原因。即使我們不直接在代碼中對變量進行裝箱,我們也可能使用了導致裝箱的插件,或者在其他函數的幕後進行裝箱。最佳實踐建議是盡可能避免裝箱,並刪除導致裝箱的任何函數調用。

協程(Cotoutines)

調用StartCoroutine()會創建少量垃圾,因為Unity必須創建實例來管理這個協程。考慮到這一點,在我們的遊戲是交互的並且性能要求需要考慮時,應該限制對StartCoroutine()的調用。為了減少以這種方式創建的垃圾,必須在性能關鍵時刻運行的任何協程都應該提前啟動,在使用嵌套的協程時,我們應該特別小心,因為它可能包含對StartCoroutine()的延遲調用。 協程中的yield語句本身不會創建堆分配:但是,使用yield語句傳遞的值可能會創建不必要的堆分配。例如,下面的代碼會創建垃圾:
yield return 0;

這段代碼創建了垃圾,因為值為0的int類型被裝箱了。在這種情況下,如果我們希望簡單地等待一個幀而不引起任何堆分配,那麽最好的方法是使用以下代碼:
yield return null;

協同程序的另一個常見失誤是在多次使用相同值時使用new。例如,下面的代碼將在每次循環叠代時創建並釋放WaitForSeconds對象:
while (!isComplete)
{
yield return new WaitForSeconds(1f);
}

如果緩存和重用WaitForSeconds對象,則創建的垃圾會少很多。下面的代碼說明改進的用法:
WaitForSeconds delay = new WaitForSeconds(1f);
 
while (!isComplete)
{
yield return delay;
}

如果我們的代碼由於協程而產生大量垃圾,我們可能會考慮重構代碼以使用協程之外的其他方法。重構代碼是一個復雜的主題,每個項目都是獨特的,但是我們可能希望記住,協程有一個常見的替代方案。例如,如果我們主要使用協程來管理時間,我們可能希望在Update()函數中簡單地跟蹤時間。如果我們使用協程主要是為了控制遊戲中事情發生的順序,我們可能希望創建某種消息傳遞系統來允許對象進行通信。對於這一點,沒有一種適合所有人的方法,但是請記住,在代碼中實現同一目標的方法通常不止一種。

foreach循環

在5.5之前的Unity版本中,foreach循環在每次結束時,遍歷除數組之外的任何東西都會產生垃圾。這是由於裝箱只在幕後進行的。一個System.Object在循環開始時分配到堆上,在循環結束時釋放。這個問題在Unity 5.5中修復了。 例如,在5.5之前的Unity版本中,下面代碼中的循環會產生垃圾:
void ExampleFunction(List listOfInts)
{
foreach (int currentInt in listOfInts)
{
DoSomething(currentInt);
}
}

如果我們不能升級我們的Unity版本,有一個簡單的解決方案。for和while循環不會在幕後導致裝箱,因此不會生成任何垃圾。在遍歷非數組的集合時,我們應該支持使用它們。 下面代碼中的循環不會生成垃圾:
void ExampleFunction(List listOfInts)
{
for (int i = 0; i < listOfInts.Count; i ++)
{
int currentInt = listOfInts[i];
DoSomething(currentInt);
}
}

函數引用

對函數的引用,無論是引用匿名方法還是命名方法,在Unity中都是引用類型變量。它們將導致堆分配。將匿名方法轉換為閉包(在閉包中,匿名方法在創建時可以訪問作用域中的變量)會顯著增加內存使用和堆分配的數量。 函數引用和閉包如何分配內存的精確細節取決於平臺和編譯器的設置,但是如果垃圾回收是一個需要考慮的問題,那麽最好在遊戲過程中盡量減少函數引用和閉包的使用。這裏有一篇文章更詳細地描述了有關這個這方面內容的技術細節。

LINQ和正則表達式

LINQ和正則表達式都會因為幕後的裝箱而生成垃圾。最好避免在需要考慮性能的情況下使用它們。同樣,這裏也有一篇關於這個主題的更多細節描述的文章。

構造代碼以達到最小化垃圾回收的影響

我們的代碼的結構方式會影響垃圾回收。即使我們的代碼沒有創建堆分配,它也會增加垃圾回收器的工作負擔。 我們的代碼不必要地增加垃圾回收器的工作負擔的一種方式是要求它檢查它不應該檢查的東西。結構體是值類型變量,但是如果我們有一個包含引用類型的結構體,那麽垃圾回收器必須檢查整個結構體。如果我們有大量這樣的結構體,那麽這會為垃圾回收器增加大量額外的工作。 在這個例子中,結構體包含一個引用類型的字符串。垃圾回收器在運行時必須檢查整個結構體數組。
public struct ItemData
{
public string name;
public int cost;
public Vector3 position;
}

private ItemData[] itemData;

在下面這個例子中,我們將數據存儲在不同的數組中。當垃圾回收器運行時,它只需要檢查字符串數組,並可以忽略其他數組。這減少了垃圾回收器必須要做的工作。
private string[] itemNames;
private int[] itemCosts;
private Vector3[] itemPositions;

代碼不必要地在增加垃圾回收器工作負擔的另一種方式是使用不必要的對象引用。當垃圾回收器搜索堆上對象的引用時,它必須檢查代碼中的每個當前對象的引用。代碼中對象引用的減少意味著它要做的工作更少,即使我們不減少堆上對象的總數。 在這個例子中,我們有一個彈出對話框的類。當用戶查看該對話框時,將顯示另一個對話框。我們的代碼包含對下一個應該顯示的DialogData實例的引用,這意味著垃圾回收器必須將該引用作為其操作的一部分進行檢查。
public class DialogData
{
private DialogData nextDialog;
 
public DialogData GetNextDialog()
{
return nextDialog;
}
}

在這裏,我們重新構造了代碼,使其返回一個標識符,用於查找DialogData的下一個實例,而不是實例本身。這不是一個對象引用,因此它不會增加垃圾回收器所花費的時間。
public class DialogData
{
private int nextDialogID;
 
public int GetNextDialogID()
{
return nextDialogID;
}
}

就其本身而言,這個示例相當簡單。但是,如果我們的遊戲包含大量對象,這些對象包含對其他對象的引用,那麽我們可以通過以這種方式重構代碼來大大降低堆的復雜性。

手動強制垃圾回收

最後,我們可能希望自己觸發垃圾回收。如果我們知道堆內存被分配,但不再使用(例如,假如我i們的代碼在加載資源的時候生成垃圾),並且我們知道垃圾回收凍結也不會影響玩家(例如,加載屏幕仍然顯示),我們可以使用下面的代碼請求垃圾回收:
System.GC.Collect();

這將強制垃圾回收器運行,在我們方便的時候釋放未使用的內存。

總結

我們已經學習了Unity中的垃圾回收是如何工作的,為什麽垃圾回收會導致性能問題,以及如何最小化垃圾回收對遊戲的影響。利用這些知識和我們的分析工具,我們可以修復與垃圾回收相關的性能問題,並構建遊戲的結構,從而有效地管理內存。 下面的鏈接提供了關於本文主題的進一步闡述。

延伸閱讀

Unity中的內存管理和垃圾回收
  1. Unity Manual: Understanding Optimization in Unity
  2. Unity Manual: Understanding Automatic Memory Management
  3. Gamasutra: C# Memory Management for Unity Developers by Wendelin Reich
  4. Gamasutra: C# memory and performance tips for Unity by Robert Zubek
  5. Gamasutra: Reducing memory allocations to avoid Garbage Collection on Unity by Grhyll JDD
  6. Gamasutra: Unity Garbage Collection Tips and Tricks by Megan Hughes
裝箱
  1. MSDN: Boxing and Unboxing (C# Programming Guide)
對象池
  1. Unity Learn: Object Pooling Tutorial
  2. Wikipedia: Object Pool Pattern
字符串
  1. Best Practices for Using Strings in the .NET Framework

Unity優化方向——優化Unity遊戲中的垃圾回收