深入淺出聊Unity3D項(xiàng)目?jī)?yōu)化:從Draw Calls到GC

? ? ? ?提Unity3D項(xiàng)目?jī)?yōu)化則必提DrawCall,這自然沒(méi)錯(cuò),但也有很不好影響。因?yàn)檫@會(huì)給人一個(gè)錯(cuò)誤的認(rèn)識(shí):所謂的優(yōu)化就是把DrawCall弄的比較低就對(duì)了。

對(duì)優(yōu)化有這種第一印象的人不在少數(shù),drawcall的確是一個(gè)很重要的指標(biāo),但絕非全部。為了讓各位和匹夫能達(dá)成盡可能多的共識(shí),匹夫首先介紹一下本文可能會(huì)涉及到的幾個(gè)概念,之后會(huì)提出優(yōu)化所涉及的三大方面:

drawcall是啥?其實(shí)就是對(duì)底層圖形程序(比如:OpenGL ES)接口的調(diào)用,以在屏幕上畫(huà)出東西。所以,是誰(shuí)去調(diào)用這些接口呢?CPU。

fragment是啥?經(jīng)常有人說(shuō)vf啥的,vertex我們都知道是頂點(diǎn),那fragment是啥呢?說(shuō)它之前需要先說(shuō)一下像素,像素各位應(yīng)該都知道吧?像素是構(gòu)成數(shù)碼影像的基本單元呀。那fragment呢?是有可能成為像素的東西。啥叫有可能?就是最終會(huì)不會(huì)被畫(huà)出來(lái)不一定,是潛在的像素。這會(huì)涉及到誰(shuí)呢?GPU。

batching是啥?都知道批處理是干嘛的吧?沒(méi)錯(cuò),將批處理之前需要很多次調(diào)用(drawcall)的物體合并,之后只需要調(diào)用一次底層圖形程序的接口就行。聽(tīng)上去這簡(jiǎn)直就是優(yōu)化的終極方案?。〉?,理想是美好的,世界是殘酷的,一些不足之后我們?cè)偌?xì)聊。

內(nèi)存的分配:記住,除了Unity3D自己的內(nèi)存損耗。我們可是還帶著Mono呢啊,還有托管的那一套東西呢。更別說(shuō)你一激動(dòng),又引入了自己的幾個(gè)dll。這些都是內(nèi)存開(kāi)銷(xiāo)上需要考慮到的。

好啦,文中的幾個(gè)概念提前講清楚了,其實(shí)各位也能看的出來(lái)匹夫接下來(lái)要說(shuō)的匹夫關(guān)注的優(yōu)化時(shí)需要注意的方面:

CPU方面

GPU方面

內(nèi)存方面

所以,這篇文章也會(huì)按照CPU—->GPU—->內(nèi)存的順序進(jìn)行。

CPU的方面的優(yōu)化:

上文中說(shuō)了,drawcall影響的是CPU的效率,而且也是最知名的一個(gè)優(yōu)化點(diǎn)。但是除了drawcall之外,還有哪些因素也會(huì)影響到CPU的效率呢?讓我們一一列出暫時(shí)能想得到的:

DrawCalls

物理組件(Physics)

GC(什么?GC不是處理內(nèi)存問(wèn)題的嘛?匹夫你不要騙我啊!不過(guò),匹夫也要提醒一句,GC是用來(lái)處理內(nèi)存的,但是是誰(shuí)使用GC去處理內(nèi)存的呢?)

當(dāng)然,還有代碼質(zhì)量

DrawCalls:

前面說(shuō)過(guò)了,DrawCall是CPU調(diào)用底層圖形接口。比如有上千個(gè)物體,每一個(gè)的渲染都需要去調(diào)用一次底層接口,而每一次的調(diào)用CPU都需要做很多工作,那么CPU必然不堪重負(fù)。但是對(duì)于GPU來(lái)說(shuō),圖形處理的工作量是一樣的。所以對(duì)DrawCall的優(yōu)化,主要就是為了盡量解放CPU在調(diào)用圖形接口上的開(kāi)銷(xiāo)。所以針對(duì)drawcall我們主要的思路就是每個(gè)物體盡量減少渲染次數(shù),多個(gè)物體最好一起渲染。所以,按照這個(gè)思路就有了以下幾個(gè)方案:

使用Draw Call Batching,也就是描繪調(diào)用批處理。Unity在運(yùn)行時(shí)可以將一些物體進(jìn)行合并,從而用一個(gè)描繪調(diào)用來(lái)渲染他們。具體下面會(huì)介紹。

通過(guò)把紋理打包成圖集來(lái)盡量減少材質(zhì)的使用。

盡量少的使用反光啦,陰影啦之類(lèi)的,因?yàn)槟菚?huì)使物體多次渲染。

Draw Call Batching

首先我們要先理解為何2個(gè)沒(méi)有使用相同材質(zhì)的物體即使使用批處理,也無(wú)法實(shí)現(xiàn)Draw Call數(shù)量的下降和性能上的提升。

因?yàn)楸弧芭幚怼钡?個(gè)物體的網(wǎng)格模型需要使用相同材質(zhì)的目的,在于其紋理是相同的,這樣才可以實(shí)現(xiàn)同時(shí)渲染的目的。因而保證材質(zhì)相同,是為了保證被渲染的紋理相同。

因此,為了將2個(gè)紋理不同的材質(zhì)合二為一,我們就需要進(jìn)行上面列出的第二步,將紋理打包成圖集。具體到合二為一這種情況,就是將2個(gè)紋理合成一個(gè)紋理。這樣我們就可以只用一個(gè)材質(zhì)來(lái)代替之前的2個(gè)材質(zhì)了。

而Draw Call Batching本身,也還會(huì)細(xì)分為2種。

Static Batching 靜態(tài)批處理

看名字,猜使用的情景。

靜態(tài)?那就是不動(dòng)的咯。還有呢?額,聽(tīng)上去狀態(tài)也不會(huì)改變,沒(méi)有“生命”,比如山山石石,樓房校舍啥的。那和什么比較類(lèi)似呢?嗯,聰明的各位一定覺(jué)得和場(chǎng)景的屬性很像吧!所以我們的場(chǎng)景似乎就可以采用這種方式來(lái)減少draw call了。

那么寫(xiě)個(gè)定義:只要這些物體不移動(dòng),并且擁有相同的材質(zhì),靜態(tài)批處理就允許引擎對(duì)任意大小的幾何物體進(jìn)行批處理操作來(lái)降低描繪調(diào)用。

那要如何使用靜態(tài)批來(lái)減少Draw Call呢?你只需要明確指出哪些物體是靜止的,并且在游戲中永遠(yuǎn)不會(huì)移動(dòng)、旋轉(zhuǎn)和縮放。想完成這一步,你只需要在檢測(cè)器(Inspector)中將Static復(fù)選框打勾即可,如下圖所示:

至于效果如何呢?

舉個(gè)例子:新建4個(gè)物體,分別是Cube,Sphere, Capsule, Cylinder,它們有不同的網(wǎng)格模型,但是也有相同的材質(zhì)(Default-Diffuse)。

首先,我們不指定它們是static的。Draw Call的次數(shù)是4次,如圖:

我們現(xiàn)在將它們4個(gè)物體都設(shè)為static,在來(lái)運(yùn)行一下:

如圖,Draw Call的次數(shù)變成了1,而Saved by batching的次數(shù)變成了3。

靜態(tài)批處理的好處很多,其中之一就是與下面要說(shuō)的動(dòng)態(tài)批處理相比,約束要少很多。所以一般推薦的是draw call的靜態(tài)批處理來(lái)減少draw call的次數(shù)。那么接下來(lái),我們就繼續(xù)聊聊draw call的動(dòng)態(tài)批處理。

Dynamic Batching 動(dòng)態(tài)批處理

有陰就有陽(yáng),有靜就有動(dòng),所以聊完了靜態(tài)批處理,肯定跟著就要說(shuō)說(shuō)動(dòng)態(tài)批處理了。首先要明確一點(diǎn),Unity3D的draw call動(dòng)態(tài)批處理機(jī)制是引擎自動(dòng)進(jìn)行的,無(wú)需像靜態(tài)批處理那樣手動(dòng)設(shè)置static。我們舉一個(gè)動(dòng)態(tài)實(shí)例化prefab的例子,如果動(dòng)態(tài)物體共享相同的材質(zhì),則引擎會(huì)自動(dòng)對(duì)draw call優(yōu)化,也就是使用批處理。首先,我們將一個(gè)cube做成prefab,然后再實(shí)例化500次,看看draw call的數(shù)量。

for(inti =0; i <500; i++)

{

GameObject cube;

cube = GameObject.Instantiate(prefab)asGameObject;

}

draw call的數(shù)量:

可以看到draw call的數(shù)量為1,而 saved by batching的數(shù)量是499。而這個(gè)過(guò)程中,我們除了實(shí)例化創(chuàng)建物體之外什么都沒(méi)做。不錯(cuò),unity3d引擎為我們自動(dòng)處理了這種情況。

但是有很多童靴也遇到這種情況,就是我也是從prefab實(shí)例化創(chuàng)建的物體,為何我的draw call依然很高呢?這就是匹夫上文說(shuō)的,draw call的動(dòng)態(tài)批處理存在著很多約束。下面匹夫就演示一下,針對(duì)cube這樣一個(gè)簡(jiǎn)單的物體的創(chuàng)建,如果稍有不慎就會(huì)造成draw call飛漲的情況吧。

我們同樣是創(chuàng)建500個(gè)物體,不同的是其中的100個(gè)物體,每個(gè)物體的大小都不同,也就是Scale不同。

for(inti =0; i <500; i++)

{

GameObject cube;

cube = GameObject.Instantiate(prefab)asGameObject;

if(i /100==0)

{

cube.transform.localScale =newVector3(2+ i,2+ i,2+ i);

}

}

draw call的數(shù)量:

我們看到draw call的數(shù)量上升到了101次,而saved by batching的數(shù)量也下降到了399。各位看官可以看到,僅僅是一個(gè)簡(jiǎn)單的cube的創(chuàng)建,如果scale不同,竟然也不會(huì)去做批處理優(yōu)化。這僅僅是動(dòng)態(tài)批處理機(jī)制的一種約束,那我們總結(jié)一下動(dòng)態(tài)批處理的約束,各位也許也能從中找到為何動(dòng)態(tài)批處理在自己的項(xiàng)目中不起作用的原因:

批處理動(dòng)態(tài)物體需要在每個(gè)頂點(diǎn)上進(jìn)行一定的開(kāi)銷(xiāo),所以動(dòng)態(tài)批處理僅支持小于900頂點(diǎn)的網(wǎng)格物體。

如果你的著色器使用頂點(diǎn)位置,法線(xiàn)和UV值三種屬性,那么你只能批處理300頂點(diǎn)以下的物體;如果你的著色器需要使用頂點(diǎn)位置,法線(xiàn),UV0,UV1和切向量,那你只能批處理180頂點(diǎn)以下的物體。

不要使用縮放。分別擁有縮放大小(1,1,1) 和(2,2,2)的兩個(gè)物體將不會(huì)進(jìn)行批處理。

統(tǒng)一縮放的物體不會(huì)與非統(tǒng)一縮放的物體進(jìn)行批處理。

使用縮放尺度(1,1,1) 和 (1,2,1)的兩個(gè)物體將不會(huì)進(jìn)行批處理,但是使用縮放尺度(1,2,1) 和(1,3,1)的兩個(gè)物體將可以進(jìn)行批處理。

使用不同材質(zhì)的實(shí)例化物體(instance)將會(huì)導(dǎo)致批處理失敗。

擁有l(wèi)ightmap的物體含有額外(隱藏)的材質(zhì)屬性,比如:lightmap的偏移和縮放系數(shù)等。所以,擁有l(wèi)ightmap的物體將不會(huì)進(jìn)行批處理(除非他們指向lightmap的同一部分)。

多通道的shader會(huì)妨礙批處理操作。比如,幾乎unity中所有的著色器在前向渲染中都支持多個(gè)光源,并為它們有效地開(kāi)辟多個(gè)通道。

預(yù)設(shè)體的實(shí)例會(huì)自動(dòng)地使用相同的網(wǎng)格模型和材質(zhì)。

所以,盡量使用靜態(tài)的批處理。

物理組件

曾幾何時(shí),匹夫在做一個(gè)策略類(lèi)游戲的時(shí)候需要在單元格上排兵布陣,而要偵測(cè)到哪個(gè)兵站在哪個(gè)格子匹夫選擇使用了射線(xiàn),由于士兵單位很多,而且為了精確每一幀都會(huì)執(zhí)行檢測(cè),那時(shí)候CPU的負(fù)擔(dān)叫一個(gè)慘不忍睹。后來(lái)匹夫果斷放棄了這種做法,并且對(duì)物理組件產(chǎn)生了心理的陰影。

這里匹夫只提2點(diǎn)匹夫感覺(jué)比較重要的優(yōu)化措施:

1.設(shè)置一個(gè)合適的Fixed Timestep。設(shè)置的位置如圖:

那何謂“合適”呢?首先我們要搞明白Fixed Timestep和物理組件的關(guān)系。物理組件,或者說(shuō)游戲中模擬各種物理效果的組件,最重要的是什么呢?計(jì)算啊。對(duì),需要通過(guò)計(jì)算才能將真實(shí)的物理效果展現(xiàn)在虛擬的游戲中。那么Fixed Timestep這貨就是和物理計(jì)算有關(guān)的啦。所以,若計(jì)算的頻率太高,自然會(huì)影響到CPU的開(kāi)銷(xiāo)。同時(shí),若計(jì)算頻率達(dá)不到游戲設(shè)計(jì)時(shí)的要求,有會(huì)影響到功能的實(shí)現(xiàn),所以如何抉擇需要各位具體分析,選擇一個(gè)合適的值。

2.就是不要使用網(wǎng)格碰撞器(mesh collider):為啥?因?yàn)閷?shí)在是太復(fù)雜了。網(wǎng)格碰撞器利用一個(gè)網(wǎng)格資源并在其上構(gòu)建碰撞器。對(duì)于復(fù)雜網(wǎng)狀模型上的碰撞檢測(cè),它要比應(yīng)用原型碰撞器精確的多。標(biāo)記為凸起的(Convex )的網(wǎng)格碰撞器才能夠和其他網(wǎng)格碰撞器發(fā)生碰撞。各位上網(wǎng)搜一下mesh collider的圖片,自然就會(huì)明白了。我們的手機(jī)游戲自然無(wú)需這種性?xún)r(jià)比不高的東西。

當(dāng)然,從性能優(yōu)化的角度考慮,物理組件能少用還是少用為好。

處理內(nèi)存,卻讓CPU受傷的GC

在CPU的部分聊GC,感覺(jué)是不是怪怪的?其實(shí)小匹夫不這么覺(jué)得,雖然GC是用來(lái)處理內(nèi)存的,但的確增加的是CPU的開(kāi)銷(xiāo)。因此它的確能達(dá)到釋放內(nèi)存的效果,但代價(jià)更加沉重,會(huì)加重CPU的負(fù)擔(dān),因此對(duì)于GC的優(yōu)化目標(biāo)就是盡量少的觸發(fā)GC。

首先我們要明確所謂的GC是Mono運(yùn)行時(shí)的機(jī)制,而非Unity3D游戲引擎的機(jī)制,所以GC也主要是針對(duì)Mono的對(duì)象來(lái)說(shuō)的,而它管理的也是Mono的托管堆。 搞清楚這一點(diǎn),你也就明白了GC不是用來(lái)處理引擎的assets(紋理啦,音效啦等等)的內(nèi)存釋放的,因?yàn)閁3D引擎也有自己的內(nèi)存堆而不是和Mono一起使用所謂的托管堆。

其次我們要搞清楚什么東西會(huì)被分配到托管堆上?不錯(cuò)咯,就是引用類(lèi)型咯。比如類(lèi)的實(shí)例,字符串,數(shù)組等等。而作為int,float,包括結(jié)構(gòu)體struct其實(shí)都是值類(lèi)型,它們會(huì)被分配在堆棧上而非堆上。所以我們關(guān)注的對(duì)象無(wú)外乎就是類(lèi)實(shí)例,字符串,數(shù)組這些了。

那么GC什么時(shí)候會(huì)觸發(fā)呢??jī)煞N情況:

首先當(dāng)然是我們的堆的內(nèi)存不足時(shí),會(huì)自動(dòng)調(diào)用GC。

其次呢,作為編程人員,我們自己也可以手動(dòng)的調(diào)用GC。

所以為了達(dá)到優(yōu)化CPU的目的,我們就不能頻繁的觸發(fā)GC。而上文也說(shuō)了GC處理的是托管堆,而不是Unity3D引擎的那些資源,所以GC的優(yōu)化說(shuō)白了也就是代碼的優(yōu)化。那么匹夫覺(jué)得有以下幾點(diǎn)是需要注意的:

字符串連接的處理。因?yàn)閷蓚€(gè)字符串連接的過(guò)程,其實(shí)是生成一個(gè)新的字符串的過(guò)程。而之前的舊的字符串自然而然就成為了垃圾。而作為引用類(lèi)型的字符串,其空間是在堆上分配的,被棄置的舊的字符串的空間會(huì)被GC當(dāng)做垃圾回收。

盡量不要使用foreach,而是使用for。foreach其實(shí)會(huì)涉及到迭代器的使用,而據(jù)傳說(shuō)每一次循環(huán)所產(chǎn)生的迭代器會(huì)帶來(lái)24 Bytes的垃圾。那么循環(huán)10次就是240Bytes。

不要直接訪(fǎng)問(wèn)gameobject的tag屬性。比如if (go.tag == “human”)最好換成if (go.CompareTag (“human”))。因?yàn)樵L(fǎng)問(wèn)物體的tag屬性會(huì)在堆上額外的分配空間。如果在循環(huán)中這么處理,留下的垃圾就可想而知了。

使用“池”,以實(shí)現(xiàn)空間的重復(fù)利用。

最好不用LINQ的命令,因?yàn)樗鼈儠?huì)分配臨時(shí)的空間,同樣也是GC收集的目標(biāo)。而且我很討厭LINQ的一點(diǎn)就是它有可能在某些情況下無(wú)法很好的進(jìn)行AOT編譯。比如“OrderBy”會(huì)生成內(nèi)部的泛型類(lèi)“OrderedEnumerable”。這在A(yíng)OT編譯時(shí)是無(wú)法進(jìn)行的,因?yàn)樗皇窃贠rderBy的方法中才使用。所以如果你使用了OrderBy,那么在IOS平臺(tái)上也許會(huì)報(bào)錯(cuò)。

代碼?腳本?

聊到代碼這個(gè)話(huà)題,也許有人會(huì)覺(jué)得匹夫多此一舉。因?yàn)榇a質(zhì)量因人而異,很難像上面提到的幾點(diǎn),有一個(gè)明確的評(píng)判標(biāo)準(zhǔn)。也是,公寫(xiě)公有理,婆寫(xiě)婆有理。但是匹夫這里要提到的所謂代碼質(zhì)量是基于一個(gè)前提的:Unity3D是用C++寫(xiě)的,而我們的代碼是用C#作為腳本來(lái)寫(xiě)的,那么問(wèn)題就來(lái)了~腳本和底層的交互開(kāi)銷(xiāo)是否需要考慮呢?也就是說(shuō),我們用Unity3D寫(xiě)游戲的“游戲腳本語(yǔ)言”,也就是C#是由mono運(yùn)行時(shí)托管的。而功能是底層引擎的C++實(shí)現(xiàn)的,“游戲腳本”中的功能實(shí)現(xiàn)都離不開(kāi)對(duì)底層代碼的調(diào)用。那么這部分的開(kāi)銷(xiāo),我們應(yīng)該如何優(yōu)化呢?

1.以物體的Transform組件為例,我們應(yīng)該只訪(fǎng)問(wèn)一次,之后就將它的引用保留,而非每次使用都去訪(fǎng)問(wèn)。這里有人做過(guò)一個(gè)小實(shí)驗(yàn),就是對(duì)比通過(guò)方法GetComponent()獲取Transform組件, 通過(guò)MonoBehavor的transform屬性去取,以及保留引用之后再去訪(fǎng)問(wèn)所需要的時(shí)間:

GetComponent = 619ms

Monobehaviour = 60ms

CachedMB = 8ms

Manual Cache = 3ms

2.如上所述,最好不要頻繁使用GetComponent,尤其是在循環(huán)中。

3.善于使用OnBecameVisible()和OnBecameVisible(),來(lái)控制物體的update()函數(shù)的執(zhí)行以減少開(kāi)銷(xiāo)。

4.使用內(nèi)建的數(shù)組,比如用Vector3.zero而不是new Vector(0, 0, 0);

5.對(duì)于方法的參數(shù)的優(yōu)化:善于使用ref關(guān)鍵字。值類(lèi)型的參數(shù),是通過(guò)將實(shí)參的值復(fù)制到形參,來(lái)實(shí)現(xiàn)按值傳遞到方法,也就是我們通常說(shuō)的按值傳遞。復(fù)制嘛,總會(huì)讓人感覺(jué)很笨重。比如Matrix4x4這樣比較復(fù)雜的值類(lèi)型,如果直接復(fù)制一份新的,反而不如將值類(lèi)型的引用傳遞給方法作為參數(shù)。

好啦,CPU的部分匹夫覺(jué)得到此就介紹的差不多了。下面就簡(jiǎn)單聊聊其實(shí)匹夫并不是十分熟悉的部分,GPU的優(yōu)化。

GPU的優(yōu)化

GPU與CPU不同,所以側(cè)重點(diǎn)自然也不一樣。GPU的瓶頸主要存在在如下的方面:

填充率,可以簡(jiǎn)單的理解為圖形處理單元每秒渲染的像素?cái)?shù)量。

像素的復(fù)雜度,比如動(dòng)態(tài)陰影,光照,復(fù)雜的shader等等

幾何體的復(fù)雜度(頂點(diǎn)數(shù)量)

當(dāng)然還有GPU的顯存帶寬

那么針對(duì)以上4點(diǎn),其實(shí)仔細(xì)分析我們就可以發(fā)現(xiàn),影響的GPU性能的無(wú)非就是2大方面,一方面是頂點(diǎn)數(shù)量過(guò)多,像素計(jì)算過(guò)于復(fù)雜。另一方面就是GPU的顯存帶寬。那么針?shù)h相對(duì)的兩方面舉措也就十分明顯了。

少頂點(diǎn)數(shù)量,簡(jiǎn)化計(jì)算復(fù)雜度。

縮圖片,以適應(yīng)顯存帶寬。

減少繪制的數(shù)目

那么第一個(gè)方面的優(yōu)化也就是減少頂點(diǎn)數(shù)量,簡(jiǎn)化復(fù)雜度,具體的舉措就總結(jié)如下了:

保持材質(zhì)的數(shù)目盡可能少。這使得Unity更容易進(jìn)行批處理。

使用紋理圖集(一張大貼圖里包含了很多子貼圖)來(lái)代替一系列單獨(dú)的小貼圖。它們可以更快地被加載,具有很少的狀態(tài)轉(zhuǎn)換,而且批處理更友好。

如果使用了紋理圖集和共享材質(zhì),使用Renderer.sharedMaterial?來(lái)代替Renderer.material?。

使用光照紋理(lightmap)而非實(shí)時(shí)燈光。

使用LOD,好處就是對(duì)那些離得遠(yuǎn),看不清的物體的細(xì)節(jié)可以忽略。

遮擋剔除(Occlusion culling)

使用mobile版的shader。因?yàn)楹?jiǎn)單。

優(yōu)化顯存帶寬

第二個(gè)方向呢?壓縮圖片,減小顯存帶寬的壓力。

OpenGL ES 2.0使用ETC1格式壓縮等等,在打包設(shè)置那里都有。

使用mipmap。

MipMap

這里匹夫要著重介紹一下MipMap到底是啥。因?yàn)橛腥苏f(shuō)過(guò)MipMap會(huì)占用內(nèi)存呀,但為何又會(huì)優(yōu)化顯存帶寬呢?那就不得不從MipMap是什么開(kāi)始聊起。一張圖其實(shí)就能解決這個(gè)疑問(wèn)。

上面是一個(gè)mipmap 如何儲(chǔ)存的例子,左邊的主圖伴有一系列逐層縮小的備份小圖

是不是很一目了然呢?Mipmap中每一個(gè)層級(jí)的小圖都是主圖的一個(gè)特定比例的縮小細(xì)節(jié)的復(fù)制品。因?yàn)榇媪酥鲌D和它的那些縮小的復(fù)制品,所以?xún)?nèi)存占用會(huì)比之前大。但是為何又優(yōu)化了顯存帶寬呢?因?yàn)榭梢愿鶕?jù)實(shí)際情況,選擇適合的小圖來(lái)渲染。所以,雖然會(huì)消耗一些內(nèi)存,但是為了圖片渲染的質(zhì)量(比壓縮要好),這種方式也是推薦的。

內(nèi)存的優(yōu)化

既然要聊Unity3D運(yùn)行時(shí)候的內(nèi)存優(yōu)化,那我們自然首先要知道Unity3D游戲引擎是如何分配內(nèi)存的。大概可以分成三大部分:

Unity3D內(nèi)部的內(nèi)存

Mono的托管內(nèi)存

若干我們自己引入的DLL或者第三方DLL所需要的內(nèi)存。

第3類(lèi)不是我們關(guān)注的重點(diǎn),所以接下來(lái)我們會(huì)分別來(lái)看一下Unity3D內(nèi)部?jī)?nèi)存和Mono托管內(nèi)存,最后還將分析一個(gè)官網(wǎng)上Assetbundle的案例來(lái)說(shuō)明內(nèi)存的管理。

Unity3D內(nèi)部?jī)?nèi)存

Unity3D的內(nèi)部?jī)?nèi)存都會(huì)存放一些什么呢?各位想一想,除了用代碼來(lái)驅(qū)動(dòng)邏輯,一個(gè)游戲還需要什么呢?對(duì),各種資源。所以簡(jiǎn)單總結(jié)一下Unity3D內(nèi)部?jī)?nèi)存存放的東西吧:

資源:紋理、網(wǎng)格、音頻等等

GameObject和各種組件。

引擎內(nèi)部邏輯需要的內(nèi)存:渲染器,物理系統(tǒng),粒子系統(tǒng)等等

Mono托管內(nèi)存

因?yàn)槲覀兊挠螒蚰_本是用C#寫(xiě)的,同時(shí)還要跨平臺(tái),所以帶著一個(gè)Mono的托管環(huán)境顯然必須的。那么Mono的托管內(nèi)存自然就不得不放到內(nèi)存的優(yōu)化范疇中進(jìn)行考慮。那么我們所說(shuō)的Mono托管內(nèi)存中存放的東西和Unity3D內(nèi)部?jī)?nèi)存中存放的東西究竟有何不同呢?其實(shí)Mono的內(nèi)存分配就是很傳統(tǒng)的運(yùn)行時(shí)內(nèi)存的分配了:

值類(lèi)型:int型啦,float型啦,結(jié)構(gòu)體struct啦,bool啦之類(lèi)的。它們都存放在堆棧上(注意額,不是堆所以不涉及GC)。

引用類(lèi)型:其實(shí)可以狹義的理解為各種類(lèi)的實(shí)例。比如游戲腳本中對(duì)游戲引擎各種控件的封裝。其實(shí)很好理解,C#中肯定要有對(duì)應(yīng)的類(lèi)去對(duì)應(yīng)游戲引擎中的控件。那么這部分就是C#中的封裝。由于是在堆上分配,所以會(huì)涉及到GC。

而Mono托管堆中的那些封裝的對(duì)象,除了在在Mono托管堆上分配封裝類(lèi)實(shí)例化之后所需要的內(nèi)存之外,還會(huì)牽扯到其背后對(duì)應(yīng)的游戲引擎內(nèi)部控件在Unity3D內(nèi)部?jī)?nèi)存上的分配。

舉一個(gè)例子:

一個(gè)在.cs腳本中聲明的WWW類(lèi)型的對(duì)象www,Mono會(huì)在Mono托管堆上為www分配它所需要的內(nèi)存。同時(shí),這個(gè)實(shí)例對(duì)象背后的所代表的引擎資源所需要的內(nèi)存也需要被分配。

一個(gè)WWW實(shí)例背后的資源:

壓縮的文件

解壓縮所需的緩存

解壓縮之后的文件

如圖:

那么下面就舉一個(gè)AssetBundle的例子:

Assetbundle的內(nèi)存處理

以下載Assetbundle為例子,聊一下內(nèi)存的分配。匹夫從官網(wǎng)的手冊(cè)上找到了一個(gè)使用Assetbundle的情景如下:

IEnumerator DownloadAndCache (){

// Wait for the Caching system to be ready

while(!Caching.ready)

yieldreturnnull;

// Load the AssetBundle file from Cache if it exists with the same version or download and store it in the cache

using(WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)){

yieldreturnwww;//WWW是第1部分

if(www.error !=null)

thrownewException("WWW download had an error:"+ www.error);

AssetBundle bundle = www.assetBundle;//AssetBundle是第2部分

if(AssetName =="")

Instantiate(bundle.mainAsset);//實(shí)例化是第3部分

else

Instantiate(bundle.Load(AssetName));

// Unload the AssetBundles compressed contents to conserve memory

bundle.Unload(false);

}// memory is freed from the web stream (www.Dispose() gets called implicitly)

}

}

內(nèi)存分配的三個(gè)部分匹夫已經(jīng)在代碼中標(biāo)識(shí)了出來(lái):

Web Stream:包括了壓縮的文件,解壓所需的緩存,以及解壓后的文件。

AssetBundle:Web Stream中的文件的映射,或者說(shuō)引用。

實(shí)例化之后的對(duì)象就是引擎的各種資源文件了,會(huì)在內(nèi)存中創(chuàng)建出來(lái)。

那就分別解析一下:

WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)

將壓縮的文件讀入內(nèi)存中

創(chuàng)建解壓所需的緩存

將文件解壓,解壓后的文件進(jìn)入內(nèi)存

關(guān)閉掉為解壓創(chuàng)建的緩存

AssetBundle bundle = www.assetBundle;

AssetBundle此時(shí)相當(dāng)于一個(gè)橋梁,從Web Stream解壓后的文件到最后實(shí)例化創(chuàng)建的對(duì)象之間的橋梁。

所以AssetBundle實(shí)質(zhì)上是Web Stream解壓后的文件中各個(gè)對(duì)象的映射。而非真實(shí)的對(duì)象。

實(shí)際的資源還存在Web Stream中,所以此時(shí)要保留Web Stream。

Instantiate(bundle.mainAsset);

通過(guò)AssetBundle獲取資源,實(shí)例化對(duì)象

最后各位可能看到了官網(wǎng)中的這個(gè)例子使用了:

using(WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)){

}

這種using的用法。這種用法其實(shí)就是為了在使用完Web Stream之后,將內(nèi)存釋放掉的。因?yàn)閃WW也繼承了idispose的接口,所以可以使用using的這種用法。其實(shí)相當(dāng)于最后執(zhí)行了:

//刪除Web Stream

www.Dispose();

OK,Web Stream被刪除掉了。那還有誰(shuí)呢?對(duì)Assetbundle。那么使用

//刪除AssetBundle

bundle.Unload(false);


使用Unity Profiler工具檢測(cè)內(nèi)存

這篇文章當(dāng)時(shí)寫(xiě)的時(shí)候略顯倉(cāng)促,因此并沒(méi)有特別介紹Unity Profiler工具,也更談不上用Unity Profiler工具來(lái)監(jiān)測(cè)內(nèi)存的使用狀態(tài)了。但是使用Unity Profiler工具來(lái)監(jiān)測(cè)還是十分必要的,下面就簡(jiǎn)單補(bǔ)充一下這方面的知識(shí)。

在Profiler工具中提供了兩種模式供我們監(jiān)測(cè)內(nèi)存的使用情況,即簡(jiǎn)易模式和詳細(xì)模式。在簡(jiǎn)易模式中,我們可以看到總的內(nèi)存(total)列出了兩列,即Used Total(使用總內(nèi)存)Reserved Total(預(yù)定總內(nèi)存)。Used Total和Reserved 均是物理內(nèi)存,其中Reserved是unity向系統(tǒng)申請(qǐng)的總內(nèi)存,Unity底層為了不經(jīng)常向系統(tǒng)申請(qǐng)開(kāi)辟內(nèi)存,開(kāi)啟了較大一塊內(nèi)存作為緩存,即所謂的Reserved內(nèi)存,而運(yùn)行時(shí),unity所使用的內(nèi)存首先是向Reserved中來(lái)申請(qǐng)內(nèi)存,當(dāng)不使用時(shí)也是先向Reserved中釋放內(nèi)存,從而來(lái)保證游戲運(yùn)行的流暢性。一般來(lái)說(shuō),Used Total越大,則Reserved Total越大,而當(dāng)Used Total降下去后,Reserved Total也是會(huì)隨之下降的(但并不一定與Used Total同步)。

Unity3D的內(nèi)存從大體上可以分為以下幾個(gè)部分:

Unity:位Unity3D的底層代碼所分配的內(nèi)存。

Mono:即托管堆。Mono運(yùn)行時(shí)在運(yùn)行游戲腳本時(shí)所需要的內(nèi)存,換句話(huà)說(shuō)托管堆的大小與我們的GameObject數(shù)量、資源量無(wú)關(guān),僅是腳本代碼造成的。這部分內(nèi)存是有垃圾回收機(jī)制的。

GfxDriver:可以理解為GPU顯存開(kāi)銷(xiāo),主要由Texture,Vertex buffer以及index buffer組成。所以盡可能地減少或釋放Texture和mesh等資源,即可降低GfxDriver內(nèi)存。

FMOD:音頻的內(nèi)存開(kāi)銷(xiāo)。

Profiler

而在簡(jiǎn)易模式下的監(jiān)視器最下方,則列出了常見(jiàn)的一些資源以及它們所消耗的內(nèi)存。

紋理

網(wǎng)格

材質(zhì)

動(dòng)作

音頻

游戲?qū)ο蟮臄?shù)量

而詳細(xì)模式則需要點(diǎn)擊“Take Sample”按鈕來(lái)捕獲詳細(xì)的內(nèi)存使用情況。需要注意的是,由于獲得數(shù)據(jù)需要花費(fèi)一定的時(shí)間,因此我們無(wú)法獲得實(shí)時(shí)的詳細(xì)內(nèi)存的使用情況。在詳細(xì)模式中,我們可以觀(guān)察每個(gè)具體資源和游戲?qū)ο蟮膬?nèi)存使用情況。

若需轉(zhuǎn)載請(qǐng)保留原文鏈接http://www.cnblogs.com/murongxiaopifu/p/4284988.html)及作者信息慕容小匹夫

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容