Unity的資源管理——用法彙總(二)

歡迎加入Unity業內qq交流羣:956187480

qq掃描二維碼加羣,行業縱橫頗多,每個人精通領域各異,旨在交流,


此篇是網上其他人的理解跟總結,大家可以看一遍過一遍大腦,有點碎有點亂也有的重複了。有疑問可以留言溝通

用法總結:

總結一下各種加載和初始化的用法:
AssetBundle.CreateFrom.....:創建一個AssetBundle內存鏡像,注意同一個assetBundle文件在沒有Unload之前不能再次被使用
WWW.AssetBundle:同上,當然要先new一個再 yield return 然後才能使用
AssetBundle.Load(name): 從AssetBundle讀取一個指定名稱的Asset並生成Asset內存對象,如果多次Load同名對象,除第一次外都只會返回已經生成的Asset 對象,也就是說多次Load一個Asset並不會生成多個副本(singleton)。
Resources.Load(path&name):同上,只是從默認的位置加載。
Instantiate(object):Clone 一個object的完整結構,包括其所有Component和子物體(詳見官方文檔),淺Copy,並不複製所有引用類型。有個特別用法,雖然很少這樣 用,其實可以用Instantiate來完整的拷貝一個引用類型的Asset,比如Texture等,要拷貝的Texture必須類型設置爲 Read/Write able。

總結一下各種釋放
Destroy: 主要用於銷燬克隆對象,也可以用於場景內的靜態物體,不會自動釋放該對象的所有引用。雖然也可以用於Asset,但是概念不一樣要小心,如果用於銷燬從文 件加載的Asset對象會銷燬相應的資源文件!但是如果銷燬的Asset是Copy的或者用腳本動態生成的,只會銷燬內存對象。
AssetBundle.Unload(false):釋放AssetBundle文件內存鏡像
AssetBundle.Unload(true):釋放AssetBundle文件內存鏡像同時銷燬所有已經Load的Assets內存對象
Reources.UnloadAsset(Object):顯式的釋放已加載的Asset對象,只能卸載磁盤文件加載的Asset對象
Resources.UnloadUnusedAssets:用於釋放所有沒有引用的Asset對象
GC.Collect()強制垃圾收集器立即釋放內存 Unity的GC功能不算好,沒把握的時候就強制調用一下
在3.5.2之前好像Unity不能顯式的釋放Asset。
 

實例1-Asset Object和Clone Object同時存在問題:

一個常見的錯誤:你從某個AssetBundle裏Load了一個prefab並克隆之:obj = Instaniate(AssetBundle1.Load('MyPrefab”);
這個prefab比如是個npc然後你不需要他的時候你用了:Destroy(obj);你以爲就釋放乾淨了
其實這時候只是釋放了Clone對象,通過Load加載的所有引用、非引用Assets對象全都靜靜靜的躺在內存裏。
這種情況應該在Destroy以後用:AssetBundle1.Unload(true),徹底釋放乾淨。
如果這個AssetBundle1是要反覆讀取的 不方便Unload,那可以在Destroy以後用:Resources.UnloadUnusedAssets()把所有和這個npc有關的Asset都銷燬。

一是靜態引用,建一個public的變量,在Inspector裏把prefab拉上去,用的時候instantiate
二是Resource.Load,Load以後instantiate
三是AssetBundle.Load,Load以後instantiate
三種方式有細 節差異,前兩種方式,引用對象texture是在instantiate時加載,而assetBundle.Load會把perfab的全部assets 都加載,instantiate時只是生成Clone。所以前兩種方式,除非你提前加載相關引用對象,否則第一次instantiate時會包含加載引用 assets的操作,導致第一次加載的lag。
AssetBundle.Load是更好的方式。
 

 實例2-多個引用Asset-Object或Clone-Object問題:

從磁盤讀取一個1.unity3d文件到內存並建立一個AssetBundle1對象
AssetBundle AssetBundle1 = AssetBundle.CreateFromFile("1.unity3d");
從AssetBundle1裏讀取並創建一個Texture Asset,把obj1的主貼圖指向它
obj1.renderer.material.mainTexture = AssetBundle1.Load("wall") as Texture;
把obj2的主貼圖也指向同一個Texture Asset
obj2.renderer.material.mainTexture =obj1.renderer.material.mainTexture;
Texture是引用對象,永遠不會有自動複製的情況出現(除非你真需要,用代碼自己實現copy),只會是創建和添加引用
如果繼續:AssetBundle1.Unload(true) 那obj1和obj2都變成黑的了,因爲指向的Texture Asset沒了
如果:AssetBundle1.Unload(false) 那obj1和obj2不變,只是AssetBundle1的內存鏡像釋放了
繼續:Destroy(obj1),//obj1被釋放,但並不會釋放剛纔Load的Texture
如果這時候:Resources.UnloadUnusedAssets();不會有任何內存釋放 因爲Texture asset還被obj2用着
如果:Destroy(obj2),obj2被釋放,但也不會釋放剛纔Load的Texture
繼續:Resources.UnloadUnusedAssets();這時候剛纔load的Texture Asset釋放了,因爲沒有任何引用了
最後CG.Collect();強制立即釋放內存。

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);
}


這樣卸載比較快。



一.用法彙總:

感覺這是Unity內存管理暗黑和混亂的地方,特別是牽扯到Texture我最近也一直在測試這些用AssetBundle加載的asset一樣可以用Resources.UnloadUnusedAssets卸載, 但必須先 AssetBundle.Unload, 纔會被識別爲無用的 asset 。 比較保險的做法是

創建時:
先建立一個AssetBundle,無論是從www還是文件還是memory
用AssetBundle.load加載需要的asset。Resources.Load和靜態引用用的時候才真正的Load數據。
用完後立即AssetBundle.Unload(false),關閉AssetBundle但不摧毀創建的對象和引用
銷燬時:
對Instantiate的對象進行Destroy
在合適的地方調用Resources.UnloadUnusedAssets,釋放已經沒有引用的Asset.
如果需要立即釋放加上GC.Collect()
這樣可以保證內存始終被及時釋放
只要你Unload過的AssetBundle,那些創建的對象和引用都會在LoadLevel時被自動釋放。

所以:UnusedAssets不但要沒有被實際物體引用,也要沒有被生命週期內的變量所引用,纔可以理解爲 Unused(引用計數爲0)。
Texture 加載以後是到內存,顯示的時候才進入顯存的Texture Memory。
所有的東西基礎都是Object。
Load的是Asset,Instantiate的是GameObject和Object in Scene。
Load的Asset要Unload,new的或者Instantiate的object可以Destroy。
 

二,資源結構圖:

1.Resources文件夾 

Resources文件夾是一個只讀的文件夾,通過Resources.Load()來讀取對象。因爲這個文件夾下的所有資源都可以運行時來加載,所以Resources文件夾下的所有東西都會被無條件的打到發佈包中。建議這個文件夾下只放Prefab或者一些Object對象,因爲Prefab會自動過濾掉對象上不需要的資源。舉個例子我把模型文件還有貼圖文件都放在了Resources文件夾下,但是我有兩張貼圖是沒有在模型上用的,那麼此時這兩張沒用的貼圖也會被打包到發佈包中。假如這裏我用Prefab,那麼Prefab會自動過濾到這兩張不被用的貼圖,這樣發佈包就會小一些了。 

Resources資源是指在Unity工程的Assets目錄下面可以建一個Resources文件夾,在這個文件夾下面放置的所有資源,不論是否被場景用到,都會被打包到遊戲中,並且可以通過Resources.Load方法動態加載。這是平時開發是常用的資源加載方式,但是缺點是資源都直接打包到遊戲包中了,沒法做增量更新。

2.StreamingAssets
StreamingAssets文件夾也是一個只讀的文件夾,但是它和Resources有點區別,Resources文件夾下的資源會進行一次壓縮,而且也會加密,不使用點特殊辦法是拿不到原始資源的。但是StreamingAssets文件夾就不一樣了,它下面的所有資源不會被加密,然後是原封不動的打包到發佈包中,這樣很容易就拿到裏面的文件。
所以StreamingAssets適合放一些二進制文件,而Resources更適合放一些GameObject和Object文件。 StreamingAssets 只能用過www類來讀取!!
AssetBundle資源是指我們可以通過編輯器腳本來將資源打包成多個獨立的AssetBundle。這些AssetBundle和遊戲包是分離的,可以通過WWW類來加載。AssetBundle的使用很靈活:可以用來做分包發佈,例如大多數頁遊資源是隨着遊戲的過程增量下載的,或者有些手遊資源過大,渠道要求發佈的包限制在100M以內,那隻能把一開始玩不到的內容做成增量包,等玩家玩到的時候通過網絡下載。
打包過程只需要BuildPipeline.BuildAssetBundles一句話就行了,Unity5會根據依賴關係自動生成所有的包。每個包還會生成一個manifest文件,這個文件描述了包大小、crc驗證、包之間的依賴關係等等,通過這個manifest打包工具在下次打包的時候可以判斷哪些包中的資源有改變,只打包資源改變的包,加快了打包速度。manifest只是打包工具自己用的,發佈包的時候並不需要。
更深的坑在於,如果你公用的是一個FBX模型,你只給這個模型設置BundleName還不行,它用到的貼圖,材質都要設,否則模型是公用了,貼圖沒有公用,結果貼圖還是被打包到多個包中了。所以設置BundleName這個工作最好還是由編輯器腳本來完成。
Unity3D 裏有兩種動態加載機制:一個是Resources.Load,另外一個通過AssetBundle,其實兩者區別不大。 Resources.Load就是從一個缺省打進程序包裏的AssetBundle里加載資源,而一般AssetBundle文件需要你自己創建,運行時 動態加載,可以指定路徑和來源的。
其實場景裏所有靜態的對象也有這麼一個加載過程,只是Unity3D後臺替你自動完成了。

爲GameObject動態的添加遊戲其它組件:
  public Component AddComponent(Type componentType);
        [ExcludeFromDocs]
        public void BroadcastMessage(string methodName);

 Type classType =  typeof(UnityEngine.UI.Text);
        this.gameObject.AddComponent(classType);
        ulua中用GetClassType()
        例如:return u3DObj:AddComponent(BGCUUIDisplayController.GetClassType())

創建Asset內存對象:
你 Instaniate一個Prefab,是一個對Assets進行Clone(複製)+引用結合的過程,GameObject transform 是Clone是新生成的。其他mesh / texture / material / shader 等,這其中些是純引用的關係的,包括:Texture和TerrainData,還有引用和複製同時存在的,包括:Mesh/material /PhysicMaterial。引用的Asset對象不會被複制,只是一個簡單的指針指向已經Load的Asset對象。這種含糊的引用加克隆的混合, 大概是搞糊塗大多數人的主要原因。

專門要提一下的是一個特殊的東西:Script Asset,看起來很奇怪,Unity裏每個Script都是一個封閉的Class定義而已,並沒有寫調用代碼,光Class的定義腳本是不會工作的。其 實Unity引擎就是那個調用代碼,Clone一個script asset等於new一個class實例,實例纔會完成工作。把他掛到Unity主線程的調用鏈裏去,Class實例裏的OnUpdate OnStart等纔會被執行。多個物體掛同一個腳本,其實就是在多個物體上掛了那個腳本類的多個實例而已,這樣就好理解了。在new class這個過程中,數據區是複製的,代碼區是共享的,算是一種特殊的複製+引用關係。
你可以再Instaniate一個同樣的Prefab,還是這套mesh/texture/material/shader...,這時候會有新的GameObject等,但是不會創建新的引用對象比如Texture.
所以你Load出來的Assets其實就是個數據源,用於生成新對象或者被引用,生成的過程可能是複製(clone)也可能是引用(指針)。
當你Destroy一個實例時,只是釋放那些Clone對象,並不會釋放引用對象和Clone的數據源對象,Destroy並不知道是否還有別的object在引用那些對象。
等到沒有任何 遊戲場景物體在用這些Assets以後,這些assets就成了沒有引用的遊離數據塊了,是UnusedAssets了,這時候就可以通過 Resources.UnloadUnusedAssets來釋放,Destroy不能完成這個任 務,AssetBundle.Unload(false)也不行,AssetBundle.Unload(true)可以但不安全,除非你很清楚沒有任何 對象在用這些Assets了。

三,優化資源:

1.代碼優化
當使用Unity開發時,默認的Mono包含庫可以說大部分用不上,在Player Setting(Edit->Project Setting->Player或者Shift+Ctrl(Command)+B裏的Player Setting按鈕)
面板裏,將最下方的Optimization欄目中“Api Compatibility Level”選爲.NET 2.0 Subset,表示你只會使用到部分的.NET 2.0 Subset,不需要Unity將全部.NET的Api包含進去。接下來的“Stripping Level”表示從build的庫中剝離的力度,每一個剝離選項都將從打包好的庫中去掉一部分內容。你需要保證你的代碼沒有用到這部分被剝離的功能,
選爲“Use micro mscorlib”的話將使用最小的庫(一般來說也沒啥問題,不行的話可以試試之前的兩個)。庫剝離可以極大地降低打包後的程序的尺寸以及程序代碼的內存佔用,唯一的缺點是這個功能只支持Pro版的Unity。

2.託管堆優化 Unity有一篇不錯的關於託管堆代碼如何寫比較好的說明,在此基礎上我個人有一些補充。
首先需要明確,託管堆中存儲的是你在你的代碼中申請的內存(不論是用js,C#還是Boo寫的)。
一般來說,無非是new或者Instantiate兩種生成object的方法(事實上Instantiate中也是調用了new)。
在接收到alloc請求後,託管堆在其上爲要新生成的對象實例以及其實例變量分配內存,如果可用空間不足,則向系統申請更多空間。

都需要對其Destory(),然後新的金幣進入臺子時又需要Instantiate,這對性能是極大的浪費。一種通常的做法是在不需要時,不摧毀這個GameObject,而只是隱藏它。
如果不是必要,應該在遊戲進行的過程中儘量減少對GameObject的Instantiate()和Destroy()調用,因爲對計算資源會有很大消耗。在便攜設備上短時間大量生成和摧毀物體的
話,很容易造成瞬時卡頓。如果內存沒有問題的話,儘量選擇先將他們收集起來,然後在合適的時候(比如按暫停鍵或者是關卡切換),將它們批量地銷燬並 且回收內存。

四,如何管理資源:

Unity提供的就這些了,下面就自己發揮:如何做一個方便的資源管理方案,既可以開發時方便,又可以方便發佈更新包呢?開發過程全用AssetsBundle是不合適的,因爲開發中資源經常添加和更新,每次添加或者更新都生成一下AssetsBundle才能運行是很麻煩的。而且我們要做的是自動更新而不是分包下載,這也就是說在發佈遊戲的時候這些資源應該都是在遊戲包中的,所以他們也不該從AssetsBundle加載。

分析完需求,方案也就出來了:資源還是放在Resources下面,但是這些資源同時也會打包到AssetBundle中。代碼中所有加載資源的地方都通過自己的ResourceManager來加載,由ResourceMananger來決定是調用Resources.Load來加載資源還是從AssetsBundle加載。在開發環境下(Editor)這些資源顯然是直接從Resources加載的,發佈的完整安裝包資源也是從Resources加載,只有當有一個增量版本時,遊戲主程序纔會去服務器把增量的AssetBundle下載下來,然後從AssetBundle加載資源。

比較合理的做法是根據邏輯來,例如每個角色可以有獨立的AssetBundle,公用的一些UI資源可以打到一個AssetBundle裏面,每個場景獨立的UI資源可以打成獨立的AssetBundle。這樣做資源預加載的時候也方便,每個場景需要用到幾個Bundle就加載幾個Bundle,無關的資源不會被加載。

下面要考慮的是如何來確定一個資源是從Resources加載還是AssetBundle加載。爲此我們需要一個配置文件resourcesinfo。這個文件隨打包過程自動生成。裏面包含了資源版本號version,所有包的名字,每個包的HashCode以及每個包裏面包含的資源的名字。HashCode直接可以從Unity生成的manifest中得到(AssetBundleManifest.GetAssetBundleHash),用來檢查包的內容是否發生變化。這個resourceinfo每次打包AssetBundle時都會生成一個,發佈增量時將它和新的Bundle一起全部複製到服務器上。同時在Resources文件夾下也存一份,隨完整安裝包發佈,這就保證了新安裝遊戲的玩家手機上也有一份完整的資源配置文件,記錄了這個完整包包含的資源。

當遊戲啓動時,首先請求服務器檢查版本號,前端用的版本號就是Resources下面的這個resourcesinfo中的version。服務器比對這個版本號來告訴前端是否需要更新。如果需要更新,前端就去獲取服務器端的新resourcesinfo,然後比對裏面每個bundle的HashCode,把HashCode不同的bundle記錄下來,然後通過WWW類來下載這些發生改變的bundle,當然如果服務器版的resourcesinfo中包含了本地resourceinfo中所沒有的Bundle,這些Bundle就是新增的,也需要下載下來。所有下載完成後,前端將這個新的resourceinfo保存到本地存儲中,後面前端的所有操作都將以這個resourceinfo爲準而不再是Resources下面的resourceinfo了。

加載AssetBundle,我們直接使用WWW類而不用WWW.LoadFromCacheOrDownload, 因爲我們的資源在遊戲開始的時候已經下載到外部存儲了,不要再Download也不要再Cache。注意WWW類加載是異步的,在遊戲中我們需要同步加載資源的地方就要注意把資源預加載好存在ResourceManager中,不然等用的時候加載肯定要寫異步代碼了。大部分時候我們應該在一個場景初始化時就預加載好所有資源,用的時候直接從ResourceManager的緩存取就可以了。


歡迎加入Unity業內qq交流羣:956187480

qq掃描二維碼加羣,行業縱橫頗多,每個人精通領域各異,旨在交流,

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章