注:本文中用到的大部分術語和函數都是Unity中比較基本的概念,因此本文只是直接引用,再也不詳細解釋各類概念的具體內容,若要深刻了解,請查閱相關資料。網絡
遊戲資源的加載和釋放致使的內存泄漏問題一直是Unity遊戲開發的一個黑洞。所以致使遊戲拖慢,卡頓甚至閃退問題成爲了Unity遊戲的一個常見症狀。異步
究其根源,一方面是因遊戲設備尤爲是Unity擅長的移動設備運行內存很是有限,另一方面是由於Unity不太清晰的加載釋放策略和謎同樣的GC(垃圾收集)機制,共同賦予了Unity 「內存殺手」「低效引擎」的惡名,但事實上若是可以深刻的瞭解Unity的資源加載釋放機制,亦步亦趨的根據自身狀況管理好內存的使用,那麼Unity遊戲徹底能夠跳出內存泄漏的陷阱。函數
那麼下面,咱們從資源的加載方式,資源的相關概念,加載釋放的最佳策略三個方面來逐步探討這個Unity的「危險領域」。性能
Unity的資源加載方式分兩大種類:靜態加載和動態加載。設計
顧名思義,直接經過設置屬性的辦法,把資源直接綁定在場景內的任意對象上,如2D對象的Sprite屬性和3D對象的Materials屬性;另外經過自定義代碼上的Public屬性綁定的任何資源也屬於靜態加載範疇。對象
靜態加載是最爲常見的資源加載方式,其資源的生命週期與其所在的場景徹底一致,在場景加載時加載,在場景切換時釋放,因此這種方式的優缺點也是顯而易見的:blog
優勢:能夠在場景加載過程當中完成自身的加載過程,因此在場景運行期間該資源沒有任何性能隱患;另外在場景切換時會被徹底釋放,無須擔憂由於釋放不及時不完整而致使內測泄漏問題。生命週期
缺點:只支持不變的靜態資源,沒法根據遊戲的實際須要靈活更換不一樣資源;全部資源必須和場景同生共死,沒法在場景運行過程當中提早釋放,若是該資源很是龐大而且只在短期內須要,則會帶來不小的內存浪費。遊戲
動態加載通常發生在場景的運行期間,遊戲爲了必定的需求動態的加載和表現不一樣的資源而產生的需求:若是遊戲根據不一樣的玩家顯示不一樣的頭像,根據玩家選擇的不一樣角色而顯示不一樣的3D模型。動態加載的優缺點是很是極端的:內存
優勢:根據遊戲設計要求,有些資源在場景開始時沒法肯定,必須動態加載;動態資源能夠在場景運行的任什麼時候間加載,也能夠在任什麼時候間釋放,開發者具備很強的靈活性和主動性。
缺點:很明顯,動態資源的控制須要開發者親力親爲和更高的技巧;而一旦缺少對其合理的控制,內存陷阱將會遍地開花,遊戲的性能問題和內存泄漏將沒法避免。
Resources 本地資源加載:經過引擎內部的Resources類,對項目中全部Resources目錄下的資源進行動態加載。
AssetBundle本地或者遠程資源包加載:經過引擎內部的AssetBundle類,對網絡,內存和本地文件中的AssetBundle資源包進行加載。而後從資源包中獲取資源,在遊戲中使用。
Instantiate實例化遊戲對象:經過Resources或AssetBundle中的加載的對象,通常不能直接在場景中使用,須要經過Instantiate方法,實例化這些對象,使其成爲場景中可用的遊戲對象。
AssetDatabase加載資源:經過AssetDatabase的相關函數加載資源,因爲僅適用於Editor環境,在這裏不加累述。
Unity中常見的資源包括如下幾種:
GameObject(遊戲對象)
Shader(着色器)
Mesh(網格)
Material(材質)
Texture/Sprite(貼圖/精靈)
要理解Unity資源的使用,必須先了解如下幾個概念:
內存鏡像:任何遊戲資源或對象一旦加載,都會佔用設備的一部份內存區域,這個內存區域就是資源或對象的內存鏡像,若是內存鏡像過多達到設備的極限,遊戲必然會發生性能問題。
引用和複製:Unity的「黑科技」之一, 也是資源加載和釋放的主要難點。
引用:指對原資源僅僅是引用關係,再也不從新複製一分內存鏡像,但引用的關鍵在於,若是原資源被刪除會致使引用關係損壞,使得引用的對象發生資源丟失。
複製:複製原資源的內存鏡像,從而產生兩個不一樣的內存區域,若是被複制的資源被釋放,不會影響複製的資源。
但不幸的是,Unity中的遊戲對象不能簡單的用引用和複製來進行區分,大部分的對象不一樣部分採用了不一樣模式甚至混合模式,使得遊戲對象的內存分配顯得錯綜複雜。
下面經過一個實例來講明資源加載會使用多少內存,好比一個普通的3D對象,包括了Shader/Mesh/Material/Texture等資源,這些資源須要從AssetBundle加載,若是要將其實例化到場景,那麼將會佔用以下圖所示的內存空間:
首先,從文件、網絡或者其餘內存空間加載AssetBundle之後,會造成AssetBundle內存鏡像(上圖紫色部分)。
其次,從AssetBundle內存鏡像中再加載GameObject之後,該GameObject用到的Shader/Mesh/Material/Texture也同時被加載出來,造成各自不一樣的內存鏡像(注意:請參考上圖紫色虛線框中的內容,可知這些資源內存鏡像與AssetBundle內存鏡像是不一樣的)
最後Instantiate實例化GameObject之後,GameObject會再一次複製GameObject資源的內存鏡像到一個新的內存區域,造成全新的對象數據。(上圖上方綠色框中內容)
資源的加載須要理解如下要點
要點1:儘管GameObject是對原有資源內存鏡像的徹底複製,但因爲Unity對各類資源種類的處理方式不一樣,致使GameObject中的其餘相關資源並非簡單的複製關係:
Shader:徹底的引用,不佔用額外內存,若是原Shader資源被釋放會形成資源丟失而損壞對象。
Mesh:複製原資源內存空間的同時,還引用了原資源的數據,也就是說不但佔用額外的內存,並且一旦原資源被釋放,也會形成數據丟失而損壞對象。
Material:同Mesh,複製並引用原資源。
Texture:通Shader,徹底引用原資源。
要點2:從AssetBundle加載到GameObject實例化,大部分資源實際佔用3處內存,那麼最終咱們要釋放這3處內存纔算將該資源徹底釋放。
要點3:要特別注意和理解引用關係,這個在後面的資源釋放章節中具備重大意義。
Resources加載是將遊戲內部一部分以文件形式存儲的資源加載出來供遊戲使用,Resources加載的步驟通常有二步(下面是示例代碼):
Object cubePreb = Resources.Load< GameObject >(cubePath);
GameObject cube = Instantiate(cubePreb) as GameObject;
首先經過Resources.Load函數把對象資源(cubePreb)加載到內存鏡像。
其次經過Instantiate實例化該資源的內存鏡像變成遊戲中可用的對象(cube),固然若是是Shader/Mesh/Material/Texture類型資源無須再次實例化,能夠直接使用。因而可知Resources加載的資源通常佔用2處內存空間:所用資源cubePreb的內存鏡像和實例化對象cube的內存鏡像。
這裏順便提下Resources資源加載的一個「黑科技」:OnDemand方式。以上述代碼爲例,cubePreb的所需資源在Resources.Load的時候不會加載,而將在第一次Instantiate的時候一塊兒加載,也經常會致使一些比較大的對象在第一次實例化時形成卡頓現象,不過這個性能問題和內測泄漏無關,不在本文的探討範疇。
Resources最佳加載策略:
單體釋放Reources.UnloadAsset(Object)
主動卸載獨立資源,主要做用在於及時釋放場景的中的資源,減低運行時的內存損耗,提升遊戲性能;但這種方式也帶來了不小的風險,因爲Unity遊戲的資源引用關係錯綜複雜,若是要單獨釋放一個資源,要明確該資源已經在場景中再也不被引用,不然輕者形成遊戲顯示錯誤,重則形成遊戲報錯。
另外,Reources.UnloadAsset(Object)還有一些暗坑,好比釋放Sprite須要先釋放Sprite.Texture不然Texture就會存留在內存,因此在使用這個函數的時候,要清楚釋放的對象有無內部引用資源。
統一釋放Resources.UnloadUnusedAssets
這是一個統一的,一次性的,比較完整的釋放閒置資源的函數,並且是Unity官方很是推薦的一種方式,但這個函數實際的使用效果並無想象的那麼美好,該函數自己就是Unity資源釋放的一個陷阱。
首先UnloadUnusedAssets對全部須要釋放資源有一個很是重要的前置條件:只有不存在任何引用關係的資源才能被該函數釋放,看起來這是一個明確的要求,但因爲Unity資源的相互引用關係比較隱晦繁複,想要明確的判斷某一個資源不存在引用關係是有必定難度的,而且,若是一個咱們想釋放的資源存在任何隱性的引用關係,UnloadUnusedAssets將會無視這個資源而無任何反饋,這種狀況經常會被開發人員忽略而形成內存的泄漏。
通常狀況下,要明確一個資源再也不被引用,首先要把全部用到該資源使用GameObject.Destroy函數進行銷燬,而後要把全部引用到該資源的變量顯性的設置爲Null,尤爲要關注的是類成員和靜態變量的引用,最後調用UnloadUnusedAssets纔能有效地釋放這個資源。
根據實戰經驗來看,最佳使用UnloadUnusedAssets的時機仍是在場景切換的時候,因爲Unity的場景關閉會有效地銷燬全部的對象和全部代碼的引用,那麼在場景切換,尤爲是在新場景的開頭UnloadUnusedAssets上一個場景的資源處理是比較穩妥的作法;而在場景運行過程當中但願不斷調用UnloadUnusedAssets來快速釋放當前空閒資源實際上是一招險棋,有欲速則不達的可能:
Resources最佳釋放策略:
AssetBundle是Unity提供的另外一種資源加載方式,開發者能夠把一批資源打包,而後經過網絡下載或者文件加載的方式進行加載。
介於Resources方式的資源必須一塊兒打入遊戲包體,AssetBundle方式則提供了一種更爲靈活的資源加載方式,AssetBundle無需進入遊戲包體,大大減小了遊戲文件的體積,另外,AssetBundle容許經過網絡下載,也爲遊戲資源的獲取和升級提供了更爲靈活的選擇。
AssetBundle加載資源通常分3步,(下面是示例代碼):
var bundle= AssetBundle.LoadFromFile(path);
var prefab = myLoadedAssetBundle.LoadAsset.<GameObject>("MyObject");
var obj = Instantiate(prefab);
根據前面提到的資源的內存使用和以上示例代碼所示,能夠得知AssetBundle資源加載到最終加入遊戲場景,須要存在3個對象:bundle自己,加載的資源prefab,和實例化出來的obj。這3個對象分別對應不一樣的內存鏡像,在釋放的時候須要分別考慮。
AssetBundle最佳加載策略:
根據AssetBundle的3級對象,咱們分別說下各自的釋放辦法:
實例化的obj:用GameObject.Destroy釋放。
加載的資源prefab:由於是內存鏡像,也能夠用Object.Destroy釋放。另外Resources.UnloadUnusedAssets方法對這種資源釋放也是有效的,但條件比較苛刻,prefab的父(bundle)和子(obj)都要已經被釋放的狀況下,加上自己引用清空,而後使用UnloadUnusedAssets纔有效,因此這種辦法並不十分推薦。
加載的資源包bundle:AssetBundle.Unload方法是惟一的釋放手段。這個方法有2個參數,都有必定的意義:
參數爲false的時候,僅僅把資源包內存釋放,但保留任何已經加載的資源和實例化對象,這些資源和對象的釋放有待後續代碼完成。
參數爲true的時候,是一次比較完全的內存釋放,資源包和全部被加載出的資源都會被釋放,固然實例化的obj不會被釋放,但引用關係會被破壞,因此在使用這種方式前必須提早銷燬全部實例化對象。
AssetBundle最佳釋放策略: