Unity的資源加載及管理,基礎(chǔ)很重要。此篇文章作為近期梳理項目內(nèi)資源管理器的一個小總結(jié),嘗試盡量用人話將Unity對資源管理的關(guān)鍵點梳理清楚,個人覺得比較適合像我這樣剛?cè)腴T且對AssetBundle還不甚了解的家伙。
我理解的資源管理
舉一個不恰當?shù)睦觼砻枋鑫宜斫獾馁Y源管理(因為我實在想不出更合適的例子了),想象一個畫面:一個表演者,站在一個臺子后面,面向觀眾,按照規(guī)定的劇本,操作著臺子后面不被觀眾看到的箱子,從里面不斷的取出和放回各種新鮮的玩意兒,一會這么組合,一會那么拆散,博觀眾的眼球,最終完成表演。
我沒有當表演者的經(jīng)歷,雖然我很想嘗試,但想想也覺得這肯定不容易:
1、如果箱子里的東西都太大,拿起來會很費勁。
2、如果太小呢?恐怕會拿很多次。
3、不用的道具不收?放在臺子上會影響接下來的表演。
4、用過的道具收了吧。萬一收了后面需要的道具,一會兒用的時候還要再費勁拿一次,不拿的話吧還容易導(dǎo)致表演失敗。
帶著問題看文章
是選擇合適的時間取出資源,并在合適的時候釋放它們,盡可能保持較低的內(nèi)存占用;還是選擇讓資源常駐內(nèi)存,換取更快的讀取和計算速度?如何在時間和空間上做出平衡,才能最大的提升游戲體驗?我以為這些就是資源管理的目標和意義。可惜這并非易事。這不僅需要結(jié)合項目的實際情況,更需要豐富的實戰(zhàn)經(jīng)驗。
但是在這里,你將不會看到任何可以參考的經(jīng)驗或建議,因為我也不知道啊。
無論你是否單身,在Unity的世界里,你都不愁找不到對象,因為一切都是對象。
無論是紋理、音樂還是預(yù)制體,在進入Unity的世界后,都變成了各種對象供我們使用,例如紋理轉(zhuǎn)變?yōu)門exture2D或Sprite,音效文件轉(zhuǎn)變?yōu)锳udioClip,預(yù)制體變成了GameObject等等。這個由Asset(資源文件)轉(zhuǎn)變?yōu)镺bject(對象),從磁盤進入內(nèi)存的過程,就是實例化。而對資源進行的管理,本質(zhì)上是對Object的管理。
“小當家,這個黃金炒飯是怎么加載出來的?”
簡單介紹一下Unity加載資源的流程
在介紹Unity的資源加載機制之前,先舉一個生活中的例子,來輔助我們了解Unity是如何工作的。
因為我從小熱愛歐洲文學(xué),所以在這就拿我最喜歡的《三國演義》做例子,我們都知道書中多次提到“集齊七顆龍珠,就可以召喚神龍,并幫你實現(xiàn)一個愿望”這種說法。
咸魚都有夢想,何況一個上了歲數(shù)的程序員呢?但是很可惜,我們一顆龍珠都沒有,為了湊齊這七顆龍珠,我們首先要知道它們分別在哪。一摸左兜,哎?發(fā)現(xiàn)了一本《召喚神龍的小訣竅》,里面記錄了召喚神龍所必須七顆龍珠的所在位置、大小、顏色以及如何使用等非常關(guān)鍵的信息。
根據(jù)《召喚神龍的小訣竅》指引,我們知道原來第一顆龍珠藏在了素有小巴黎之稱的北京通縣,可是通縣在哪兒呢?一摸右兜,原來這還有一本1986年出版的《中國地圖》。那就放心了,出發(fā)吧!
終于,歷經(jīng)了81難,我們來到了目的地并最終找到了這顆龍珠。費這么大勁找到的龍珠,當然應(yīng)該認真記錄下來,于是我們馬上掏出一個黑皮小本本,認真的記下:“第一顆龍珠放在背后小書包的左邊縫有一個機器貓的側(cè)兜里...”。
...
最終,經(jīng)歷了無數(shù)艱難險阻,我們湊齊了七顆龍珠(所以說人只要肯努力,老天就一定回饋你,至少讓你知道你浪費了時間?。=鸸庖婚W,我們召喚出了神龍... 后面實現(xiàn)了什么愿望我們不談,因為誰沒有點小秘密呢。
現(xiàn)在,讓我們來回顧一下整個過程
1、這條召喚出來的神龍,就好比我們想要實例化的對象,就比如游戲?qū)ο蟀?,因為它相對?fù)雜些。而這七顆龍珠呢,就好似組成這個游戲?qū)ο笏仨毜母鞣N組件(Component)、紋理(Texture)、網(wǎng)格(Mesh)等等。
2、《召喚神龍的小訣竅》就好比我們讀取的這個.prefab文件,它記錄了組成這個GameObject所必須的其他對象以及它們的位置。
重點來了:File GUID 及 Local ID。
File GUID
Unity會為每一個加入到Assets文件夾中的文件,創(chuàng)建一個同級同名的.meta文件,雖然文件類型的不同會影響這個.meta的具體內(nèi)容,但它們都包含一個用來標記文件身份的File GUID。
例如,如果一個資源引用了另一個外部資源,比如一個Prefab引用了其他腳本、紋理或Prefab等,則一定會標明引用資源文件的File GUID。
Local ID
如果說File GUID表示為文件和文件之間的關(guān)系,那么Local ID表示的就是文件內(nèi)部各對象之間的關(guān)系,打開一個*.Prefab文件可以很清晰的看到:
(點擊上圖,可放大查看)
一個對象通常是由一個或多個對象構(gòu)成,每個記錄在&符號后面的數(shù)字都是一個Local ID,每一個Local ID也表示這它將來也會被實例化成一個對象。也就是說,當一個prefab文件要實例化成一個GameObject時,它會自動嘗試獲取其內(nèi)部Local ID所指的那個對象。如果這個所指的對象當前還沒有被實例化出來,那么Unity會自動實例化這個對象,如此遞歸,直到所有涉及的對象都被實例化。
3、我們可以發(fā)現(xiàn)手中沒有龍珠,是因為我們手中的黑色小本本,并沒有記錄龍珠裝在書包的那個位置里;同樣,Unity通過Instance ID,來獲取或判斷一個對象是否已經(jīng)被加載完畢。Instance ID由File GUID和Local ID轉(zhuǎn)換而成,可以簡單理解成是記錄了資源所在內(nèi)存地址的寫著數(shù)字的鑰匙牌。
每當Unity讀入一個File GUID和LocalID時,就會自動將其轉(zhuǎn)換成一個簡單好記的數(shù)字牌,因為通過File GUID和Local ID定位資源的效率并沒有直接解引用一個地址那么快。
如果發(fā)現(xiàn)這個牌上并沒有掛著一把鑰匙,表示當前這個這個資源還在磁盤中,尚不在內(nèi)存里(沒有加載);相反,如果這個牌子上有一把鑰匙,表示這個資源已經(jīng)被加載完畢,你可以快速的找到并使用它。
Unity會在項目啟動后,創(chuàng)建并一直維護一張“映射表”,這張映射表記錄的就是File GUID、Local ID以及由它們轉(zhuǎn)換而成的Instance ID之間的關(guān)系,這樣下次在請求資源時就可以快速的通過查看鑰匙牌來獲取資源了。
4、剛才的例子里,因為沒有龍珠(資源沒有加載),因此我們必須經(jīng)歷一場前往小巴黎的歷險(LoadingAsset),而能夠幫助我們準確定位北京通縣的86版《中國地圖》,可以近似理解成是Unity維護的一套將GUID和FileID解析為數(shù)據(jù)源地址的機制,這套機制中的信息,來自于:
(1) 場景加載時,Unity收集了與該場景關(guān)聯(lián)的資源信息。
(2) 項目啟動時,Unity收集了所有Resources文件夾下的資源信息。
(3) 讀取AssetBundle時,Unity獲取了AssetBundle文件的頭部信息(Header)。
可以理解為:隨著Unity知道更多的信息,這套機制將能夠解析并定位更多的GUID和FileID。
5、當我們費勁千辛萬苦找到龍珠后,記錄在小本本上的7條位置,就好比7個能幫助夠準確定位內(nèi)存位置的Instance ID。想象一下,當我們下次再看到諸如《三顆龍珠召喚小神龍》這樣的小訣竅(另外一個*.prefab),便可直接打開小本本(查詢映射表中的Instance ID),對著編號及位置從書包里掏出龍珠(對InstanceID所指的內(nèi)存地址進行解引用),啪啪啪一操作,小神龍這個游戲?qū)ο缶湍芎芸毂徽賳境鰜砹?,再也不用去什么通縣了,可以節(jié)省大把時間,想想就覺的美滋滋呢。
AssetBundle
AssetBundle(阿賽特邦豆)是Unity官方推薦的資源加載方式,網(wǎng)上對AssetBundle的介紹有很多,且在了解了Unity對資源的加載機制后,其本身沒有什么特別難以理解的地方了,因此在這不過多介紹,僅挑選幾個關(guān)鍵點進行闡述。
AssetBundle的生成
生成AssetBundle有很多種方式,在此僅簡單說一下比較常用的方式,使用BuildPipeline生成AssetBundle文件。
每一次調(diào)用BuildPipleLine.BuildAssetBundles時,將會生成一批AssetBundle文件,具體數(shù)量根據(jù)傳遞AssetBundleBuild數(shù)組決定,每一個AssetBundleBuild對象將對應(yīng)一個AssetBundle及一個同名+.manifest后綴文件。其中AssetBundle文件的后綴用戶自行設(shè)置,比如".unity3d",".ab"等等;而.manifest文件是給人看的,里面有這個AssetBundle的基本信息以及非常關(guān)鍵的資源列表。
除了AssetBundleBuild數(shù)組所定的AssetBundle外,還將額外在output路徑下生成的一對與output文件夾同名的文件及一個同名.manifest后綴文件。這個同名文件可厲害了,它記錄了這批次AssetBundle之間的相互依賴關(guān)系。當然.manifest文件還是給人看的,我們可以用它分析資源間的依賴關(guān)系,但是在項目實際運行時,Unity并不會關(guān)心它。
(點擊上圖,可放大查看)
可以通過這張圖來看一下每次Build后資源的對應(yīng)關(guān)系,當然這都不如你自己親自Build一次看的清楚。
AssetBundle的加載
根據(jù)AssetBundle文件所在的位置(本地、遠端),AssetBundle有不同的加載方式,在此僅總結(jié)最常用的本地AssetBundle文件加載。
我個人將AssetBundle拆分理解為:Bundle加載和Asset加載兩部分。因為AssetBundle文件可以從功能上分為兩大塊:
1、記錄文件標記、壓縮信息、文件列表的Header部分;
2、記錄資源實際內(nèi)容的Data部分。
當使用AssetBundle.LoadFromFile或LoadFromFileAsync時,在pc平臺及移動平臺上,unity僅會為我們讀取AssetBundle的header部分,并不會將bundle的data部分整個讀入內(nèi)存。
當調(diào)用上一步生成的AssetBundle對象讀取具體資源時(LoadAsset, LoadAssetAsync, LoadAllAssets),Unity會參考已經(jīng)緩存的文件列表,找到目標資源在data部分的位置并讀入到內(nèi)存中。
如果一個資源引用到了其他資源,則必須要先讀入被引用資源的AssetBundle文件,否則就會發(fā)生引用Miss。這就好似召喚神龍時,通過《召喚神龍的小訣竅》得知第一顆龍珠在北京通縣,但是當打開《中國地圖》時,北京的地方被摳了一個窟窿,我去,這樣我們就無法通過它準確定位龍珠位置了,只有六顆龍珠召喚出的神龍,當然有一部分是Miss嘍。
為了避免上面Miss的情況,在加載資源時,首先需要將該資源的依賴項全部加載完畢,不過僅需加載依賴資源的AssetBundle文件。也就是說,我們只要將該依賴AssetBundle的Header部分加載(AssetBundle.LoadFromFile或LoadFromFileAsync)就可以,這樣在真正讀取Asset時,Unity會自動處理好真實依賴的Asset,我們不用操心。
AssetBundle的依賴關(guān)系如何讀取呢?加載上面提到的那個很厲害的文件就可以了。
(點擊上圖,可放大查看)
非常簡單的獲取依賴關(guān)系的方法,通常會在項目啟動時將全部依賴關(guān)系保存下來。
AssetBundle的使用
當AssetBundle被成功加載后,調(diào)用該Assebbundle對象的LoadAsset、LoadAllAssets或?qū)?yīng)的異步版本即可加載資源,也就是實例化對象。如果這個對象已經(jīng)被加載過,Unity并不會重復(fù)加載,還記得之前所說的映射表么,被加載過的資源就好比掛上了數(shù)字牌的鑰匙,直接對地址解引用即可。
AssetBundle的卸載
如果說AssetBundle真的有什么容易出問題的地方,那恐怕就是卸載了。
在這里只說最常用的這個卸載方法吧:
public void?Unload(bool?unloadAllLoadedObjects);
一個被加載過的AssetBundle可以通過調(diào)用Unload來卸載這個Bundle下所有的Asset。但是調(diào)用這個函數(shù)時傳入的參數(shù)對卸載結(jié)果影響甚大。
Unity官方對這個函數(shù)的講解非常詳細,配圖也非常直觀,因此我只是簡單總結(jié)一下。
相同點:
無論傳入?yún)?shù)為 true 或是 false,調(diào)用Unload都可以Destroy當前AssetBundle對象,釋放之前從AssetBundle文件中的Header部分所獲取的信息。當然,被釋放的AssetBundle對象無法再使用諸如LoadAsset、LoadAllAssets等函數(shù)加載資源。
不同點:
unloadAllLoadedObjects == true:
不僅Destroy了AssetBundle這個對象,而且這個AssetBundle下包含的所有對象,只要實例化了,有一個算一個,統(tǒng)統(tǒng)釋放掉。
感覺就像
foreach(Object ?asset in assets)
{
if(asset != null)
{
delete asset;
asset = null;
}
}
比如你通過ab.LoadAsset(apple)后,將apple設(shè)置給go_0的一個Renderer,如果這時候ab.Unload(true),那go_0就傻了,咋回事兒啊,圖咋沒了呢?WTF啊。
它的好處是:不會有重復(fù)資源問題的情況發(fā)生,每次都處理的干干凈凈。
unloadAllLoadedObjects == false:
僅僅Destroy了AssetBundle這個對象,但是并沒有釋放這個AssetBundle下的任何Asset,因此如果有對象引用了這些Asset,也不會有問題。
它的風險(代價)是:下次再Load這個AssetBundle,并且通過這個AssetBundle重新讀取了這個Asset,會在內(nèi)存中重新創(chuàng)建一份,這樣如果之前的Asset沒有被釋放,那么現(xiàn)在內(nèi)存中就有兩份Asset了。
這種情況如果頻繁發(fā)生,便意味著內(nèi)存中有很多資源將“不受控制”,容易引發(fā)內(nèi)存占用過高的問題,而釋放這種不受控的資源,僅有兩種方式:
1、當沒有對象引用到這些不受控資源時,每次調(diào)用Resources.UnloadUnusedAssets,回收之。
2、加載場景時,如果加載模式?jīng)]有設(shè)置為LoadSceneMode.Additive,則會自動調(diào)用Resources.UnloadUnusedAssets。
同樣,再舉一個生活中的小例子以闡述這兩種釋放的差異吧:
小A交女朋友時喜歡送心形的石頭給對方,這天小A認識了一個女孩,并確定了關(guān)系,送了一個精心挑選的心形石頭給她,海誓山盟又云雨一番后,第二天由于感情不和等原因兩人分手了。小A是個暖男,他為了女孩能徹底忘記優(yōu)秀的自己并開始一段新的感情,約見了女孩,將之前送給女孩的石頭拿(搬)走了,從此注銷了微信消失在茫茫人海中。
確實,小A喜歡強壯的女孩,因為這樣比較有安全感。
小B交女朋友時也喜歡送石頭給對方,周一小B認識了一個女孩,并確定了關(guān)系,送了一個精心挑選的石頭給她,海誓山盟又云雨一番后,第二天由于感情不和等原因兩人分手了。但是小B家里是開石材加工場的,他并不關(guān)心這塊石頭,”送了就送了吧,至少我經(jīng)歷了浪漫的愛情“,小B這么想。并注銷了微信消失在茫茫人海中...達1天之久。
周二的時候小B重出江湖,并認識了一個新的女孩,確定了關(guān)系,第三天...第四天..啪啪啪...第七天,第二周的時候,江湖上就出現(xiàn)了一個傳說,集齊小B湊齊的七顆石頭,便可以召喚神龍,于是就回到了文章開頭我們提到的那個故事。
沒錯,小A對應(yīng)的就是Unload(true),而小B對應(yīng)的則是Unload(false)。
補充三點
1、移動Unity資源時,要在Unity編輯器內(nèi)拖動,不要在操作系統(tǒng)下剪切粘貼。因為這樣Unity會為這個文件生成一個新的File GUID及.meta文件,它會打破之前建立好的關(guān)系,讓所有引用過這個文件的prefab出現(xiàn)miss的情況。
2、實際上在項目build完成后,就已經(jīng)不存在File GUID和Local ID的概念了,轉(zhuǎn)而用相對簡單方式建立映射,這也是為什么我們在項目運行的過程中無法獲取到File GUID的原因,不過原理上它們是一樣的。
3、盡管一個AssetBundle的Header部分非常小,通常只有幾十KB,但是Unity并不能保證讀入大量AssetBundle的Header部分后資源的加載效率。因此還是按需讀取AssetBundle吧。