Unity加載和內(nèi)存管理

Babybus-u3d技術(shù)交流-Unity加載和內(nèi)存管理

[unity
里有兩種動(dòng)態(tài)加載機(jī)制:一是Resources.Load,一是通過(guò)AssetBundle,其實(shí)兩者本質(zhì)上我理解沒(méi)有什么區(qū)別。Resources.Load就是從一個(gè)缺省打進(jìn)程序包里的AssetBundle里加載資源,而一般AssetBundle文件需要你自己創(chuàng)建,運(yùn)行時(shí)動(dòng)態(tài)加載,可以指定路徑和來(lái)源的。

其實(shí)場(chǎng)景里所有靜態(tài)的對(duì)象也有這么一個(gè)加載過(guò)程,只是Unity后臺(tái)替你自動(dòng)完成了。

詳細(xì)說(shuō)一下細(xì)節(jié)概念:

AssetBundle運(yùn)行時(shí)加載:

來(lái)自文件就用CreateFromFile(注意這種方法只能用于standalone程序)這是最快的加載方法

也可以來(lái)自Memory,用CreateFromMemory(byte[]),這個(gè)byte[]可以來(lái)自文件讀取的緩沖,www的下載或者其他可能的方式。

其實(shí)[WWW

的assetBundle就是內(nèi)部數(shù)據(jù)讀取完后自動(dòng)創(chuàng)建了一個(gè)assetBundle而已

Create完以后,等于把硬盤(pán)或者網(wǎng)絡(luò)的一個(gè)文件讀到內(nèi)存一個(gè)區(qū)域,這時(shí)候只是個(gè)AssetBundle內(nèi)存鏡像數(shù)據(jù)塊,還沒(méi)有Assets的概念。

Assets加載:

用AssetBundle.Load(同Resources.Load) 這才會(huì)從AssetBundle的內(nèi)存鏡像里讀取并創(chuàng)建一個(gè)Asset對(duì)象,創(chuàng)建Asset對(duì)象同時(shí)也會(huì)分配相應(yīng)內(nèi)存用于存放(反序列化)

異步讀取用AssetBundle.LoadAsync

也可以一次讀取多個(gè)用AssetBundle.LoadAll

AssetBundle的釋放:

AssetBundle.Unload(flase)是釋放AssetBundle文件的內(nèi)存鏡像,不包含Load創(chuàng)建的Asset內(nèi)存對(duì)象。

AssetBundle.Unload(true)是釋放那個(gè)AssetBundle文件內(nèi)存鏡像和并銷(xiāo)毀所有用Load創(chuàng)建的Asset內(nèi)存對(duì)象。

一個(gè)Prefab從assetBundle里L(fēng)oad出來(lái) 里面可能包括:Gameobject transform mesh texture material shader script和各種其他Assets。

你Instantiate一個(gè)Prefab,是一個(gè)對(duì)Assets進(jìn)行Clone(復(fù)制)+引用結(jié)合的過(guò)程,GameObject transform 是Clone是新生成的。其他mesh / texture / material / shader 等,這其中有些是純引用的關(guān)系的,包括:Texture
和TerrainData
,還有引用和復(fù)制同時(shí)存在的,包括:Mesh/material/PhysicMaterial
。引用的Asset對(duì)象不會(huì)被復(fù)制,只是一個(gè)簡(jiǎn)單的指針指向已經(jīng)Load的Asset對(duì)象。

這種含糊的引用加克隆的混合,大概是搞糊涂大多數(shù)人的主要原因。

專(zhuān)門(mén)要提一下的是一個(gè)特殊的東西:Script Asset

,看起來(lái)很奇怪,Unity里每個(gè)Script都是一個(gè)封閉的Class定義而已,并沒(méi)有寫(xiě)調(diào)用代碼,光Class的定義腳本是不會(huì)工作的。其實(shí)Unity引擎就是那個(gè)調(diào)用代碼,Clone一個(gè)script asset等于new一個(gè)class實(shí)例,

實(shí)例才會(huì)完成工作。通過(guò)AddComponent給物體添加一個(gè)Script Assets,就完成了

把腳本類(lèi)實(shí)例掛到Unity主線程的調(diào)用鏈里去的工作,Class實(shí)例里的OnUpdate OnStart等才會(huì)被執(zhí)行。多個(gè)物體掛同一個(gè)腳本,其實(shí)就是在多個(gè)物體上掛了那個(gè)腳本類(lèi)的多個(gè)實(shí)例而已,這樣就好理解了。在new class這個(gè)過(guò)程中,數(shù)據(jù)區(qū)是復(fù)制的,代碼區(qū)是共享的,算是一種特殊的復(fù)制+引用關(guān)系。

你可以再I(mǎi)nstantiate一個(gè)同樣的Prefab,還是這套mesh/texture/material/shader...,這時(shí)候會(huì)有新的GameObject等,但是不會(huì)創(chuàng)建新的引用對(duì)象比如Texture.

所以你Load出來(lái)的Assets其實(shí)就是個(gè)數(shù)據(jù)源,用于生成新對(duì)象或者被引用,生成的過(guò)程可能是復(fù)制(clone)也可能是引用(指針)

當(dāng)你Destroy一個(gè)實(shí)例時(shí),只是釋放那些Clone對(duì)象,并不會(huì)釋放引用對(duì)象和Clone的數(shù)據(jù)源對(duì)象,Destroy并不知道是否還有別的object在引用那些對(duì)象。

等到?jīng)]有任何游戲場(chǎng)景物體在用這些Assets以后,這些assets就成了沒(méi)有引用的游離數(shù)據(jù)塊了,是UnusedAssets了,這時(shí)候就可以通過(guò)Resources.UnloadUnusedAssets來(lái)釋放,Destroy不能完成這個(gè)任務(wù),AssetBundle.Unload(false)也不行,AssetBundle.Unload(true)可以但不安全,除非你很清楚沒(méi)有任何對(duì)象在用這些Assets了。

配個(gè)圖加深理解:

Unity加載和內(nèi)存管理.jpg

雖然都叫Asset,但復(fù)制的和引用的是不一樣的,這點(diǎn)被Unity的暗黑技術(shù)細(xì)節(jié)掩蓋了,需要自己去理解。

關(guān)于內(nèi)存管理

按照傳統(tǒng)的編程思維,最好的方法是:自己維護(hù)所有對(duì)象,用一個(gè)Queue來(lái)保存所有object,不用時(shí)該Destory的,該Unload的自己處理。

但這樣在C# .net框架底下有點(diǎn)沒(méi)必要,而且很麻煩。

穩(wěn)妥起見(jiàn)你可以這樣管理

創(chuàng)建時(shí):

先建立一個(gè)AssetBundle,無(wú)論是從www還是文件還是memory

用AssetBundle.load加載需要的asset

加載完后立即AssetBundle.Unload(false),釋放AssetBundle文件本身的內(nèi)存鏡像,但不銷(xiāo)毀加載的Asset對(duì)象。(這樣你不用保存AssetBundle的引用并且可以立即釋放一部分內(nèi)存)

釋放時(shí):

如果有Instantiate的對(duì)象,用Destroy進(jìn)行銷(xiāo)毀

在合適的地方調(diào)用Resources.UnloadUnusedAssets,釋放已經(jīng)沒(méi)有引用的Asset.

如果需要立即釋放內(nèi)存加上GC.Collect(),否則內(nèi)存未必會(huì)立即被釋放,有時(shí)候可能導(dǎo)致內(nèi)存占用過(guò)多而引發(fā)異常。

這樣可以保證內(nèi)存始終被及時(shí)釋放,占用量最少。也不需要對(duì)每個(gè)加載的對(duì)象進(jìn)行引用。

當(dāng)然這并不是唯一的方法,只要遵循加載和釋放的原理,任何做法都是可以的。

系統(tǒng)在加載新場(chǎng)景時(shí),所有的內(nèi)存對(duì)象都會(huì)被自動(dòng)銷(xiāo)毀,包括你用AssetBundle.Load加載的對(duì)象和Instaniate克隆的。但是不包括AssetBundle文件自身的內(nèi)存鏡像,那個(gè)必須要用Unload來(lái)釋放,用.net的術(shù)語(yǔ),這種數(shù)據(jù)緩存是非托管的。

總結(jié)一下各種加載和初始化的用法:

AssetBundle.CreateFrom.....:創(chuàng)建一個(gè)AssetBundle內(nèi)存鏡像,注意同一個(gè)assetBundle文件在沒(méi)有Unload之前不能再次被使用

WWW.AssetBundle:同上,當(dāng)然要先new一個(gè)再 yield return 然后才能使用

AssetBundle.Load(name):從AssetBundle讀取一個(gè)指定名稱(chēng)的Asset并生成Asset內(nèi)存對(duì)象,如果多次Load同名對(duì)象,除第一次外都只會(huì)返回已經(jīng)生成的Asset對(duì)象,也就是說(shuō)多次Load一個(gè)Asset并不會(huì)生成多個(gè)副本(
singleton)

。

Resources.Load(path;name):同上,只是從默認(rèn)的位置加載。

Instantiate(object):Clone一個(gè)object的完整結(jié)構(gòu),包括其所有Component和子物體(詳見(jiàn)官方文檔),淺Copy,并不復(fù)制所有引用類(lèi)型。有個(gè)特別用法,雖然很少這樣用,其實(shí)可以用Instantiate來(lái)完整的拷貝一個(gè)引用類(lèi)型的Asset,比如Texture等,要拷貝的Texture必須類(lèi)型設(shè)置為Read/Write able。

總結(jié)一下各種釋放

Destroy:主要用于銷(xiāo)毀克隆對(duì)象,也可以用于場(chǎng)景內(nèi)的靜態(tài)物體,不會(huì)自動(dòng)釋放該對(duì)象的所有引用。雖然也可以用于Asset,但是概念不一樣要小心,如果用于銷(xiāo)毀從文件加載的Asset對(duì)象會(huì)銷(xiāo)毀相應(yīng)的資源文件!但是如果銷(xiāo)毀的Asset是Copy的或者用腳本動(dòng)態(tài)生成的,只會(huì)銷(xiāo)毀內(nèi)存對(duì)象。

AssetBundle.Unload(false):釋放AssetBundle文件內(nèi)存鏡像

AssetBundle.Unload(true):釋放AssetBundle文件內(nèi)存鏡像同時(shí)銷(xiāo)毀所有已經(jīng)Load的Assets內(nèi)存對(duì)象

Reources.UnloadAsset(Object):顯式的釋放已加載的Asset對(duì)象,只能卸載磁盤(pán)文件加載的Asset對(duì)象

Resources.UnloadUnusedAssets:用于釋放所有沒(méi)有引用的Asset對(duì)象

GC.Collect()強(qiáng)制垃圾收集器立即釋放內(nèi)存 Unity的GC功能不算好,沒(méi)把握的時(shí)候就強(qiáng)制調(diào)用一下

在3.5.2之前好像Unity不能顯式的釋放Asset

舉兩個(gè)例子幫助理解

例子1:

一個(gè)常見(jiàn)的錯(cuò)誤:

你從某個(gè)AssetBundle里L(fēng)oad了一個(gè)prefab并克隆之:obj = Instantiate(AssetBundle1.Load('MyPrefab”);

這個(gè)prefab比如是個(gè)npc

然后你不需要他的時(shí)候你用了:Destroy(obj);你以為就釋放干凈了

其實(shí)這時(shí)候只是釋放了Clone對(duì)象,通過(guò)Load加載的所有引用、非引用Assets對(duì)象全都靜靜靜的躺在內(nèi)存里。

這種情況應(yīng)該在Destroy以后用:AssetBundle1.Unload(true),徹底釋放干凈。

如果這個(gè)AssetBundle1是要反復(fù)讀取的 不方便Unload,那可以在Destroy以后用:Resources.UnloadUnusedAssets()把所有和這個(gè)npc有關(guān)的Asset都銷(xiāo)毀。

當(dāng)然如果這個(gè)NPC也是要頻繁創(chuàng)建 銷(xiāo)毀的 那就應(yīng)該讓那些Assets呆在內(nèi)存里以加速游戲體驗(yàn)。

由此可以解釋另一個(gè)之前有人提過(guò)的[話題
:為什么
第一次
Instantiate一個(gè)Prefab的時(shí)候都會(huì)卡一下,因?yàn)樵谀愕谝淮蜪nstantiate之前,相應(yīng)的Asset對(duì)象還沒(méi)有被創(chuàng)建,要加載系統(tǒng)內(nèi)置的AssetBundle并創(chuàng)建Assets,第一次以后你雖然Destroy了,但Prefab的Assets對(duì)象都還在內(nèi)存里,所以就很快了。

例子2:

從磁盤(pán)讀取一個(gè)1.[unity3d
文件到內(nèi)存并建立一個(gè)AssetBundle1對(duì)象

AssetBundle AssetBundle1 = AssetBundle.CreateFromFile("1.unity3d");

從AssetBundle1里讀取并創(chuàng)建一個(gè)Texture Asset,把obj1的主貼圖指向它

obj1.renderer.material.mainTexture = AssetBundle1.Load("wall") as Texture;

把obj2的主貼圖也指向同一個(gè)Texture Asset

obj2.renderer.material.mainTexture =

obj1.renderer.material.mainTexture;

Texture是引用對(duì)象,永遠(yuǎn)不會(huì)有自動(dòng)復(fù)制的情況出現(xiàn)(除非你真需要,用代碼自己實(shí)現(xiàn)copy),只會(huì)是創(chuàng)建和添加引用

如果繼續(xù):

AssetBundle1.Unload(true) 那obj1和obj2都變成黑的了,因?yàn)橹赶虻腡exture Asset沒(méi)了

如果:

AssetBundle1.Unload(false) 那obj1和obj2不變,只是AssetBundle1的內(nèi)存鏡像釋放了

繼續(xù):

Destroy(obj1),//obj1被釋放,但并不會(huì)釋放剛才Load的Texture

如果這時(shí)候:

Resources.UnloadUnusedAssets();

不會(huì)有任何內(nèi)存釋放 因?yàn)門(mén)exture asset還被obj2用著

如果

Destroy(obj2)

obj2被釋放,但也不會(huì)釋放剛才Load的Texture

繼續(xù)

Resources.UnloadUnusedAssets();

這時(shí)候剛才load的Texture Asset釋放了,因?yàn)闆](méi)有任何引用了

最后CG.Collect();

強(qiáng)制立即釋放內(nèi)存

由此可以引申出論壇里另一個(gè)被提了幾次的[問(wèn)題
,如何加載一堆大圖片輪流顯示又不爆掉

不考慮AssetBundle,直接用www讀圖片文件的話等于是直接創(chuàng)建了一個(gè)Texture Asset

假設(shè)文件保存在一個(gè)List里

TLlist<string> fileList;
int n=0;IEnumerator OnClick()
{

WWW image = new www(fileList[n++]);
yield return image;
obj.mainTexture = image.texture;
n = (n>=fileList.Length-1)?0:n;
Resources.UnloadUnusedAssets();
}

這樣可以保證內(nèi)存里始終只有一個(gè)巨型Texture Asset資源,也不用代碼追蹤上一個(gè)加載的Texture Asset,但是速度比較慢

或者:

IEnumerator OnClick()

{
WWW image = new www(fileList[n++]);
yield return image;
Texture tex = obj.mainTexture;
obj.mainTexture = image.texture;
n = (n>=fileList.Length-1)?0:n;
Resources.UnloadAsset(tex);

}

這樣卸載比較快

最后編輯于
?著作權(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)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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