介紹
當咱們的遊戲運行時,它使用內存來存儲數據。當再也不須要該數據時,存儲該數據的內存將被釋放,以即可以重用。垃圾是用來存儲數據但再也不使用的內存的術語。垃圾回收是該內存再次可用以進行重用的進程的名稱。
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()。
裝箱是當使用值類型變量代替引用類型變量時發生的操做。裝箱一般發生在咱們將值類型變量(如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語句傳遞的值可能會建立沒必要要的堆分配。例如,下面的代碼會建立垃圾:
這段代碼建立了垃圾,由於值爲0的int類型被裝箱了。在這種狀況下,若是咱們但願簡單地等待一個幀而不引發任何堆分配,那麼最好的方法是使用如下代碼:
協同程序的另外一個常見失誤是在屢次使用相同值時使用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們的代碼在加載資源的時候生成垃圾),而且咱們知道垃圾回收凍結也不會影響玩家(例如,加載屏幕仍然顯示),咱們可使用下面的代碼請求垃圾回收:
這將強制垃圾回收器運行,在咱們方便的時候釋放未使用的內存。
總結
咱們已經學習了Unity中的垃圾回收是如何工做的,爲何垃圾回收會致使性能問題,以及如何最小化垃圾回收對遊戲的影響。利用這些知識和咱們的分析工具,咱們能夠修復與垃圾回收相關的性能問題,並構建遊戲的結構,從而有效地管理內存。
下面的連接提供了關於本文主題的進一步闡述。
延伸閱讀
Unity中的內存管理和垃圾回收