當建立對象、字符串或數組時,存儲它所需的內存將從稱爲堆的中央池中分配。當項目再也不使用時,它曾經佔用的內存能夠被回收並用於別的東西。在過去,一般由程序員經過適當的函數調用明確地分配和釋放這些堆內存塊。現在,像Unity的Mono引擎這樣的運行時系統會自動爲您管理內存。自動內存管理須要比顯式分配/釋放更少的編碼工做,並大大下降內存泄漏(內存被分配但從未隨後釋放的狀況)的可能性。html
當調用一個函數時,它的參數值將被複制到一個保留特定調用的內存區域。只佔用幾個字節的數據類型能夠很是快速方便地複製。然而,對象、字符串和數組要大得多,若是這些類型的數據被按期複製,那將是很是低效的。幸運的是,這是沒必要要的;大項目的實際存儲空間是從堆中分配的,一個小的「指針」值用來記住它的位置。從那時起,只有指針在參數傳遞過程當中須要被複制。只要運行時系統可以定位指針標識的項,就能夠常用數據的一個副本。
在參數傳遞期間直接存儲和複製的類型稱爲值類型。這些包括整數,浮點數,布爾和Unity的結構類型(例如Color和Vector3)。分配在堆上而後經過指針訪問的類型稱爲引用類型,由於存儲在變量中的值僅僅是「引用」到真實數據。引用類型的示例包括對象,字符串和數組。git
內存管理器跟蹤它知道未被使用的堆中的區域。當請求一個新的內存塊時(例如當一個對象被實例化時),管理器選擇一個未使用的區域,從中分配該塊,而後從已知的未使用的空間中移除分配的內存。後續請求以相同的方式處理,直到沒有足夠大的空閒區域分配所需的塊大小。在這一點上,從堆中分配的全部內存仍然在使用中是很是不可能的。只要還存在能夠找到它的引用變量,就只能訪問堆上的引用項。若是對內存塊的全部引用都消失了(即,引用變量已被從新分配,或者它們是如今超出範圍的局部變量),則它佔用的內存能夠安全地從新分配。
爲了肯定哪些堆塊再也不被使用,內存管理器會搜索全部當前活動的引用變量,並將它們所指的塊標記爲live
。在搜索結束時,內存管理器認爲這些live
塊之間的任何空間都是空的,而且可用於後續分配。因爲顯而易見的緣由,定位和釋放未使用的內存的過程被稱爲垃圾回收(或簡稱GC)。程序員
垃圾收集對程序員來講是自動的、不可見的,可是收集過程實際上須要大量的CPU時間。若是正確使用,自動內存管理一般會等於或擊敗手動分配的總體性能。可是,對於程序員來講,重要的是要避免那些比實際須要觸發更屢次收集器和在執行中引入暫停的錯誤。有一些臭名昭著的算法,多是GC噩夢,儘管他們乍一看是無辜的。重複字符串鏈接是一個典型的例子:github
//C# script example using UnityEngine; using System.Collections; public class ExampleScript : MonoBehaviour { void ConcatExample(int[] intArray) { string line = intArray[0].ToString(); for (i = 1; i < intArray.Length; i++) { line += ", " + intArray[i].ToString(); } return line; } } //JS script example function ConcatExample(intArray: int[]) { var line = intArray[0].ToString(); for (i = 1; i < intArray.Length; i++) { line += ", " + intArray[i].ToString(); } return line; }
這裏的關鍵細節是,新的部分不會被一個接一個地添加到字符串中。實際狀況是,每次循環line
變量的前一個內容都會變死——一個完整的新字符串被分配到包含原來的部分,再在最後加上新的部分。因爲字符串隨着i
值的增長而變得更長,因此所消耗的堆空間數量也增長了,所以每次調用這個函數時都很容易消耗數百字節的空閒堆空間。若是你須要鏈接多個字符串,那麼一個更好的選擇是Mono庫的System.Text.StringBuilder類。然而,即便反覆鏈接也不會引發太多麻煩,除非它被頻繁調用,而在Unity中一般意味着幀更新。就像是:算法
//C# script example using UnityEngine; using System.Collections; public class ExampleScript : MonoBehaviour { public GUIText scoreBoard; public int score; void Update() { string scoreText = "Score: " + score.ToString(); scoreBoard.text = scoreText; } } //JS script example var scoreBoard: GUIText; var score: int; function Update() { var scoreText: String = "Score: " + score.ToString(); scoreBoard.text = scoreText; }
...每次調用Update將分配新字符串,並不斷生成的新垃圾。大多數狀況下,只有當分數變化時才更新文本:數組
//C# script example using UnityEngine; using System.Collections; public class ExampleScript : MonoBehaviour { public GUIText scoreBoard; public string scoreText; public int score; public int oldScore; void Update() { if (score != oldScore) { scoreText = "Score: " + score.ToString(); scoreBoard.text = scoreText; oldScore = score; } } } //JS script example var scoreBoard: GUIText; var scoreText: String; var score: int; var oldScore: int; function Update() { if (score != oldScore) { scoreText = "Score: " + score.ToString(); scoreBoard.text = scoreText; oldScore = score; } }
當函數返回數組值時,會發生另外一個潛在的問題:安全
//C# script example using UnityEngine; using System.Collections; public class ExampleScript : MonoBehaviour { float[] RandomList(int numElements) { var result = new float[numElements]; for (int i = 0; i < numElements; i++) { result[i] = Random.value; } return result; } } //JS script example function RandomList(numElements: int) { var result = new float[numElements]; for (i = 0; i < numElements; i++) { result[i] = Random.value; } return result; }
當建立一個充滿值的新數組時,這種函數很是優雅和方便。可是,若是反覆調用,那麼每次都會分配新的內存。因爲數組可能很是大,可用空間可能會迅速消耗,從而致使垃圾收集頻繁。避免這個問題的一個方法是利用數組是引用類型的事實。做爲參數傳遞給函數的數組能夠在該函數內修改,結果將在函數返回後保留。
像上面這樣的功能一般能夠被替換成:dom
//C# script example using UnityEngine; using System.Collections; public class ExampleScript : MonoBehaviour { void RandomList(float[] arrayToFill) { for (int i = 0; i < arrayToFill.Length; i++) { arrayToFill[i] = Random.value; } } } //JS script example function RandomList(arrayToFill: float[]) { for (i = 0; i < arrayToFill.Length; i++) { arrayToFill[i] = Random.value; } }
這只是用新值替換數組的現有內容。雖然這須要在調用代碼中完成數組的初始分配(這彷佛有些不雅),可是在調用該函數時不會產生任何新的垃圾。函數
如上所述,最好儘可能避免分配。然而,鑑於它們不能被徹底消除,您可使用兩種主要策略來最大限度地減小其入侵遊戲:性能
這個策略一般最適合長期遊戲的遊戲,其中平滑的幀速率是主要的關注點。這樣的遊戲一般會頻繁地分配小塊,但這些塊將僅在短期內使用。在iOS上使用此策略時,典型的堆大小約爲200KB,iPhone 3G上的垃圾收集大約須要5ms。若是堆增長到1MB,則收集大約須要7ms。所以,有時候能夠以規則的幀間隔請求垃圾回收。這一般會使垃圾收集發生的次數比嚴格的須要的更多,可是它們將被快速處理,對遊戲的影響最小:
if (Time.frameCount % 30 == 0) { System.GC.Collect(); }
可是,您應該謹慎使用此技術,並檢查profiler統計信息,以確保它真正減小了遊戲的收集時間。
這個策略對於分配(和所以收集)相對不頻繁並能夠在遊戲暫停期間處理的遊戲最適用。對於堆來講,儘量大,而不是由於系統內存太少而致使操做系統殺死你的應用程序。可是,若是可能,Mono運行時會自動避免擴展堆。您能夠經過在啓動期間預先分配一些佔位符空間來手動擴展堆(即,您實例化一個純粹用於對內存管理器產生影響的「無用」對象):
//C# script example using UnityEngine; using System.Collections; public class ExampleScript : MonoBehaviour { void Start() { var tmp = new System.Object[1024]; // make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocks for (int i = 0; i < 1024; i++) tmp[i] = new byte[1024]; // release reference tmp = null; } } //JS script example function Start() { var tmp = new System.Object[1024]; // make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocks for (var i : int = 0; i < 1024; i++) tmp[i] = new byte[1024]; // release reference tmp = null; }
一個足夠大的堆不該該在遊戲中的暫停期間徹底被填滿,這樣能夠容納一次收集。當發生這樣的暫停時,您能夠顯式地請求垃圾收集:
System.GC.Collect();
一樣,在使用此策略時應該當心,並注意Profiler統計數據,而不是僅僅假定它具備所指望的效果。
不少狀況下,只要減小建立和銷燬對象的數量,就能夠避免生成垃圾。遊戲中存在着某些類型的物體,如拋射體,儘管一次只會有少許的物體在遊戲中,但它們可能會被反覆地遇到。在這種狀況下,經常能夠重用對象,而不是破壞舊對象,並用新的對象替換它們。
內存管理是一個微妙而複雜的課題,它已經投入了大量的學術研究。若是您有興趣瞭解更多信息,那麼memorymanagement.org是一個很好的資源,列出了許多出版物和在線文章。有關對象池的更多信息能夠在維基百科頁面和Sourcemaking.com上找到。
本文做者: Sheh偉偉
本文連接: http://davidsheh.github.io/2017/07/13/「翻譯」理解Unity的自動內存管理/
版權聲明: 本博客全部文章除特別聲明外,均採用 CC BY-NC-SA 3.0 許可協議。轉載請註明出處!