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