Unity优化之GC——合理优化Unity的GC
介紹:
在游戲運行的時候,數(shù)據(jù)主要存儲在內(nèi)存中,當游戲的數(shù)據(jù)在不需要的時候,存儲當前數(shù)據(jù)的內(nèi)存就可以被回收以再次使用。內(nèi)存垃圾是指當前廢棄數(shù)據(jù)所占用的內(nèi)存,垃圾回收(GC)是指將廢棄的內(nèi)存重新回收再次使用的過程。
Unity中將垃圾回收當作內(nèi)存管理的一部分,如果游戲中廢棄數(shù)據(jù)占用內(nèi)存較大,則游戲的性能會受到極大影響,此時垃圾回收會成為游戲性能的一大障礙點。
本文我們主要學習垃圾回收的機制,垃圾回收如何被觸發(fā)以及如何提GC收效率來提高游戲的性能。
Unity內(nèi)存管理機制簡介
要想了解垃圾回收如何工作以及何時被觸發(fā),我們首先需要了解unity的內(nèi)存管理機制。Unity主要采用自動內(nèi)存管理的機制,開發(fā)時在代碼中不需要詳細地告訴unity如何進行內(nèi)存管理,unity內(nèi)部自身會進行內(nèi)存管理。這和使用C++開發(fā)需要隨時管理內(nèi)存相比,有一定的優(yōu)勢,當然帶來的劣勢就是需要隨時關(guān)注內(nèi)存的增長,不要讓游戲在手機上跑“飛”了。
unity的自動內(nèi)存管理可以理解為以下幾個部分:
1)unity內(nèi)部有兩個內(nèi)存管理池:堆內(nèi)存和堆棧內(nèi)存。堆棧內(nèi)存(stack)主要用來存儲較小的和短暫的數(shù)據(jù),堆內(nèi)存(heap)主要用來存儲較大的和存儲時間較長的數(shù)據(jù)。
2)unity中的變量只會在堆棧或者堆內(nèi)存上進行內(nèi)存分配,變量要么存儲在堆棧內(nèi)存上,要么處于堆內(nèi)存上。
3)只要變量處于激活狀態(tài),則其占用的內(nèi)存會被標記為使用狀態(tài),則該部分的內(nèi)存處于被分配的狀態(tài)。
4)一旦變量不再激活,則其所占用的內(nèi)存不再需要,該部分內(nèi)存可以被回收到內(nèi)存池中被再次使用,這樣的操作就是內(nèi)存回收。處于堆棧上的內(nèi)存回收及其快速,處于堆上的內(nèi)存并不是及時回收的,此時其對應的內(nèi)存依然會被標記為使用狀態(tài)。
5) 垃圾回收主要是指堆上的內(nèi)存分配和回收,unity中會定時對堆內(nèi)存進行GC操作。
在了解了GC的過程后,下面詳細了解堆內(nèi)存和堆棧內(nèi)存的分配和回收機制的差別。
堆棧內(nèi)存分配和回收機制
堆棧上的內(nèi)存分配和回收十分快捷簡單,因為堆棧上只會存儲短暫的或者較小的變量。內(nèi)存分配和回收都會以一種順序和大小可控制的形式進行。
堆棧的運行方式就像stack: 其本質(zhì)只是一個數(shù)據(jù)的集合,數(shù)據(jù)的進出都以一種固定的方式運行。正是這種簡潔性和固定性使得堆棧的操作十分快捷。當數(shù)據(jù)被存儲在堆棧上的時候,只需要簡單地在其后進行擴展。當數(shù)據(jù)失效的時候,只需要將其從堆棧上移除。
堆內(nèi)存分配和回收機制
堆內(nèi)存上的內(nèi)存分配和存儲相對而言更加復雜,主要是堆內(nèi)存上可以存儲短期較小的數(shù)據(jù),也可以存儲各種類型和大小的數(shù)據(jù)。其上的內(nèi)存分配和回收順序并不可控,可能會要求分配不同大小的內(nèi)存單元來存儲數(shù)據(jù)。
堆上的變量在存儲的時候,主要分為以下幾步:
1)首先,unity檢測是否有足夠的閑置內(nèi)存單元用來存儲數(shù)據(jù),如果有,則分配對應大小的內(nèi)存單元;
2)如果沒有足夠的存儲單元,unity會觸發(fā)垃圾回收來釋放不再被使用的堆內(nèi)存。這步操作是一步緩慢的操作,如果垃圾回收后有足夠大小的內(nèi)存單元,則進行內(nèi)存分配。
3)如果垃圾回收后并沒有足夠的內(nèi)存單元,則unity會擴展堆內(nèi)存的大小,這步操作會很緩慢,然后分配對應大小的內(nèi)存單元給變量。
堆內(nèi)存的分配有可能會變得十分緩慢,特別是在需要垃圾回收和堆內(nèi)存需要擴展的情況下,通常需要減少這樣的操作次數(shù)。
垃圾回收時的操作
當堆內(nèi)存上一個變量不再處于激活狀態(tài)的時候,其所占用的內(nèi)存并不會立刻被回收,不再使用的內(nèi)存只會在GC的時候才會被回收。
每次運行GC的時候,主要進行下面的操作:
1)GC會檢查堆內(nèi)存上的每個存儲變量;
2)對每個變量會檢測其引用是否處于激活狀態(tài);
3)如果變量的引用不再處于激活狀態(tài),則會被標記為可回收;
4)被標記的變量會被移除,其所占有的內(nèi)存會被回收到堆內(nèi)存上。
GC操作是一個極其耗費的操作,堆內(nèi)存上的變量或者引用越多則其運行的操作會更多,耗費的時間越長。
何時會觸發(fā)垃圾回收
主要有三個操作會觸發(fā)垃圾回收:
1) 在堆內(nèi)存上進行內(nèi)存分配操作而內(nèi)存不夠的時候都會觸發(fā)垃圾回收來利用閑置的內(nèi)存;
2) GC會自動的觸發(fā),不同平臺運行頻率不一樣;
3) GC可以被強制執(zhí)行。
特別是在堆內(nèi)存上進行內(nèi)存分配時內(nèi)存單元不足夠的時候,GC會被頻繁觸發(fā),這就意味著頻繁在堆內(nèi)存上進行內(nèi)存分配和回收會觸發(fā)頻繁的GC操作。
GC操作帶來的問題
在了解GC在unity內(nèi)存管理中的作用后,我們需要考慮其帶來的問題。最明顯的問題是GC操作會需要大量的時間來運行,如果堆內(nèi)存上有大量的變量或者引用需要檢查,則檢查的操作會十分緩慢,這就會使得游戲運行緩慢。其次GC可能會在關(guān)鍵時候運行,例如在CPU處于游戲的性能運行關(guān)鍵時刻,此時任何一個額外的操作都可能會帶來極大的影響,使得游戲幀率下降。
另外一個GC帶來的問題是堆內(nèi)存的碎片劃。當一個內(nèi)存單元從堆內(nèi)存上分配出來,其大小取決于其存儲的變量的大小。當該內(nèi)存被回收到堆內(nèi)存上的時候,有可能使得堆內(nèi)存被分割成碎片化的單元。也就是說堆內(nèi)存總體可以使用的內(nèi)存單元較大,但是單獨的內(nèi)存單元較小,在下次內(nèi)存分配的時候不能找到合適大小的存儲單元,這也會觸發(fā)GC操作或者堆內(nèi)存擴展操作。
堆內(nèi)存碎片會造成兩個結(jié)果,一個是游戲占用的內(nèi)存會越來越大,一個是GC會更加頻繁地被觸發(fā)。
分析GC帶來的問題
GC操作帶來的問題主要表現(xiàn)為幀率運行低,性能間歇中斷或者降低。如果游戲有這樣的表現(xiàn),則首先需要打開unity中的profiler window來確定是否是GC造成。
了解如何運用profiler window,可以參考此處,如果游戲確實是由GC造成的,可以繼續(xù)閱讀下面的內(nèi)容。
分析堆內(nèi)存的分配
如果GC造成游戲的性能問題,我們需要知道游戲中的哪部分代碼會造成GC,內(nèi)存垃圾在變量不再激活的時候產(chǎn)生,所以首先我們需要知道堆內(nèi)存上分配的是什么變量。
堆內(nèi)存和堆棧內(nèi)存分配的變量類型
在Unity中,值類型變量都在堆棧上進行內(nèi)存分配,其他類型的變量都在堆內(nèi)存上分配。如果你不知道值類型和引用類型的差別,可以查看此處
下面的代碼可以用來理解值類型的分配和釋放,其對應的變量在函數(shù)調(diào)用完后會立即回收:
void ExampleFunciton() {int localInt = 5; }對應的引用類型的參考代碼如下,其對應的變量在GC的時候才回收:
void ExampleFunction() {List localList = new List(); }利用profiler window 來檢測堆內(nèi)存分配:
我們可以在profier window中檢查堆內(nèi)存的分配操作:在CPU usage分析窗口中,我們可以檢測任何一幀cpu的內(nèi)存分配情況。其中一個選項是GC Alloc,通過分析其來定位是什么函數(shù)造成大量的堆內(nèi)存分配操作。一旦定位該函數(shù),我們就可以分析解決其造成問題的原因從而減少內(nèi)存垃圾的產(chǎn)生。現(xiàn)在Unity5.5的版本,還提供了deep profiler的方式深度分析GC垃圾的產(chǎn)生。
降低GC的影響的方法
大體上來說,我們可以通過三種方法來降低GC的影響:
1)減少GC的運行次數(shù);
2)減少單次GC的運行時間;
3)將GC的運行時間延遲,避免在關(guān)鍵時候觸發(fā),比如可以在場景加載的時候調(diào)用GC
似乎看起來很簡單,基于此,我們可以采用三種策略:
1)對游戲進行重構(gòu),減少堆內(nèi)存的分配和引用的分配。更少的變量和引用會減少GC操作中的檢測個數(shù)從而提高GC的運行效率。
2)降低堆內(nèi)存分配和回收的頻率,尤其是在關(guān)鍵時刻。也就是說更少的事件觸發(fā)GC操作,同時也降低堆內(nèi)存的碎片化。
3)我們可以試著測量GC和堆內(nèi)存擴展的時間,使其按照可預測的順序執(zhí)行。當然這樣操作的難度極大,但是這會大大降低GC的影響。
減少內(nèi)存垃圾的數(shù)量
減少內(nèi)存垃圾主要可以通過一些方法來減少:
緩存
如果在代碼中反復調(diào)用某些造成堆內(nèi)存分配的函數(shù)但是其返回結(jié)果并沒有使用,這就會造成不必要的內(nèi)存垃圾,我們可以緩存這些變量來重復利用,這就是緩存。
例如下面的代碼每次調(diào)用的時候就會造成堆內(nèi)存分配,主要是每次都會分配一個新的數(shù)組:
void OnTriggerEnter(Collider other) {Renderer[] allRenderers = FindObjectsOfType<Renderer>();ExampleFunction(allRenderers); }對比下面的代碼,只會生產(chǎn)一個數(shù)組用來緩存數(shù)據(jù),實現(xiàn)反復利用而不需要造成更多的內(nèi)存垃圾:
private Renderer[] allRenderers;void Start() {allRenderers = FindObjectsOfType<Renderer>(); }void OnTriggerEnter(Collider other) {ExampleFunction(allRenderers); }不要在頻繁調(diào)用的函數(shù)中反復進行堆內(nèi)存分配
在MonoBehaviour中,如果我們需要進行堆內(nèi)存分配,最壞的情況就是在其反復調(diào)用的函數(shù)中進行堆內(nèi)存分配,例如Update()和LateUpdate()函數(shù)這種每幀都調(diào)用的函數(shù),這會造成大量的內(nèi)存垃圾。我們可以考慮在Start()或者Awake()函數(shù)中進行內(nèi)存分配,這樣可以減少內(nèi)存垃圾。
下面的例子中,update函數(shù)會多次觸發(fā)內(nèi)存垃圾的產(chǎn)生:
void Update() {ExampleGarbageGenerationFunction(transform.position.x); }通過一個簡單的改變,我們可以確保每次在x改變的時候才觸發(fā)函數(shù)調(diào)用,這樣避免每幀都進行堆內(nèi)存分配:
private float previousTransformPositionX;void Update() {float transformPositionX = transform.position.x;if(transfromPositionX != previousTransformPositionX){ExampleGarbageGenerationFunction(transformPositionX); previousTransformPositionX = trasnformPositionX;} }另外的一種方法是在update中采用計時器,特別是在運行有規(guī)律但是不需要每幀都運行的代碼中,例如:
void Update() {ExampleGarbageGeneratiingFunction() }通過添加一個計時器,我們可以確保每隔1s才觸發(fā)該函數(shù)一次:
private float timeSinceLastCalled; private float delay = 1f; void Update() {timSinceLastCalled += Time.deltaTime;if(timeSinceLastCalled > delay){ExampleGarbageGenerationFunction();timeSinceLastCalled = 0f;} }通過這樣細小的改變,我們可以使得代碼運行的更快同時減少內(nèi)存垃圾的產(chǎn)生。
附: 不要忽略這一個方法,在最近的項目性能優(yōu)化中,我經(jīng)常采用這樣的方法來優(yōu)化游戲的性能,很多對于固定時間的事件回調(diào)函數(shù)中,如果每次都分配新的緩存,但是在操作完后并不釋放,這樣就會造成大量的內(nèi)存垃圾,對于這樣的緩存,最好的辦法就是當前周期回調(diào)后執(zhí)行清除或者標志為廢棄。
清除鏈表
在堆內(nèi)存上進行鏈表的分配的時候,如果該鏈表需要多次反復的分配,我們可以采用鏈表的clear函數(shù)來清空鏈表從而替代反復多次的創(chuàng)建分配鏈表。
void Update() {List myList = new List();PopulateList(myList); }通過改進,我們可以將該鏈表只在第一次創(chuàng)建或者該鏈表必須重新設(shè)置的時候才進行堆內(nèi)存分配,從而大大減少內(nèi)存垃圾的產(chǎn)生:
private List myList = new List(); void Update() {myList.Clear();PopulateList(myList); }對象池
即便我們在代碼中盡可能地減少堆內(nèi)存的分配行為,但是如果游戲有大量的對象需要產(chǎn)生和銷毀依然會造成GC。對象池技術(shù)可以通過重復使用對象來降低堆內(nèi)存的分配和回收頻率。對象池在游戲中廣泛的使用,特別是在游戲中需要頻繁的創(chuàng)建和銷毀相同的游戲?qū)ο蟮臅r候,例如槍的子彈這種會頻繁生成和銷毀的對象。
要詳細的講解對象池已經(jīng)超出本文的范圍,但是該技術(shù)值得我們深入的研究This tutorial on object pooling on the Unity Learn site對于對象池有詳細深入的講解。
附:對象池技術(shù)屬于游戲中比較通用的技術(shù),如果有閑余時間,大家可以學習一下這方面的知識。
造成不必要的堆內(nèi)存分配的因素
我們已經(jīng)知道值類型變量在堆棧上分配,其他的變量在堆內(nèi)存上分配,但是任然有一些情況下的堆內(nèi)存分配會讓我們感到吃驚。下面讓我們分析一些常見的不必要的堆內(nèi)存分配行為并對其進行優(yōu)化。
字符串
在c#中,字符串是引用類型變量而不是值類型變量,即使看起來它是存儲字符串的值的。這就意味著字符串會造成一定的內(nèi)存垃圾,由于代碼中經(jīng)常使用字符串,所以我們需要對其格外小心。
c#中的字符串是不可變更的,也就是說其內(nèi)部的值在創(chuàng)建后是不可被變更的。每次在對字符串進行操作的時候(例如運用字符串的“加”操作),unity會新建一個字符串用來存儲新的字符串,使得舊的字符串被廢棄,這樣就會造成內(nèi)存垃圾。
我們可以采用以下的一些方法來最小化字符串的影響:
1)減少不必要的字符串的創(chuàng)建,如果一個字符串被多次利用,我們可以創(chuàng)建并緩存該字符串。
2)減少不必要的字符串操作,例如如果在Text組件中,有一部分字符串需要經(jīng)常改變,但是其他部分不會,則我們可以將其分為兩個部分的組件,對于不變的部分就設(shè)置為類似常量字符串即可,見下面的例子。
3)如果我們需要實時的創(chuàng)建字符串,我們可以采用StringBuilderClass來代替,StringBuilder專為不需要進行內(nèi)存分配而設(shè)計,從而減少字符串產(chǎn)生的內(nèi)存垃圾。
4)移除游戲中的Debug.Log()函數(shù)的代碼,盡管該函數(shù)可能輸出為空,對該函數(shù)的調(diào)用依然會執(zhí)行,該函數(shù)會創(chuàng)建至少一個字符(空字符)的字符串。如果游戲中有大量的該函數(shù)的調(diào)用,這會造成內(nèi)存垃圾的增加。
在下面的代碼中,在Update函數(shù)中會進行一個string的操作,這樣的操作就會造成不必要的內(nèi)存垃圾:
public Text timerText; private float timer; void Update() {timer += Time.deltaTime;timerText.text = "Time:"+ timer.ToString(); }通過將字符串進行分隔,我們可以剔除字符串的加操作,從而減少不必要的內(nèi)存垃圾:
public Text timerHeaderText; public Text timerValueText; private float timer; void Start() {timerHeaderText.text = "TIME:"; }void Update() {timerValueText.text = timer.ToString(); }Unity函數(shù)調(diào)用
在代碼編程中,當我們調(diào)用不是我們自己編寫的代碼,無論是Unity自帶的還是插件中的,我們都可能會產(chǎn)生內(nèi)存垃圾。Unity的某些函數(shù)調(diào)用會產(chǎn)生內(nèi)存垃圾,我們在使用的時候需要注意它的使用。
這兒沒有明確的列表指出哪些函數(shù)需要注意,每個函數(shù)在不同的情況下有不同的使用,所以最好仔細地分析游戲,定位內(nèi)存垃圾的產(chǎn)生原因以及如何解決問題。有時候緩存是一種有效的辦法,有時候盡量降低函數(shù)的調(diào)用頻率是一種辦法,有時候用其他函數(shù)來重構(gòu)代碼是一種辦法。現(xiàn)在來分析unity中常見的造成堆內(nèi)存分配的函數(shù)調(diào)用。
在Unity中如果函數(shù)需要返回一個數(shù)組,則一個新的數(shù)組會被分配出來用作結(jié)果返回,這不容易被注意到,特別是如果該函數(shù)含有迭代器,下面的代碼中對于每個迭代器都會產(chǎn)生一個新的數(shù)組:
void ExampleFunction() {for(int i=0; i < myMesh.normals.Length;i++){Vector3 normal = myMesh.normals[i];} }對于這樣的問題,我們可以緩存一個數(shù)組的引用,這樣只需要分配一個數(shù)組就可以實現(xiàn)相同的功能,從而減少內(nèi)存垃圾的產(chǎn)生:
void ExampleFunction() {Vector3[] meshNormals = myMesh.normals;for(int i=0; i < meshNormals.Length;i++){Vector3 normal = meshNormals[i];} }此外另外的一個函數(shù)調(diào)用GameObject.name 或者 GameObject.tag也會造成預想不到的堆內(nèi)存分配,這兩個函數(shù)都會將結(jié)果存為新的字符串返回,這就會造成不必要的內(nèi)存垃圾,對結(jié)果進行緩存是一種有效的辦法,但是在Unity中都對應的有相關(guān)的函數(shù)來替代。對于比較gameObject的tag,可以采用GameObject.CompareTag()來替代。
在下面的代碼中,調(diào)用gameobject.tag就會產(chǎn)生內(nèi)存垃圾:
private string playerTag="Player"; void OnTriggerEnter(Collider other) {bool isPlayer = other.gameObject.tag == playerTag; }采用GameObject.CompareTag()可以避免內(nèi)存垃圾的產(chǎn)生:
private string playerTag = "Player"; void OnTriggerEnter(Collider other) {bool isPlayer = other.gameObject.CompareTag(playerTag); }不只是GameObject.CompareTag,unity中許多其他的函數(shù)也可以避免內(nèi)存垃圾的生成。比如我們可以用Input.GetTouch()和Input.touchCount()來代替Input.touches,或者用Physics.SphereCastNonAlloc()來代替Physics.SphereCastAll()。
裝箱操作
裝箱操作是指一個值類型變量被用作引用類型變量時候的內(nèi)部變換過程,如果我們向帶有對象類型參數(shù)的函數(shù)傳入值類型,這就會觸發(fā)裝箱操作。比如String.Format()函數(shù)需要傳入字符串和對象類型參數(shù),如果傳入字符串和int類型數(shù)據(jù),就會觸發(fā)裝箱操作。如下面代碼所示:
void ExampleFunction() {int cost = 5;string displayString = String.Format("Price:{0} gold",cost); }在Unity的裝箱操作中,對于值類型會在堆內(nèi)存上分配一個System.Object類型的引用來封裝該值類型變量,其對應的緩存就會產(chǎn)生內(nèi)存垃圾。裝箱操作是非常普遍的一種產(chǎn)生內(nèi)存垃圾的行為,即使代碼中沒有直接的對變量進行裝箱操作,在插件或者其他的函數(shù)中也有可能會產(chǎn)生。最好的解決辦法是盡可能的避免或者移除造成裝箱操作的代碼。
協(xié)程
調(diào)用 StartCoroutine()會產(chǎn)生少量的內(nèi)存垃圾,因為unity會生成實體來管理協(xié)程。所以在游戲的關(guān)鍵時刻應該限制該函數(shù)的調(diào)用。基于此,任何在游戲關(guān)鍵時刻調(diào)用的協(xié)程都需要特別的注意,特別是包含延遲回調(diào)的協(xié)程。
yield在協(xié)程中不會產(chǎn)生堆內(nèi)存分配,但是如果yield帶有參數(shù)返回,則會造成不必要的內(nèi)存垃圾,例如:
yield return 0;由于需要返回0,引發(fā)了裝箱操作,所以會產(chǎn)生內(nèi)存垃圾。這種情況下,為了避免內(nèi)存垃圾,我們可以這樣返回:
yield return null;另外一種對協(xié)程的錯誤使用是每次返回的時候都new同一個變量,例如:
while(!isComplete) {yield return new WaitForSeconds(1f); }我們可以采用緩存來避免這樣的內(nèi)存垃圾產(chǎn)生:
WaitForSeconds delay = new WaiForSeconds(1f); while(!isComplete) {yield return delay; }如果游戲中的協(xié)程產(chǎn)生了內(nèi)存垃圾,我們可以考慮用其他的方式來替代協(xié)程。重構(gòu)代碼對于游戲而言十分復雜,但是對于協(xié)程而言我們也可以注意一些常見的操作,比如如果用協(xié)程來管理時間,最好在update函數(shù)中保持對時間的記錄。如果用協(xié)程來控制游戲中事件的發(fā)生順序,最好對于不同事件之間有一定的信息通信的方式。對于協(xié)程而言沒有適合各種情況的方法,只有根據(jù)具體的代碼來選擇最好的解決辦法。
foreach 循環(huán)
在unity5.5以前的版本中,在foreach的迭代中都會生成內(nèi)存垃圾,主要來自于其后的裝箱操作。每次在foreach迭代的時候,都會在堆內(nèi)存上生產(chǎn)一個System.Object用來實現(xiàn)迭代循環(huán)操作。在unity5.5中解決了這個問題,比如,在unity5.5以前的版本中,用foreach實現(xiàn)循環(huán):
void ExampleFunction(List listOfInts) {foreach(int currentInt in listOfInts){DoSomething(currentInt);} }如果游戲工程不能升級到5.5以上,則可以用for或者while循環(huán)來解決這個問題,所以可以改為:
void ExampleFunction(List listOfInts) {for(int i=0; i < listOfInts.Count; i++){int currentInt = listOfInts[i];DoSomething(currentInt);} }函數(shù)引用
函數(shù)的引用,無論是指向匿名函數(shù)還是顯式函數(shù),在unity中都是引用類型變量,這都會在堆內(nèi)存上進行分配。匿名函數(shù)的調(diào)用完成后都會增加內(nèi)存的使用和堆內(nèi)存的分配。具體函數(shù)的引用和終止都取決于操作平臺和編譯器設(shè)置,但是如果想減少GC最好減少函數(shù)的引用。
LINQ和常量表達式
由于LINQ和常量表達式以裝箱的方式實現(xiàn),所以在使用的時候最好進行性能測試。
重構(gòu)代碼來減小GC的影響
即使我們減小了代碼在堆內(nèi)存上的分配操作,代碼也會增加GC的工作量。最常見的增加GC工作量的方式是讓其檢查它不必檢查的對象。struct是值類型的變量,但是如果struct中包含有引用類型的變量,那么GC就必須檢測整個struct。如果這樣的操作很多,那么GC的工作量就大大增加。在下面的例子中struct包含一個string,那么整個struct都必須在GC中被檢查:
public struct ItemData {public string name;public int cost;public Vector3 position; } private ItemData[] itemData;我們可以將該struct拆分為多個數(shù)組的形式,從而減小GC的工作量:
private string[] itemNames; private int[] itemCosts; private Vector3[] itemPositions;另外一種在代碼中增加GC工作量的方式是保存不必要的Object引用,在進行GC操作的時候會對堆內(nèi)存上的object引用進行檢查,越少的引用就意味著越少的檢查工作量。在下面的例子中,當前的對話框中包含一個對下一個對話框引用,這就使得GC的時候會去檢查下一個對象框:
public class DialogData {private DialogData nextDialog;public DialogData GetNextDialog(){return nextDialog;} }通過重構(gòu)代碼,我們可以返回下一個對話框?qū)嶓w的標記,而不是對話框?qū)嶓w本身,這樣就沒有多余的object引用,從而減少GC的工作量:
public class DialogData {private int nextDialogID;public int GetNextDialogID(){return nextDialogID;} }當然這個例子本身并不重要,但是如果我們的游戲中包含大量的含有對其他Object引用的object,我們可以考慮通過重構(gòu)代碼來減少GC的工作量。
定時執(zhí)行GC操作
主動調(diào)用GC操作
如果我們知道堆內(nèi)存在被分配后并沒有被使用,我們希望可以主動地調(diào)用GC操作,或者在GC操作并不影響游戲體驗的時候(例如場景切換的時候),我們可以主動的調(diào)用GC操作:
System.GC.Collect()通過主動的調(diào)用,我們可以主動驅(qū)使GC操作來回收堆內(nèi)存。
總結(jié)
通過本文對于unity中的GC有了一定的了解,對于GC對于游戲性能的影響以及如何解決都有一定的了解。通過定位造成GC問題的代碼以及代碼重構(gòu)我們可以更有效的管理游戲的內(nèi)存。
總結(jié)
以上是生活随笔為你收集整理的Unity优化之GC——合理优化Unity的GC的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 都工作两年了,还不知道浮点数如何转二进制
- 下一篇: 香港十大现货白银交易软件排名(最新版)