0x00 前言
剛開始寫這篇文章的時候選了一個很土的題目毅该。博秫。】粽疲《Unity3D優(yōu)化全解析》挡育。因為這是一篇臨時起意才寫的文章,而且陳述的都是既有的事實朴爬,因而 給自己“文(dou)學(xué)(bi)”加工留下的余地就少了很多即寒。但又覺得這塊是不得不提的一個地方,平時見到很多人對此處也給予了忽略了事召噩,需要時才去網(wǎng)上 扒一些只言片語的資料母赵。也恰逢年前,尋思著周末認真寫點東西遇到節(jié)假日沒準(zhǔn)也沒什么人讀具滴,所以索性就寫了這篇臨時的文章凹嘲。題目很土,因為用了指向性很明確 的“Unity3D”构韵,讓人少了遐(瞎)想的空間施绎,同時用了“高大全”這樣的構(gòu)詞法,也讓匹夫有成為眾矢之的的可能贞绳。谷醉。。所以最后還是改成了現(xiàn)在各位看到 的題目冈闭。話不多說俱尼,下面就開始正文~正所謂“草蛇灰線,伏脈千里”。那咱們首先~~~~~~
0x01 看看優(yōu)化需要從哪里著手萎攒?
匹夫印象里遇到的童靴遇八,提Unity3D項目優(yōu)化則必提DrawCall矛绘,這自然沒錯,但也有很不好影響刃永。因為這會給人一個錯誤的認識:所謂的優(yōu)化就是把DrawCall弄的比較低就對了货矮。
對優(yōu)化有這種第一印象的人不在少數(shù),drawcall的確是一個很重要的指標(biāo)斯够,但絕非全部囚玫。為了讓各位和匹夫能達成盡可能多的共識,匹夫首先介紹一下本文可能會涉及到的幾個概念读规,之后會提出優(yōu)化所涉及的三大方面:
drawcall是啥抓督?其實就是對底層圖形程序(比如:OpenGL ES)接口的調(diào)用,以在屏幕上畫出東西束亏。所以铃在,是誰去調(diào)用這些接口呢?CPU碍遍。
fragment是啥定铜?經(jīng)常有人說vf啥的,vertex我們都知道是頂點怕敬,那fragment是啥呢揣炕?說它之前需要先說一下像素,像素各位應(yīng)該 都知道吧赖捌?像素是構(gòu)成數(shù)碼影像的基本單元呀祝沸。那fragment呢矮烹?是有可能成為像素的東西越庇。啥叫有可能?就是最終會不會被畫出來不一定奉狈,是潛在的像素卤唉。 這會涉及到誰呢?GPU仁期。
batching是啥桑驱?都知道批處理是干嘛的吧?沒錯跛蛋,將批處理之前需要很多次調(diào)用(drawcall)的物體合并熬的,之后只需要調(diào)用一次底層圖形程序的接口就行。聽上去這簡直就是優(yōu)化的終極方案吧藜丁押框!但是,理想是美好的理逊,世界是殘酷的橡伞,一些不足之后我們再細聊盒揉。
內(nèi)存的分配:記住,除了Unity3D自己的內(nèi)存損耗兑徘。我們可是還帶著Mono呢啊刚盈,還有托管的那一套東西呢。更別說你一激動挂脑,又引入了自己的幾個dll藕漱。這些都是內(nèi)存開銷上需要考慮到的。
好啦最域,文中的幾個概念提前講清楚了谴分,其實各位也能看的出來匹夫接下來要說的匹夫關(guān)注的優(yōu)化時需要注意的方面:
CPU方面
GPU方面
內(nèi)存方面
所以,這篇文章也會按照CPU---->GPU---->內(nèi)存的順序進行镀脂。
0x02 CPU的方面的優(yōu)化
上文中說了牺蹄,drawcall影響的是CPU的效率,而且也是最知名的一個優(yōu)化點薄翅。但是除了drawcall之外沙兰,還有哪些因素也會影響到CPU的效率呢?讓我們一一列出暫時能想得到的:
DrawCalls
物理組件(Physics)
GC(什么翘魄?GC不是處理內(nèi)存問題的嘛鼎天?匹夫你不要騙我啊暑竟!不過斋射,匹夫也要提醒一句,GC是用來處理內(nèi)存的但荤,但是是誰使用GC去處理內(nèi)存的呢罗岖?)
當(dāng)然,還有代碼質(zhì)量
DrawCalls:
前面說過了腹躁,DrawCall是CPU調(diào)用底層圖形接口桑包。比如有上千個物體,每一個的渲染都需要去調(diào)用一次底層接口纺非,而每一次的調(diào)用CPU都需要做 很多工作哑了,那么CPU必然不堪重負命迈。但是對于GPU來說寿谴,圖形處理的工作量是一樣的层皱。所以對DrawCall的優(yōu)化旅急,主要就是為了盡量解放CPU在調(diào)用圖 形接口上的開銷碍讨。所以針對drawcall我們主要的思路就是每個物體盡量減少渲染次數(shù)媳荒,多個物體最好一起渲染衷敌。所以武翎,按照這個思路就有了以下幾個方案:
1.使用Draw Call Batching,也就是描繪調(diào)用批處理榜掌。Unity在運行時可以將一些物體進行合并优妙,從而用一個描繪調(diào)用來渲染他們。具體下面會介紹憎账。
2.通過把紋理打包成圖集來盡量減少材質(zhì)的使用套硼。
3.盡量少的使用反光啦,陰影啦之類的胞皱,因為那會使物體多次渲染邪意。
Draw Call Batching
首先我們要先理解為何2個沒有使用相同材質(zhì)的物體即使使用批處理,也無法實現(xiàn)Draw Call數(shù)量的下降和性能上的提升反砌。
因為被“批處理”的2個物體的網(wǎng)格模型需要使用相同材質(zhì)的目的雾鬼,在于其紋理是相同的,這樣才可以實現(xiàn)同時渲染的目的宴树。因而保證材質(zhì)相同策菜,是為了保證被渲染的紋理相同。
因此酒贬,為了將2個紋理不同的材質(zhì)合二為一又憨,我們就需要進行上面列出的第二步,將紋理打包成圖集锭吨。具體到合二為一這種情況蠢莺,就是將2個紋理合成一個紋理。這樣我們就可以只用一個材質(zhì)來代替之前的2個材質(zhì)了零如。
而Draw Call Batching本身躏将,也還會細分為2種。
Static Batching 靜態(tài)批處理
看名字考蕾,猜使用的情景祸憋。
靜態(tài)?那就是不動的咯辕翰。還有呢夺衍?額狈谊,聽上去狀態(tài)也不會改變喜命,沒有“生命”,比如山山石石河劝,樓房校舍啥的壁榕。那和什么比較類似呢?嗯赎瞎,聰明的各位一定覺得和場景的屬性很像吧牌里!所以我們的場景似乎就可以采用這種方式來減少draw call了。
那么寫個定義:只要這些物體不移動,并且擁有相同的材質(zhì)牡辽,靜態(tài)批處理就允許引擎對任意大小的幾何物體進行批處理操作來降低描繪調(diào)用喳篇。
那要如何使用靜態(tài)批來減少Draw Call呢?你只需要明確指出哪些物體是靜止的态辛,并且在游戲中永遠不會移動麸澜、旋轉(zhuǎn)和縮放。想完成這一步奏黑,你只需要在檢測器(Inspector)中將Static復(fù)選框打勾即可炊邦,如下圖所示:
至于效果如何呢?
舉個例子:新建4個物體熟史,分別是Cube馁害,Sphere, Capsule, Cylinder,它們有不同的網(wǎng)格模型,但是也有相同的材質(zhì)(Default-Diffuse)蹂匹。
首先碘菜,我們不指定它們是static的。Draw Call的次數(shù)是4次限寞,如圖:
我們現(xiàn)在將它們4個物體都設(shè)為static炉媒,在來運行一下:
如圖,Draw Call的次數(shù)變成了1昆烁,而Saved by batching的次數(shù)變成了3吊骤。
如圖,Draw Call的次數(shù)變成了1静尼,而Saved by batching的次數(shù)變成了3白粉。
靜態(tài)批處理的好處很多,其中之一就是與下面要說的動態(tài)批處理相比鼠渺,約束要少很多鸭巴。所以一般推薦的是draw call的靜態(tài)批處理來減少draw call的次數(shù)。那么接下來拦盹,我們就繼續(xù)聊聊draw call的動態(tài)批處理鹃祖。
Dynamic Batching 動態(tài)批處理
有陰就有陽,有靜就有動普舆,所以聊完了靜態(tài)批處理恬口,肯定跟著就要說說動態(tài)批處理了。首先要明確一點沼侣,Unity3D的draw call動態(tài)批處理機制是引擎自動進行的祖能,無需像靜態(tài)批處理那樣手動設(shè)置static。我們舉一個動態(tài)實例化prefab的例子蛾洛,如果動態(tài)物體共享相同的 材質(zhì)养铸,則引擎會自動對draw call優(yōu)化,也就是使用批處理。首先钞螟,我們將一個cube做成prefab兔甘,然后再實例化500次,看看draw call的數(shù)量鳞滨。
for(int i = 0; i < 500; i++)
{
GameObject cube;
cube = GameObject.Instantiate(prefab) as GameObject;
}
復(fù)制代碼
draw call的數(shù)量:
可以看到draw call的數(shù)量為1裂明,而 saved by batching的數(shù)量是499。而這個過程中太援,我們除了實例化創(chuàng)建物體之外什么都沒做闽晦。不錯,unity3d引擎為我們自動處理了這種情況提岔。
但是有很多童靴也遇到這種情況仙蛉,就是我也是從prefab實例化創(chuàng)建的物體,為何我的draw call依然很高呢碱蒙?這就是匹夫上文說的荠瘪,draw call的動態(tài)批處理存在著很多約束。下面匹夫就演示一下赛惩,針對cube這樣一個簡單的物體的創(chuàng)建哀墓,如果稍有不慎就會造成draw call飛漲的情況吧。
我們同樣是創(chuàng)建500個物體喷兼,不同的是其中的100個物體篮绰,每個物體的大小都不同,也就是Scale不同季惯。
for(int i = 0; i < 500; i++)
{
GameObject cube;
cube = GameObject.Instantiate(prefab) as GameObject;
if(i / 100 == 0)
{
cube.transform.localScale = new Vector3(2 + i, 2 + i, 2 + i);
}
}
復(fù)制代碼
draw call的數(shù)量:
我們看到draw call的數(shù)量上升到了101次吠各,而saved by batching的數(shù)量也下降到了399。各位看官可以看到勉抓,僅僅是一個簡單的cube的創(chuàng)建贾漏,如果scale不同,竟然也不會去做批處理優(yōu)化藕筋。這僅僅是 動態(tài)批處理機制的一種約束纵散,那我們總結(jié)一下動態(tài)批處理的約束,各位也許也能從中找到為何動態(tài)批處理在自己的項目中不起作用的原因:
1.批處理動態(tài)物體需要在每個頂點上進行一定的開銷隐圾,所以動態(tài)批處理僅支持小于900頂點的網(wǎng)格物體伍掀。
2.如果你的著色器使用頂點位置,法線和UV值三種屬性翎承,那么你只能批處理300頂點以下的物體硕盹;如果你的著色器需要使用頂點位置符匾,法線叨咖,UV0,UV1和切向量,那你只能批處理180頂點以下的物體甸各。
3.不要使用縮放垛贤。分別擁有縮放大小(1,1,1) 和(2,2,2)的兩個物體將不會進行批處理。
4.統(tǒng)一縮放的物體不會與非統(tǒng)一縮放的物體進行批處理趣倾。
5.使用縮放尺度(1,1,1) 和 (1,2,1)的兩個物體將不會進行批處理聘惦,但是使用縮放尺度(1,2,1) 和(1,3,1)的兩個物體將可以進行批處理。
6.使用不同材質(zhì)的實例化物體(instance)將會導(dǎo)致批處理失敗儒恋。
7.擁有l(wèi)ightmap的物體含有額外(隱藏)的材質(zhì)屬性善绎,比如:lightmap的偏移和縮放系數(shù)等。所以诫尽,擁有l(wèi)ightmap的物體將不會進行批處理(除非他們指向lightmap的同一部分)禀酱。
8.多通道的shader會妨礙批處理操作。比如牧嫉,幾乎unity中所有的著色器在前向渲染中都支持多個光源剂跟,并為它們有效地開辟多個通道。
9.預(yù)設(shè)體的實例會自動地使用相同的網(wǎng)格模型和材質(zhì)酣藻。
所以曹洽,盡量使用靜態(tài)的批處理。
物理組件
曾幾何時辽剧,匹夫在做一個策略類游戲的時候需要在單元格上排兵布陣送淆,而要偵測到哪個兵站在哪個格子匹夫選擇使用了射線,由于士兵單位很多怕轿,而且為了精確每一幀都會執(zhí)行檢測坊夫,那時候CPU的負擔(dān)叫一個慘不忍睹。后來匹夫果斷放棄了這種做法撤卢,并且對物理組件產(chǎn)生了心理的陰影环凿。
這里匹夫只提2點匹夫感覺比較重要的優(yōu)化措施:
1.設(shè)置一個合適的Fixed Timestep。設(shè)置的位置如圖:
那何謂“合適”呢放吩?首先我們要搞明白Fixed Timestep和物理組件的關(guān)系智听。物理組件,或者說游戲中模擬各種物理效果的組件渡紫,最重要的是什么呢到推?計算啊。對惕澎,需要通過計算才能將真實的物理效果展 現(xiàn)在虛擬的游戲中莉测。那么Fixed Timestep這貨就是和物理計算有關(guān)的啦。所以唧喉,若計算的頻率太高捣卤,自然會影響到CPU的開銷忍抽。同時,若計算頻率達不到游戲設(shè)計時的要求董朝,有會影響到 功能的實現(xiàn)鸠项,所以如何抉擇需要各位具體分析,選擇一個合適的值子姜。
2.就是不要使用網(wǎng)格碰撞器(mesh collider):為啥祟绊?因為實在是太復(fù)雜了。網(wǎng)格碰撞器利用一個網(wǎng)格資源并在其上構(gòu)建碰撞器哥捕。對于復(fù)雜網(wǎng)狀模型上的碰撞檢測牧抽,它要比應(yīng)用原型碰撞器精 確的多。標(biāo)記為凸起的(Convex )的網(wǎng)格碰撞器才能夠和其他網(wǎng)格碰撞器發(fā)生碰撞遥赚。各位上網(wǎng)搜一下mesh collider的圖片阎姥,自然就會明白了。我們的手機游戲自然無需這種性價比不高的東西鸽捻。
當(dāng)然呼巴,從性能優(yōu)化的角度考慮,物理組件能少用還是少用為好御蒲。
處理內(nèi)存衣赶,卻讓CPU受傷的GC
在CPU的部分聊GC,感覺是不是怪怪的厚满?其實小匹夫不這么覺得府瞄,雖然GC是用來處理內(nèi)存的,但的確增加的是CPU的開銷碘箍。因此它的確能達到釋放內(nèi)存的效果遵馆,但代價更加沉重,會加重CPU的負擔(dān)丰榴,因此對于GC的優(yōu)化目標(biāo)就是盡量少的觸發(fā)GC货邓。
首先我們要明確所謂的GC是Mono運行時的機制,而非Unity3D游戲引擎的機制四濒,所以GC也主要是針對Mono的對象來說的换况,而它管理的也是 Mono的托管堆。 搞清楚這一點盗蟆,你也就明白了GC不是用來處理引擎的assets(紋理啦戈二,音效啦等等)的內(nèi)存釋放的,因為U3D引擎也有自己的內(nèi)存堆而不是和Mono一 起使用所謂的托管堆喳资。
其次我們要搞清楚什么東西會被分配到托管堆上觉吭?不錯咯,就是引用類型咯仆邓。比如類的實例鲜滩,字符串伴鳖,數(shù)組等等。而作為int绒北,float黎侈,包括結(jié)構(gòu)體struct其實都是值類型察署,它們會被分配在堆棧上而非堆上闷游。所以我們關(guān)注的對象無外乎就是類實例,字符串贴汪,數(shù)組這些了脐往。
那么GC什么時候會觸發(fā)呢?兩種情況:
1.首先當(dāng)然是我們的堆的內(nèi)存不足時扳埂,會自動調(diào)用GC业簿。
2.其次呢,作為編程人員阳懂,我們自己也可以手動的調(diào)用GC梅尤。
所以為了達到優(yōu)化CPU的目的,我們就不能頻繁的觸發(fā)GC岩调。而上文也說了GC處理的是托管堆巷燥,而不是Unity3D引擎的那些資源,所以GC的優(yōu)化說白了也就是代碼的優(yōu)化号枕。那么匹夫覺得有以下幾點是需要注意的:
1.字符串連接的處理缰揪。因為將兩個字符串連接的過程,其實是生成一個新的字符串的過程葱淳。而之前的舊的字符串自然而然就成為了垃圾钝腺。而作為引用類型的字符串,其空間是在堆上分配的赞厕,被棄置的舊的字符串的空間會被GC當(dāng)做垃圾回收艳狐。
2.盡量不要使用foreach,而是使用for皿桑。foreach其實會涉及到迭代器的使用僵驰,而據(jù)傳說每一次循環(huán)所產(chǎn)生的迭代器會帶來24 Bytes的垃圾。那么循環(huán)10次就是240Bytes唁毒。
3.不要直接訪問gameobject的tag屬性蒜茴。比如if (go.tag == “human”)最好換成if (go.CompareTag (“human”))。因為訪問物體的tag屬性會在堆上額外的分配空間浆西。如果在循環(huán)中這么處理粉私,留下的垃圾就可想而知了。
4.使用“池”近零,以實現(xiàn)空間的重復(fù)利用诺核。
5.最好不用LINQ的命令抄肖,因為它們會分配臨時的空間,同樣也是GC收集的目標(biāo)窖杀。而且我很討厭LINQ的一點就是它有可能在某些情況下無法很好的進 行AOT編譯漓摩。比如“OrderBy”會生成內(nèi)部的泛型類“OrderedEnumerable”。這在AOT編譯時是無法進行的入客,因為它只是在 OrderBy的方法中才使用管毙。所以如果你使用了OrderBy,那么在IOS平臺上也許會報錯桌硫。
代碼夭咬?腳本?
聊到代碼這個話題铆隘,也許有人會覺得匹夫多此一舉卓舵。因為代碼質(zhì)量因人而異,很難像上面提到的幾點膀钠,有一個明確的評判標(biāo)準(zhǔn)掏湾。也是,公寫公有理肿嘲,婆寫婆有 理融击。但是匹夫這里要提到的所謂代碼質(zhì)量是基于一個前提的:Unity3D是用C++寫的,而我們的代碼是用C#作為腳本來寫的睦刃,那么問題就來了~腳本和底 層的交互開銷是否需要考慮呢砚嘴?也就是說,我們用Unity3D寫游戲的“游戲腳本語言”涩拙,也就是C#是由mono運行時托管的际长。而功能是底層引擎的C++ 實現(xiàn)的,“游戲腳本”中的功能實現(xiàn)都離不開對底層代碼的調(diào)用兴泥。那么這部分的開銷工育,我們應(yīng)該如何優(yōu)化呢?
1.以物體的Transform組件為例搓彻,我們應(yīng)該只訪問一次如绸,之后就將它的引用保留,而非每次使用都去訪問旭贬。這里有人做過一個小實驗怔接,就是對比通過 方法GetComponent()獲取Transform組件, 通過MonoBehavor的transform屬性去取,以及保留引用之后再去訪問所需要的時間:
GetComponent = 619ms
Monobehaviour = 60ms
CachedMB = 8ms
Manual Cache = 3ms
2.如上所述稀轨,最好不要頻繁使用GetComponent扼脐,尤其是在循環(huán)中。
3.善于使用OnBecameVisible()和OnBecameVisible(),來控制物體的update()函數(shù)的執(zhí)行以減少開銷奋刽。
4.使用內(nèi)建的數(shù)組瓦侮,比如用Vector3.zero而不是new Vector(0, 0, 0);
5.對于方法的參數(shù)的優(yōu)化:善于使用ref關(guān)鍵字艰赞。值類型的參數(shù),是通過將實參的值復(fù)制到形參肚吏,來實現(xiàn)按值傳遞到方法方妖,也就是我們通常說的按值傳遞。復(fù)制嘛罚攀,總會讓人感覺很笨重党觅。比如Matrix4x4這樣比較復(fù)雜的值類型,如果直接復(fù)制一份新的坞生,反而不如將值類型的引用傳遞給方法作為參數(shù)仔役。
好啦掷伙,CPU的部分匹夫覺得到此就介紹的差不多了是己。下面就簡單聊聊其實匹夫并不是十分熟悉的部分,GPU的優(yōu)化任柜。
GPU的優(yōu)化
GPU與CPU不同卒废,所以側(cè)重點自然也不一樣。GPU的瓶頸主要存在在如下的方面:
1.填充率宙地,可以簡單的理解為圖形處理單元每秒渲染的像素數(shù)量摔认。
2.像素的復(fù)雜度,比如動態(tài)陰影宅粥,光照参袱,復(fù)雜的shader等等
3.幾何體的復(fù)雜度(頂點數(shù)量)
4.當(dāng)然還有GPU的顯存帶寬
那么針對以上4點,其實仔細分析我們就可以發(fā)現(xiàn)秽梅,影響的GPU性能的無非就是2大方面抹蚀,一方面是頂點數(shù)量過多,像素計算過于復(fù)雜企垦。另一方面就是GPU的顯存帶寬环壤。那么針鋒相對的兩方面舉措也就十分明顯了。
1.減少頂點數(shù)量钞诡,簡化計算復(fù)雜度郑现。
2.壓縮圖片,以適應(yīng)顯存帶寬荧降。
減少繪制的數(shù)目
那么第一個方面的優(yōu)化也就是減少頂點數(shù)量接箫,簡化復(fù)雜度,具體的舉措就總結(jié)如下了:
保持材質(zhì)的數(shù)目盡可能少朵诫。這使得Unity更容易進行批處理辛友。
使用紋理圖集(一張大貼圖里包含了很多子貼圖)來代替一系列單獨的小貼圖。它們可以更快地被加載拗窃,具有很少的狀態(tài)轉(zhuǎn)換瞎领,而且批處理更友好泌辫。
如果使用了紋理圖集和共享材質(zhì),使用Renderer.sharedMaterial 來代替Renderer.material 九默。
使用光照紋理(lightmap)而非實時燈光震放。
使用LOD,好處就是對那些離得遠驼修,看不清的物體的細節(jié)可以忽略殿遂。
遮擋剔除(Occlusion culling)
使用mobile版的shader。因為簡單乙各。
優(yōu)化顯存帶寬
第二個方向呢墨礁?壓縮圖片,減小顯存帶寬的壓力耳峦。
OpenGL ES 2.0使用ETC1格式壓縮等等恩静,在打包設(shè)置那里都有。
使用mipmap蹲坷。
MipMap
這里匹夫要著重介紹一下MipMap到底是啥驶乾。因為有人說過MipMap會占用內(nèi)存呀,但為何又會優(yōu)化顯存帶寬呢循签?那就不得不從MipMap是什么開始聊起级乐。一張圖其實就能解決這個疑問。
上面是一個mipmap 如何儲存的例子县匠,左邊的主圖伴有一系列逐層縮小的備份小圖
是不是很一目了然呢风科?Mipmap中每一個層級的小圖都是主圖的一個特定比例的縮 小細節(jié)的復(fù)制品。因為存了主圖和它的那些縮小的復(fù)制品乞旦,所以內(nèi)存占用會比之前大贼穆。但是為何又優(yōu)化了顯存帶寬呢?因為可以根據(jù)實際情況杆查,選擇適合的小圖來渲 染扮惦。所以,雖然會消耗一些內(nèi)存亲桦,但是為了圖片渲染的質(zhì)量(比壓縮要好)崖蜜,這種方式也是推薦的。
內(nèi)存的優(yōu)化
既然要聊Unity3D運行時候的內(nèi)存優(yōu)化客峭,那我們自然首先要知道Unity3D游戲引擎是如何分配內(nèi)存的豫领。大概可以分成三大部分:
1.Unity3D內(nèi)部的內(nèi)存
2.Mono的托管內(nèi)存
3.若干我們自己引入的DLL或者第三方DLL所需要的內(nèi)存。
第3類不是我們關(guān)注的重點舔琅,所以接下來我們會分別來看一下Unity3D內(nèi)部內(nèi)存和Mono托管內(nèi)存等恐,最后還將分析一個官網(wǎng)上Assetbundle的案例來說明內(nèi)存的管理。
Unity3D內(nèi)部內(nèi)存
Unity3D的內(nèi)部內(nèi)存都會存放一些什么呢?各位想一想课蔬,除了用代碼來驅(qū)動邏輯囱稽,一個游戲還需要什么呢?對二跋,各種資源战惊。所以簡單總結(jié)一下Unity3D內(nèi)部內(nèi)存存放的東西吧:
資源:紋理、網(wǎng)格扎即、音頻等等
GameObject和各種組件吞获。
引擎內(nèi)部邏輯需要的內(nèi)存:渲染器,物理系統(tǒng)谚鄙,粒子系統(tǒng)等等
Mono托管內(nèi)存
因為我們的游戲腳本是用C#寫的各拷,同時還要跨平臺,所以帶著一個Mono的托管環(huán)境顯然必須的闷营。那么Mono的托管內(nèi)存自然就不得不放到內(nèi)存的優(yōu)化 范疇中進行考慮烤黍。那么我們所說的Mono托管內(nèi)存中存放的東西和Unity3D內(nèi)部內(nèi)存中存放的東西究竟有何不同呢?其實Mono的內(nèi)存分配就是很傳統(tǒng)的 運行時內(nèi)存的分配了:
值類型:int型啦粮坞,float型啦蚊荣,結(jié)構(gòu)體struct啦初狰,bool啦之類的莫杈。它們都存放在堆棧上(注意額,不是堆所以不涉及GC)奢入。
引用類型:其實可以狹義的理解為各種類的實例筝闹。比如游戲腳本中對游戲引擎各種控件的封裝。其實很好理解腥光,C#中肯定要有對應(yīng)的類去對應(yīng)游戲引擎中的控件关顷。那么這部分就是C#中的封裝。由于是在堆上分配武福,所以會涉及到GC议双。
而Mono托管堆中的那些封裝的對象,除了在在Mono托管堆上分配封裝類實例化之后所需要的內(nèi)存之外捉片,還會牽扯到其背后對應(yīng)的游戲引擎內(nèi)部控件在Unity3D內(nèi)部內(nèi)存上的分配平痰。
舉一個例子:
一個在.cs腳本中聲明的WWW類型的對象www,Mono會在Mono托管堆上為www分配它所需要的內(nèi)存伍纫。同時宗雇,這個實例對象背后的所代表的引擎資源所需要的內(nèi)存也需要被分配。
一個WWW實例背后的資源:
壓縮的文件
解壓縮所需的緩存
解壓縮之后的文件
如圖:
那么下面就舉一個AssetBundle的例子:
Assetbundle的內(nèi)存處理
以下載Assetbundle為例子莹规,聊一下內(nèi)存的分配赔蒲。匹夫從官網(wǎng)的手冊上找到了一個使用Assetbundle的情景如下:
IEnumerator DownloadAndCache (){
// Wait for the Caching system to be ready
while (!Caching.ready)
yield return null;
// 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)){
yield return www; //WWW是第1部分
if (www.error != null)
throw new Exception("WWW download had an error:" + www.error);
AssetBundle bundle = www.assetBundle;//AssetBundle是第2部分
if (AssetName == "")
Instantiate(bundle.mainAsset);//實例化是第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)
}
}
復(fù)制代碼
內(nèi)存分配的三個部分匹夫已經(jīng)在代碼中標(biāo)識了出來:
1.Web Stream:包括了壓縮的文件,解壓所需的緩存,以及解壓后的文件舞虱。
2.AssetBundle:Web Stream中的文件的映射欢际,或者說引用。
3.實例化之后的對象:就是引擎的各種資源文件了矾兜,會在內(nèi)存中創(chuàng)建出來幼苛。
那就分別解析一下:
WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)
復(fù)制代碼
1.將壓縮的文件讀入內(nèi)存中
2.創(chuàng)建解壓所需的緩存
3.將文件解壓,解壓后的文件進入內(nèi)存
4.關(guān)閉掉為解壓創(chuàng)建的緩存
AssetBundle bundle = www.assetBundle;
復(fù)制代碼
1.AssetBundle此時相當(dāng)于一個橋梁焕刮,從Web Stream解壓后的文件到最后實例化創(chuàng)建的對象之間的橋梁舶沿。
2.所以AssetBundle實質(zhì)上是Web Stream解壓后的文件中各個對象的映射。而非真實的對象配并。
3.實際的資源還存在Web Stream中括荡,所以此時要保留Web Stream。
Instantiate(bundle.mainAsset);
復(fù)制代碼
1.通過AssetBundle獲取資源溉旋,實例化對象
最后各位可能看到了官網(wǎng)中的這個例子使用了:
using(WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)){
}
復(fù)制代碼
這種using的用法畸冲。這種用法其實就是為了在使用完Web Stream之后,將內(nèi)存釋放掉的观腊。因為WWW也繼承了idispose的接口邑闲,所以可以使用using的這種用法。其實相當(dāng)于最后執(zhí)行了:
//刪除Web Stream
www.Dispose();
復(fù)制代碼
OK,Web Stream被刪除掉了梧油。那還有誰呢苫耸?對Assetbundle。那么使用
//刪除AssetBundle
bundle.Unload(false);
復(fù)制代碼
ok儡陨,寫到這里就先打住啦褪子。寫的有點超了。有點趕也有點臨時骗村,日后在補充編輯嫌褪。
使用Unity Profiler工具檢測內(nèi)存
這篇文章當(dāng)時寫的時候略顯倉促,因此并沒有特別介紹Unity Profiler工具胚股,也更談不上用Unity Profiler工具來監(jiān)測內(nèi)存的使用狀態(tài)了笼痛。但是使用Unity Profiler工具來監(jiān)測還是十分必要的,下面就簡單補充一下這方面的知識琅拌。
在Profiler工具中提供了兩種模式供我們監(jiān)測內(nèi)存的使用情況缨伊,即簡易模式和詳細模式。在簡易模式中财忽,我們可以看到總的內(nèi)存(total)列出了兩列倘核,即Used Total(使用總內(nèi)存)和Reserved Total(預(yù)定總內(nèi)存)。Used Total和Reserved 均是物理內(nèi)存即彪,其中Reserved是unity向系統(tǒng)申請的總內(nèi)存紧唱,Unity底層為了不經(jīng)常向系統(tǒng)申請開辟內(nèi)存活尊,開啟了較大一塊內(nèi)存作為緩存,即所謂的Reserved內(nèi)存漏益, 而運行時蛹锰,unity所使用的內(nèi)存首先是向Reserved中來申請內(nèi)存,當(dāng)不使用時也是先向Reserved中釋放內(nèi)存绰疤,從而來保證游戲運行的流暢性铜犬。 一般來說,Used Total越大轻庆,則Reserved Total越大癣猾,而當(dāng)Used Total降下去后,Reserved Total也是會隨之下降的(但并不一定與Used Total同步)余爆。
Unity3D的內(nèi)存從大體上可以分為以下幾個部分:
1.Unity:位Unity3D的底層代碼所分配的內(nèi)存纷宇。
2.Mono:即托管堆。Mono運行時在運行游戲腳本時所需要的內(nèi)存蛾方,換句話說托管堆的大小與我們的GameObject數(shù)量像捶、資源量無關(guān),僅是腳本代碼造成的桩砰。這部分內(nèi)存是有垃圾回收機制的拓春。
3.GfxDriver:可以理解為GPU顯存開銷,主要由Texture亚隅,Vertex buffer以及index buffer組成硼莽。所以盡可能地減少或釋放Texture和mesh等資源,即可降低GfxDriver內(nèi)存枢步。
4.FMOD:音頻的內(nèi)存開銷沉删。
5.Profiler
而在簡易模式下的監(jiān)視器最下方,則列出了常見的一些資源以及它們所消耗的內(nèi)存醉途。
1.紋理
2.網(wǎng)格
3.材質(zhì)
4.動作
5.音頻
6.游戲?qū)ο蟮臄?shù)量
而詳細模式則需要點擊“Take Sample”按鈕來捕獲詳細的內(nèi)存使用情況。需要注意的是砖茸,由于獲得數(shù)據(jù)需要花費一定的時間隘擎,因此我們無法獲得實時的詳細內(nèi)存的使用情況。在詳細模式中凉夯,我們可以觀察每個具體資源和游戲?qū)ο蟮膬?nèi)存使用情況