概述:https://www.cnblogs.com/wang-jin-fu/p/10975660.html
這篇只涉及基礎原理,下篇會講如何實現一個簡單的資源管理框架。
一、Assets和Objects
Unity文件、文件引用、Meta詳解:https://blog.uwa4d.com/archives/USparkle_inf_UnityEngine.html
meta文件:Unity在首次將Asset導入Unity時會生成meta文件,它與Asset存儲在同一個目錄中。該文件中記錄了資源的GUID和fileID(本地ID),文件GUID(File GUID)標識了資源文件(Asset file)在哪個目標資源(target resource)文件里,fileID(本地ID)用於標識Asset中的每個Object。資源間的依賴關系通過GUID來確定;資源內部的依賴關系使用fileID來確定,每個fileID對應一組組件信息,該信息記錄了其對應組件的類型及初始化信息。例如以下示例m_Script記錄腳本的guid,其他參數為m_Script的類初始化時的參數
--- !u!114 &114826744576399670 MonoBehaviour: m_ObjectHideFlags: 1 m_PrefabParentObject: {fileID: 0} m_PrefabInternal: {fileID: 100100000} m_GameObject: {fileID: 1151505213129540} m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 48fb9c66a154844a495af53fc97a7656, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 0 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_Sprite: {fileID: 21300000, guid: 5c7a7d69156d06448833b25308c032cf, type: 3} m_Type: 0 m_PreserveAspect: 0 m_FillCenter: 1 m_FillMethod: 4 m_FillAmount: 1 m_FillClockwise: 1 m_FillOrigin: 0 m_SpriteName: m_isNativeSize: 0 m_isGradualMat: 0

fileFormatVersion: 2 guid: 9070bffdf4e7444e190533a128133eb4 timeCreated: 1519804963 licenseType: Pro NativeFormatImporter: mainObjectFileID: 100100000 userData: assetBundleName: assetBundleVariant:
Library\metadata下的文件和Close.Prefabs大概是這樣子的:
如上,Close預制體包含了三個GameObject(Close、Image、BtnClose),在文件導入的時候,unity為每個文件生成一個導出配置,該配置存儲在項目的Library\metadata\xx文件夾里,其中xx為.meta記錄的guid的前兩位,例如70a2579b07749524b8c15f66a4c7216f,對應的xx為70,這個配置保存了GUID和path的對應關系,該ath會指向你的資源目錄,只要GUID沒變,unity就能索引到資源的目錄。該配置還保存了預制體內三個對象的fileID(本地ID),他與上圖右側的Close.Prefabs文件記錄的GameObject是一致的。
我們再來看看Close.Prefabs這個文件,每一個Unity對象都會有一個FileID,然后在需要引用時,使用這些FileID即可。
以Image對象為例子。Image對象擁有三個組件,RectTransform、CanvasRenderer、MonoBehaviour(對應的ImageEx組件,該組件繼承自Image),你可以在unity里查看對象的fileID
每一個組件的數據基本上就是這個組件的一堆參數了。那怎么區分這個組件是什么類型的呢?MonoBehaviour的類型參考https://docs.unity3d.com/Manual/ClassIDReference.html YAML數據,例如--- !u!222 &222167935205389516的222對應的是CanvasRenderer這個組件,用戶自定義的組件通過m_Script參數的guid定位到對應的c#文件目錄,就能識別出這個具體是什么類了
如下,114826744576399670(ImageEx)的組件信息里記錄了ImageEx文件的guid,以及ImageEx的初始化信息,實例化這個對象時,unity通過這guid找到imageEx這個類的文件並實例化,再將初始化參數賦值給實例化的對象
所以在實例化一個GameObject時,只要依照次序,依次創建物體,組件,初始化數據並進行引用綁定即可在場景中生成一個實例。
三、資源生命周期
加載方式:被動加載和顯示加載
Object會在下列時刻被自動加載:
1.映射到該Object的Instance ID被反向引用(Dereference)
2.Object當前沒有被加載到內存中
3.Object的源數據可以被定位
例如A對象引用了B對象,當加載A對象時,如果B對象未被加載且B對象資源存在,那么B會被加載
顯示加載:在腳本中通過創建或者調用資源加載API(例如AssetBundle.LoadAsset)顯式地加載Object
Object會在下列3中情況下被卸載:
1.在無用的Asset被清理時會自動卸載Object。該過程在Scene被破壞性地改變時自動發生(例如,通過SceneManager.LoadScene非增量地加載Scene),或者在腳本調用Resources.UnloadUnusedAssets時被觸發。這一過程僅卸載那些沒有被引用地Object —— 一個Object只會在沒有任何Mono變量或其他的活動Object持有對它的引用的時候才能被卸載。
2.通過調用Resources.UnloadAsset精確地卸載Resources文件夾中的Object。這些Object的Instance ID仍然是有效的,並且含有有效的File GUID和Local ID條目。如果任何Mono變量或者Object持有對這類被卸載的Object的引用,那么在任意引用被反向引用時,這個被卸載的Object都會被立刻重新加載。
3.來自AssetBundle的Object會在調用AssetBundle.Unload(true)時立即被自動卸載。這會使Object的Instance ID的File GUID和Local ID失效,並且所有對已卸載的Object的活動引用都會變為“(Missing)”引用。在C#腳本中,嘗試訪問已卸載Object的方法或屬性將會引發 NullReferenceException。
四、加載耗時
當序列化Unity GameObject的層級結構時,例如序列化預制體,整個層級結構都會被完全序列化。也就是說,這個層級結構中的每個GameObject和Component都會被單獨地序列化到數據中。
當創建GameObject層級結構時,會有幾種不同的耗費CPU時間的形式:
1.讀取源數據(從存儲設備、AssetBundle、其他GameObject等)
2.在新的Transform之間設置父子關系
3.實例化新的GameObject和Component
4.在主線程中喚醒新的GameObject和Component
后三個時間消耗通常是不變的,無論層級結構是從已有的層級結構克隆的還是從存儲設備中加載的。然而,讀取源數據消耗的時間會隨着序列化的層級結構中的GameObject和Component的數量線性增長,而且受到讀取速度的影響。
在現有的所有平台上,從內存中讀取數據都比從存儲設備中讀取數據快很多。另外,在不同平台上的不同存儲媒介上性能特征差異很大。因此,在低速存儲設備上加載預制體時,讀取預制體的序列化數據消耗的時間很容易超過實例化預制體所花費的時間。也就是說,加載操作的開銷受到了存儲設備I/O時間的限制。
前面提到過,在序列化整個預制體時,其中的每個GameObject和Component的數據都會被單獨地序列化,這里面可能含有重復的數據。例如,一個UI屏幕上由30個相同的元素,這些元素就會被序列化30次,產生一大團二進制數據。在加載時,這30個相同的元素上的每個GameObject和Component的數據都要全部從磁盤讀取出來,然后才能轉換成新的Object實例。實例化預制體的整體開銷中,文件讀取時間占了占了很大比重。對於大型的層級結構,應該將其分模塊進行實例化,然后再在運行時將他們整合到一起。
那么建議就是:將預制體中擁有相同結構的對象單獨拎出來做成預制體,采用動態加載的方式加載,例如滑動列表的單Item。
五、資源加載方式對比
1.AssetDatabase:在編輯器內加載卸載資源,並不能在游戲發布時使用,它只能在編輯器內使用。但是,它加載速度快,使用簡單。
2.Resources:該文件夾下的資源都會被打進最后的安裝包里,類似缺省打進程序包里的AssetBundle。不建議使用該文件夾,因為:
不正確地使用Resources文件夾會導致應用啟動時間變長,同時會增大構建出來的應用程序(該文件夾下的文件,不論是否有引用都會打進最終的包里)。隨着Resources文件夾的增加,管理工程各處Resources文件夾里的資源也變得很困難。
使用Resources文件夾導致細粒度的內存管理愈發地困難。
使用Resources文件夾無法熱更,就這一項就夠了~。
在工程構建的時候,所有名字為”Resources”的目錄下的所有資源都會被合並為一個序列化文件。像AssetBundle文件一樣,這個文件同時也包含了元數據(metadata)和索引信息(indexing information)。索引信息包含了一個序列化的、將對象的名稱映射為 文件GUID+本地ID 查找樹(lookup tree)。同時這個索引信息也包含了對象在序列化文件中的偏移位置信息。
因為這個查找樹的數據結構是(在大部分平台上)一個平衡搜索樹(balanced search tree)[注1].它的構建時間復雜度是 O(N log(N)),這里的 N 是樹中對象的數量。隨着Resources文件夾下資源的增長,索引信息的加載時間也會超過線性的速度增長。
這個操作是發生在應用啟動的過程中的Unity閃屏(splash screen)出現的時候,並且是不可跳過的。如果Resources 系統包含了 10000 個資源,那么在低端移動設備上面這個過程將會達到數秒之久,盡管絕大部分的Resources下面的資源在第一個場景當中都是不需要加載的。
3.AssetBundle:參考https://www.cnblogs.com/wang-jin-fu/p/11171626.html,支持熱更,但是每次資源變化都得重新打ab包(奇慢),所以適合發布模式,但開發模式千萬別用。
4.UnityWebRequest:從網絡端下載
UnityWebRequest功能分三塊:
◾上傳文件到服務器
◾從服務器下載
◾http通信控,(例如,重定向和錯誤處理)
UnityWebRequest 由三個元素組成。
◾UploadHandler 將數據發送到服務器的對象
◾DownloadHandler 從服務器接收數據的對象
◾UnityWebRequest 負責 HTTP 通信流量控制並管理上面兩個對象的對象。也是存儲錯誤和重定向信息的地方。
使用:
public class Example : MonoBehaviour { void Start() { // A correct website page. StartCoroutine(GetRequest("https://www.example.com")); // A non-existing page. StartCoroutine(GetRequest("https://error.html")); } IEnumerator GetRequest(string uri) { using (UnityWebRequest webRequest = UnityWebRequest.Get(uri)) { // Request and wait for the desired page. yield return webRequest.SendWebRequest(); string[] pages = uri.Split('/'); int page = pages.Length - 1; if (webRequest.isNetworkError) { Debug.Log(pages[page] + ": Error: " + webRequest.error); } else { Debug.Log(pages[page] + ":\nReceived: " + webRequest.downloadHandler.text); } } } }
六、資源管理
資源管理分三部分:
1.項目內文件的放置規范:合理的划分目錄才能合理的使用AssetBundle。一般來說,除了場景和模型,其他資源都是一個目錄一個ab包,當然這個目錄的細分程度視項目而定,但是更新頻繁的對象如預制體,建議細分程度高一點即目錄文件小一點。如果目錄划分混亂,會導致ab包的效率低下(試想英雄模塊和副本模塊的資源放在一個目錄下並打進ab包里,那么加載英雄界面時會把副本也加載進來,這是即浪費內存又影響加載效率的事)
1-1.Assets目錄中的所有資源文件名均采用大駝峰式命名法 ,即每一個單詞的首字母都大寫。且使用能夠描述其功能或意義的英文單詞或詞組。
1-2.Assets目錄中不得出現壓縮包、PPT、Word文檔等與游戲項目無關的資源文件
1-3.相同類型的資源放在同一個目錄下,例如ui資源和場景、模型分開放置,一般會有場景、UI(界面預制體、圖集)、模型、音效、腳本、特效、Shader等
1-4.相同功能的資源放在同一個目錄下,例如英雄相關功能可能會有十幾個界面的預制體,把這些預制體放在同一個文件夾。
1-5.所有插件放在Plugin下。所有的Editor文件放在同一個目錄下
1-6.Resources謹慎放置資源,因為該文件夾下的資源都會打進包里,不管是否有用到
1-7.一個圖集一個目錄
2.包體大小的控制
2-1.刪除無用資源。那么如何確定一個資源是否有被引用到呢?
首先我們需要使用AssetDatabase.FindAssets接口獲取到需要查找依賴的對象,例如我們想知道文件夾“xxx”下是否有文件引用資源a,那么xxx目錄下的對象就是我們需要查找依賴的對象。如下的參數searchInFolders
我們需要查找依賴的類型,例如sprite是不可能依賴sprite的,那么在查找某sprite是否有被引用(依賴)時,我們在需要查找依賴的對象里可以剔除掉sprite類型。如下的參數filter
獲取到了需要查找引用的對象后,使用AssetDatabase.GetDependencies可以獲取到這些對象引用到的資源的路徑,把這些路徑比對你想查找的資源A的路徑,如果有相等的,說明A就有被引用,就不能被刪除。
終於的實現如下
/// <summary> /// 查找資源依賴 /// </summary> /// <param name="filter"></param> 搜索條件 如"index l:ui t:texture2D" l開頭為標簽,t開頭為類型,以空格隔開,""空字符串查找整個Asset目錄 /// <param name="searchInFolders"></param> 要查找的目錄 /// <param name="targetPath"></param> 要查找引用的資源,例如Assets/Test/index.png void FindDependcy(string targetPath, string filter = "", string searchInFolders = "") { string[] searchObjs; if (!string.IsNullOrEmpty(searchInFolders)) { string[] folders = m_TargetPath.Split(','); searchObjs = AssetDatabase.FindAssets(filter, folders);//獲取需要查找引用的對象 } else { searchObjs = AssetDatabase.FindAssets(filter);//獲取需要查找引用的對象 } List<string> resultList = new List<string>(); for (var i = 0; i < searchObjs.Length; i++) { var guid = searchObjs[i]; string assetPath = AssetDatabase.GUIDToAssetPath(guid); string[] dependencies = AssetDatabase.GetDependencies(assetPath, m_Recursive);//獲取文件依賴項 foreach (string depend in dependencies) { if (targetPath == depend) { //查找到依賴資源targetPath的對象 resultList.Add(assetPath); } } } if (resultList.Count == 0) { Debug.Log(string.Format("資源{0}沒有被引用,可以刪除", targetPath)); } }
2-2.壓縮資源包:本人項目采用的是lzma壓縮方式,可以參考雨松的文章https://www.xuanyusong.com/archives/3095
AssetBundle自帶壓縮模式,但是lzma使用時需要整包解壓縮,所以我當前項目采用的是AssetBundle采用lz4壓縮,在對所有的ab包進行lzma壓縮,也就是壓縮了兩層。
2-3.上傳部分高清資源:有部分資源需要某特定的模塊才會用到,那么這部分比較大的文件可以上傳到服務器按需下載。例如商城的資源一般引用高清資源,但用戶初次進游戲的時候並不會使用到(有些用戶甚至很長一段時間都不會打開這些界面),unity 上傳資源到服務器參考UnityWebRequest接口
3.內存的控制,內存占用太高會導致程序崩潰,頻繁加載、卸載又會引起卡頓。在內存占用和加載之間取一個平衡點(卸載無用資源)
3-1.unity的內存占用如上圖所示。CreateFromFile已經被LoadFromMemory替代了。
Assets加載:用AssetBundle.Load(同Resources.Load) 這才會從AssetBundle的內存鏡像里讀取並創建一個Asset對象,創建Asset對象同時也會分配相應內存用於存放(反序列化),異步讀取用AssetBundle.LoadAsync。
AssetBundle的釋放:
AssetBundle.Unload(flase)是釋放AssetBundle文件的內存鏡像,不包含Load創建的Asset內存對象。當AssetBundle被再次加載時並不會恢復引用,而是會重新創建引用,容易造成資源冗余。
AssetBundle.Unload(true)是釋放那個AssetBundle文件內存鏡像和並銷毀所有用Load創建的Asset內存對象。
Destroy: 主要用於銷毀克隆對象,也可以用於場景內的靜態物體,不會自動釋放該對象的所有引用。雖然也可以用於Asset,但是概念不一樣要小心,如果用於銷毀從文 件加載的Asset對象會銷毀相應的資源文件!但是如果銷毀的Asset是Copy的或者用腳本動態生成的,只會銷毀內存對象。
一個Prefab從assetBundle里Load出來 里面可能包括:Gameobject transform mesh texture material shader script和各種其他Assets。
Instaniate一個Prefab,是一個對Assets進行Clone(復制)+引用結合的過程,GameObject transform 是Clone是新生成的。其他mesh / texture / material / shader 等,這其中有些是純引用的關系的,包括:Texture和TerrainData,還有引用和復制同時存在的,包括:Mesh/material /PhysicMaterial。引用的Asset對象不會被復制,只是一個簡單的指針指向已經Load的Asset對象。
再次Instaniate一個同樣的Prefab,還是這套mesh/texture/material/shader...,這時候會有新的GameObject等,但是不會創建新的引用對象比如Texture.
所以你Load出來的Assets其實就是個數據源,用於生成新對象或者被引用,生成的過程可能是復制(clone)也可能是引用(指針)
當你Destroy一個實例時,只是釋放那些Clone對象,並不會釋放引用對象和Clone的數據源對象,Destroy並不知道是否還有別的object在引用那些對象。
等到沒有任何游戲場景物體在用這些Assets以后,這些assets就成了沒有引用的游離數據塊了,是UnusedAssets了,這時候就可以通過 Resources.UnloadUnusedAssets來釋放,Destroy不能完成這個任 務
3-2.資源泄漏、冗余
資源泄漏是內存泄露的主要表現形式,其具體原因是用戶對加載后的資源進行了儲存(比如放到Container中、在腳本中引用),但在場景切換時並沒有將其Remove或Clear,從而無論是引擎本身還是手動調用Resources.UnloadUnusedAssets等相關API均無法對其進行卸載,進而造成了資源泄露。只有那些真正沒有任何引用指向的資源會被回收,因此請確保在資源不再使用時,將所有對該資源的引用設置為null或者Destroy。
當你得到一個類型為“GameObject”的c#對象時,它幾乎什么都不包含。這是因為Unity是一個C/ c++引擎。這個GameObject(游戲對象)包含的所有實際信息(它的名稱、它擁有的組件列表、它的HideFlags等等)都位於c++端。c#對象只有一個指向本機對象的指針”。也就是說一個對象包含兩部分,c++端的實際信息,當你加載一個新場景或者調用object.destroy (myObject)時,這些對象會被銷毀。c#端指向c++端的指針, c#對象的生命周期通過垃圾收集器以c#方式進行管理。這意味着可能存在一個c#對象指針指向一個已經被銷毀的c++對象。如果您將這個對象與null進行比較將返回“true”,從而就會出現對象的Null判斷為true,但實際上還是被引用着,無法被GC釋放的問題。
舉個例子,在名為A的MonoBehaviour中,有個數組來存放名為B的 MonoBehaviour對象的引用。當我們其他的邏輯去Destroy了B對象所在的GameObject后,在A對象中的數組里,遍歷打印,它們(B的引用)都為Null,在Inspector面板上看是missing。而這時候進行GC,堆內存其實並未釋放這些B對象。只有當A對象中的數組被清空后,再調用GC,才可釋放這些對象所占內存。
所謂“資源冗余”,是指在某一時刻內存中存在兩份甚至多份同樣的資源。導致這種情況的出現主要有兩種原因:
一、AssetBundle打包機制出現問題,同一份資源被打入到多份AssetBundle文件中。例如bundle1和bundle2同時引用了不再任意ab包里的資源材質A,那么bundle1和bundle2都會包含一份材質A的拷貝。當這些AssetBundle先后被加載到內存后,內存中即會出現紋理資源冗余的情況。
二、資源的實例化所致,在Unity引擎中,當我們修改了一些特定GameObject的資源屬性時,引擎會為該GameObject自動實例化一份資源供其使用,比如Material、Mesh等。
3-3.內存分類
程序代碼包括了所有的Unity引擎,使用的庫,以及你所寫的所有的游戲代碼。想要減少這部分內存的使用,能做的就是減少使用的庫
托管堆(Managed Heap)是被Mono使用的一部分內存。Mono的堆內存一旦分配,就不會返還給系統。這意味着Mono的堆內存是只升不降的。盡量避免托管堆出現峰值
堆內存的碎片化:回收的堆內存不會和其他未分配的內存合並,它的兩邊的內存可能仍然在使用,意味着內存中的對象不會被重新定位,去縮小對象之間的內存空隙。例如A,B,C,D四塊連續內存,B被回收后,原先B所在的內存只能存放大小小於或者等於B內存(如下圖),如果B足夠小,那B就是一個無法重復利用的碎片。盡管堆中可用的空間總量可能是巨大的,但有可能很多或者所有的空間都位於已經分配對象之間的小“間隙”中。在這種情況下,盡管總共有足夠大的空間來分配,但托管堆找不到足夠大的連續空間來分配內存。在下次內存分配的時候就不能找到合適大小的存儲單元,這樣就會觸發GC操作或者堆內存擴展操作。堆內存碎片會造成兩個結果,一個是游戲占用的內存會越來越大
本機堆(Native Heap)是Unity引擎進行申請和操作的地方,比如貼圖,音效,關卡數據等。
3-4.對象池。就是將對象存儲在一個池子中,當需要時再次使用,而不是每次都實例化一個新的對象。它其實是用內存換加載效率,所以對象池也不能無限地存儲對象,避免占用太多的內存,只保存一些需要頻繁加載、卸載的對象,例如子彈、通用道具item等。
在unity里頻繁地創建和銷毀對象效率很低,也會造成頻繁的資源回收(GC)。
最簡單例子如下,使用一個數組(list\queue都可以)去存儲子彈,但你需要使用子彈時,調用GetObject方法獲取,如果池子里有,直接返回,如果池子里並不存在,會實例化一個子彈。當你使用完畢后,調用Recyle回收就好了,業務不需要關心子彈的創建、銷毀、緩存。
using UnityEngine; using System.Collections; using System.Collections.Generic; public class BufferPool { private Queue<GameObject> pool; private GameObject prefab; private Transform prefabParent; //使用構造函數構造對象池 public BufferPool(GameObject obj,Transform parent,int count) { prefab = obj; pool = new Queue<GameObject>(count); prefabParent = parent; for (int i = 0; i < count; i++) { GameObject objClone = GameObject.Instantiate(prefab) as GameObject; objClone.transform.parent = prefabParent;//為克隆出來的子彈指定父物體 objClone.name = "Clone0" + i.ToString(); objClone.SetActive(false); pool.Enqueue(objClone); } } public GameObject GetObject() { GameObject obj = null; if (pool.Count > 0) { obj = pool.Dequeue(); //Dequeue()方法 移除並返回位於 Queue 開始處的對象 obj.transform.position = prefabParent.position; } else { obj = GameObject.Instantiate(prefab) as GameObject; obj.transform.SetParent(prefabParent); } obj.SetActive(true); return obj; } //回收對象 public void Recycle(GameObject obj) { obj.SetActive(false); pool.Enqueue(obj);//加入隊列 } }