概述:http://www.javashuo.com/article/p-nggymcxb-bw.htmlhtml
這篇只涉及基礎原理,下篇會講如何實現一個簡單的資源管理框架。c++
1、Assets和Objectsweb
Unity文件、文件引用、Meta詳解:https://blog.uwa4d.com/archives/USparkle_inf_UnityEngine.htmlc#
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便可。cookie
以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時,只要依照次序,依次建立物體,組件,初始化數據並進行引用綁定便可在場景中生成一個實例。
3、資源生命週期
加載方式:被動加載和顯示加載
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。
4、加載耗時
當序列化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。
5、資源加載方式對比
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:參考http://www.javashuo.com/article/p-kxvvqcjh-e.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); } } } }
6、資源管理
資源管理分三部分:
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,纔可釋放這些對象所佔內存。
所謂「資源冗餘」,是指在某一時刻內存中存在兩份甚至多份一樣的資源。致使這種狀況的出現主要有兩種緣由:
1、AssetBundle打包機制出現問題,同一份資源被打入到多份AssetBundle文件中。例如bundle1和bundle2同時引用了再也不任意ab包裏的資源材質A,那麼bundle1和bundle2都會包含一份材質A的拷貝。當這些AssetBundle前後被加載到內存後,內存中即會出現紋理資源冗餘的狀況。
2、資源的實例化所致,在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);//加入隊列 } }