Babybus-u3d技術(shù)交流-Unity加載和內(nèi)存管理
[unity
里有兩種動態(tài)加載機制:一是Resources.Load偷拔,一是通過AssetBundle,其實兩者本質(zhì)上我理解沒有什么區(qū)別。Resources.Load就是從一個缺省打進程序包里的AssetBundle里加載資源亏钩,而一般AssetBundle文件需要你自己創(chuàng)建莲绰,運行時動態(tài)加載,可以指定路徑和來源的姑丑。
其實場景里所有靜態(tài)的對象也有這么一個加載過程蛤签,只是Unity后臺替你自動完成了。
詳細說一下細節(jié)概念:
AssetBundle運行時加載:
來自文件就用CreateFromFile(注意這種方法只能用于standalone程序)這是最快的加載方法
也可以來自Memory,用CreateFromMemory(byte[]),這個byte[]可以來自文件讀取的緩沖栅哀,www的下載或者其他可能的方式震肮。
其實[WWW
的assetBundle就是內(nèi)部數(shù)據(jù)讀取完后自動創(chuàng)建了一個assetBundle而已
Create完以后称龙,等于把硬盤或者網(wǎng)絡(luò)的一個文件讀到內(nèi)存一個區(qū)域,這時候只是個AssetBundle內(nèi)存鏡像數(shù)據(jù)塊戳晌,還沒有Assets的概念鲫尊。
Assets加載:
用AssetBundle.Load(同Resources.Load) 這才會從AssetBundle的內(nèi)存鏡像里讀取并創(chuàng)建一個Asset對象,創(chuàng)建Asset對象同時也會分配相應(yīng)內(nèi)存用于存放(反序列化)
異步讀取用AssetBundle.LoadAsync
也可以一次讀取多個用AssetBundle.LoadAll
AssetBundle的釋放:
AssetBundle.Unload(flase)是釋放AssetBundle文件的內(nèi)存鏡像沦偎,不包含Load創(chuàng)建的Asset內(nèi)存對象疫向。
AssetBundle.Unload(true)是釋放那個AssetBundle文件內(nèi)存鏡像和并銷毀所有用Load創(chuàng)建的Asset內(nèi)存對象。
一個Prefab從assetBundle里Load出來 里面可能包括:Gameobject transform mesh texture material shader script和各種其他Assets豪嚎。
你Instantiate一個Prefab搔驼,是一個對Assets進行Clone(復(fù)制)+引用結(jié)合的過程,GameObject transform 是Clone是新生成的疙渣。其他mesh / texture / material / shader 等匙奴,這其中有些是純引用的關(guān)系的,包括:Texture
和TerrainData
妄荔,還有引用和復(fù)制同時存在的泼菌,包括:Mesh/material/PhysicMaterial
。引用的Asset對象不會被復(fù)制啦租,只是一個簡單的指針指向已經(jīng)Load的Asset對象哗伯。
這種含糊的引用加克隆的混合,大概是搞糊涂大多數(shù)人的主要原因篷角。
專門要提一下的是一個特殊的東西:Script Asset
焊刹,看起來很奇怪,Unity里每個Script都是一個封閉的Class定義而已,并沒有寫調(diào)用代碼恳蹲,光Class的定義腳本是不會工作的虐块。其實Unity引擎就是那個調(diào)用代碼,Clone一個script asset等于new一個class實例嘉蕾,
實例才會完成工作贺奠。通過AddComponent給物體添加一個Script Assets,就完成了
把腳本類實例掛到Unity主線程的調(diào)用鏈里去的工作错忱,Class實例里的OnUpdate OnStart等才會被執(zhí)行儡率。多個物體掛同一個腳本,其實就是在多個物體上掛了那個腳本類的多個實例而已以清,這樣就好理解了儿普。在new class這個過程中,數(shù)據(jù)區(qū)是復(fù)制的掷倔,代碼區(qū)是共享的眉孩,算是一種特殊的復(fù)制+引用關(guān)系。
你可以再Instantiate一個同樣的Prefab,還是這套mesh/texture/material/shader...,這時候會有新的GameObject等浪汪,但是不會創(chuàng)建新的引用對象比如Texture.
所以你Load出來的Assets其實就是個數(shù)據(jù)源胸囱,用于生成新對象或者被引用缕陕,生成的過程可能是復(fù)制(clone)也可能是引用(指針)
當你Destroy一個實例時俄讹,只是釋放那些Clone對象帽芽,并不會釋放引用對象和Clone的數(shù)據(jù)源對象,Destroy并不知道是否還有別的object在引用那些對象殃姓。
等到?jīng)]有任何游戲場景物體在用這些Assets以后袁波,這些assets就成了沒有引用的游離數(shù)據(jù)塊了,是UnusedAssets了蜗侈,這時候就可以通過Resources.UnloadUnusedAssets來釋放,Destroy不能完成這個任務(wù)篷牌,AssetBundle.Unload(false)也不行,AssetBundle.Unload(true)可以但不安全踏幻,除非你很清楚沒有任何對象在用這些Assets了枷颊。
配個圖加深理解:
雖然都叫Asset,但復(fù)制的和引用的是不一樣的该面,這點被Unity的暗黑技術(shù)細節(jié)掩蓋了夭苗,需要自己去理解。
關(guān)于內(nèi)存管理
按照傳統(tǒng)的編程思維隔缀,最好的方法是:自己維護所有對象题造,用一個Queue來保存所有object,不用時該Destory的,該Unload的自己處理猾瘸。
但這樣在C# .net框架底下有點沒必要界赔,而且很麻煩。
穩(wěn)妥起見你可以這樣管理
創(chuàng)建時:
先建立一個AssetBundle,無論是從www還是文件還是memory
用AssetBundle.load加載需要的asset
加載完后立即AssetBundle.Unload(false),釋放AssetBundle文件本身的內(nèi)存鏡像牵触,但不銷毀加載的Asset對象淮悼。(這樣你不用保存AssetBundle的引用并且可以立即釋放一部分內(nèi)存)
釋放時:
如果有Instantiate的對象,用Destroy進行銷毀
在合適的地方調(diào)用Resources.UnloadUnusedAssets,釋放已經(jīng)沒有引用的Asset.
如果需要立即釋放內(nèi)存加上GC.Collect()揽思,否則內(nèi)存未必會立即被釋放袜腥,有時候可能導(dǎo)致內(nèi)存占用過多而引發(fā)異常。
這樣可以保證內(nèi)存始終被及時釋放绰更,占用量最少瞧挤。也不需要對每個加載的對象進行引用锡宋。
當然這并不是唯一的方法儡湾,只要遵循加載和釋放的原理,任何做法都是可以的执俩。
系統(tǒng)在加載新場景時徐钠,所有的內(nèi)存對象都會被自動銷毀,包括你用AssetBundle.Load加載的對象和Instaniate克隆的役首。但是不包括AssetBundle文件自身的內(nèi)存鏡像尝丐,那個必須要用Unload來釋放显拜,用.net的術(shù)語,這種數(shù)據(jù)緩存是非托管的爹袁。
總結(jié)一下各種加載和初始化的用法:
AssetBundle.CreateFrom.....:創(chuàng)建一個AssetBundle內(nèi)存鏡像远荠,注意同一個assetBundle文件在沒有Unload之前不能再次被使用
WWW.AssetBundle:同上,當然要先new一個再 yield return 然后才能使用
AssetBundle.Load(name):從AssetBundle讀取一個指定名稱的Asset并生成Asset內(nèi)存對象失息,如果多次Load同名對象譬淳,除第一次外都只會返回已經(jīng)生成的Asset對象,也就是說多次Load一個Asset并不會生成多個副本(
singleton)
盹兢。
Resources.Load(path;name):同上,只是從默認的位置加載邻梆。
Instantiate(object):Clone一個object的完整結(jié)構(gòu),包括其所有Component和子物體(詳見官方文檔),淺Copy绎秒,并不復(fù)制所有引用類型浦妄。有個特別用法,雖然很少這樣用见芹,其實可以用Instantiate來完整的拷貝一個引用類型的Asset,比如Texture等剂娄,要拷貝的Texture必須類型設(shè)置為Read/Write able。
總結(jié)一下各種釋放
Destroy:主要用于銷毀克隆對象玄呛,也可以用于場景內(nèi)的靜態(tài)物體宜咒,不會自動釋放該對象的所有引用。雖然也可以用于Asset,但是概念不一樣要小心把鉴,如果用于銷毀從文件加載的Asset對象會銷毀相應(yīng)的資源文件故黑!但是如果銷毀的Asset是Copy的或者用腳本動態(tài)生成的,只會銷毀內(nèi)存對象庭砍。
AssetBundle.Unload(false):釋放AssetBundle文件內(nèi)存鏡像
AssetBundle.Unload(true):釋放AssetBundle文件內(nèi)存鏡像同時銷毀所有已經(jīng)Load的Assets內(nèi)存對象
Reources.UnloadAsset(Object):顯式的釋放已加載的Asset對象场晶,只能卸載磁盤文件加載的Asset對象
Resources.UnloadUnusedAssets:用于釋放所有沒有引用的Asset對象
GC.Collect()強制垃圾收集器立即釋放內(nèi)存 Unity的GC功能不算好,沒把握的時候就強制調(diào)用一下
在3.5.2之前好像Unity不能顯式的釋放Asset
舉兩個例子幫助理解
例子1:
一個常見的錯誤:
你從某個AssetBundle里Load了一個prefab并克隆之:obj = Instantiate(AssetBundle1.Load('MyPrefab”);
這個prefab比如是個npc
然后你不需要他的時候你用了:Destroy(obj);你以為就釋放干凈了
其實這時候只是釋放了Clone對象怠缸,通過Load加載的所有引用诗轻、非引用Assets對象全都靜靜靜的躺在內(nèi)存里。
這種情況應(yīng)該在Destroy以后用:AssetBundle1.Unload(true)揭北,徹底釋放干凈扳炬。
如果這個AssetBundle1是要反復(fù)讀取的 不方便Unload,那可以在Destroy以后用:Resources.UnloadUnusedAssets()把所有和這個npc有關(guān)的Asset都銷毀搔体。
當然如果這個NPC也是要頻繁創(chuàng)建 銷毀的 那就應(yīng)該讓那些Assets呆在內(nèi)存里以加速游戲體驗恨樟。
由此可以解釋另一個之前有人提過的[話題
:為什么
第一次
Instantiate一個Prefab的時候都會卡一下,因為在你第一次Instantiate之前疚俱,相應(yīng)的Asset對象還沒有被創(chuàng)建劝术,要加載系統(tǒng)內(nèi)置的AssetBundle并創(chuàng)建Assets,第一次以后你雖然Destroy了,但Prefab的Assets對象都還在內(nèi)存里,所以就很快了养晋。
例子2:
從磁盤讀取一個1.[unity3d
文件到內(nèi)存并建立一個AssetBundle1對象
AssetBundle AssetBundle1 = AssetBundle.CreateFromFile("1.unity3d");
從AssetBundle1里讀取并創(chuàng)建一個Texture Asset,把obj1的主貼圖指向它
obj1.renderer.material.mainTexture = AssetBundle1.Load("wall") as Texture;
把obj2的主貼圖也指向同一個Texture Asset
obj2.renderer.material.mainTexture =
obj1.renderer.material.mainTexture;
Texture是引用對象衬吆,永遠不會有自動復(fù)制的情況出現(xiàn)(除非你真需要,用代碼自己實現(xiàn)copy)绳泉,只會是創(chuàng)建和添加引用
如果繼續(xù):
AssetBundle1.Unload(true) 那obj1和obj2都變成黑的了逊抡,因為指向的Texture Asset沒了
如果:
AssetBundle1.Unload(false) 那obj1和obj2不變,只是AssetBundle1的內(nèi)存鏡像釋放了
繼續(xù):
Destroy(obj1),//obj1被釋放零酪,但并不會釋放剛才Load的Texture
如果這時候:
Resources.UnloadUnusedAssets();
不會有任何內(nèi)存釋放 因為Texture asset還被obj2用著
如果
Destroy(obj2)
obj2被釋放秦忿,但也不會釋放剛才Load的Texture
繼續(xù)
Resources.UnloadUnusedAssets();
這時候剛才load的Texture Asset釋放了,因為沒有任何引用了
最后CG.Collect();
強制立即釋放內(nèi)存
由此可以引申出論壇里另一個被提了幾次的[問題
蛾娶,如何加載一堆大圖片輪流顯示又不爆掉
不考慮AssetBundle灯谣,直接用www讀圖片文件的話等于是直接創(chuàng)建了一個Texture Asset
假設(shè)文件保存在一個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)存里始終只有一個巨型Texture Asset資源蛔琅,也不用代碼追蹤上一個加載的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);
}
這樣卸載比較快