Unity中的內存泄漏

在對內存泄漏有一個基本印象以後,咱們再來看一下在特定環境——Unity下的內存泄漏。你們都知道,遊戲程序由代碼和資源兩部分組成,Unity下的內存泄漏也主要分爲代碼側的泄漏和資源側的泄漏,固然,資源側的泄漏也是由於在代碼中對資源的不合理引用引發的。html

代碼中的泄漏 – Mono內存泄漏算法

熟悉Unity的猿類們應該都知道,Unity是使用基於Mono的C#(固然還有其餘腳本語言,不過使用的人彷佛不多,在此不作討論)做爲腳本語言,它是基於Garbage Collection(如下簡稱GC)機制的內存託管語言。那麼既然是內存託管了,爲何還會存在內存泄漏呢?由於GC自己並非萬能的,GC能作的是經過必定的算法找到「垃圾」,而且自動將「垃圾」佔用的內存回收。那麼什麼是垃圾呢? 
咱們先來看一下wikipedia上對於GC實現的簡介: 
這裏寫圖片描述緩存

定義仍是過於冗長,咱們來聯想一下生活中,咱們通常把沒有利用價值的東西,稱爲垃圾,也就是沒有用的東西,就是垃圾。在GC的世界中,也是同樣的,沒有引用的東西,就是「垃圾」。由於沒有引用了,就意味着對於其餘任何對象而言,都認爲目標對象對我已經沒有利用價值了,那它就是「垃圾」了。根據GC的機制,其佔用的內存就會被回收。 
基於以上的知識,咱們很容易就能夠想到爲何在託管內存的環境下,仍是會出現內存泄漏了。這就像現實生活中的宅男宅女,吃了泡麪老是忘記把盒子扔到門外的垃圾箱裏;從計算機的角度來講,則是,在某對象超出其做用域時,咱們 「忘記」清除對該無用對象的引用了。 
說到這,有的同窗可能會有疑問:我每次在代碼中申請的內存都很是小,少則幾B,多則幾十K,如今設備的內存都比較大(幾百M仍是有的吧),即便泄漏會產生什麼大影響麼? 
首先,水滴石穿的典故相信你們都知道,實際代碼中,並不是只有顯示調用new纔會分配內存,不少隱式的分配是不容易被發現的,例如產生一個List來存儲數據,緩存了服務器下發的一份配置,產生一個字符串等等,這些操做都會產生內存的分配。你分配幾十K,他分配幾十K,一下子內存就沒了。 
其次,有一點須要說明的是,在Unity環境下,Mono堆內存的佔用,是隻會增長不會減小的。具體來講,能夠將Mono堆,理解爲一個內存池,每次Mono內存的申請,都會在池內進行分配;釋放的時候,也是歸還給池,而不會歸還給操做系統。若是某次分配,發現池內內存不夠了,則會對池進行擴建——向操做系統申請更多的內存擴大池以知足該次的內存分配。須要注意的是,每次對池的擴建,都是一次較大的內存分配,每次擴建,都會將池擴大6-10M左右(此處無官方數據,是觀察所得)。性能優化

這裏寫圖片描述

上圖是某遊戲通過Cube測試的結果,能夠看到Mono堆內存爲39M左右,而建議值通常爲 50M。 
咱們必須知道,Mono內存泄漏是Unity遊戲開發中須要特別重視的部分。服務器

資源中的泄漏 – Native內存泄漏微信

資源泄漏,顧名思義,是指將資源加載以後佔有了內存,可是在資源不用以後,沒有將資源卸載致使內存的無謂佔用。 
一樣的,在討論資源內存泄漏的緣由以前,咱們先來看一下Unity的資源管理與回收方式。爲何要將資源內存和代碼內存分開討論,也是由於其內存管理方式存在不一樣的緣由。架構

上文中說的代碼分配的內存,是經過Mono虛擬機,分配在Mono堆內存上的,其內存佔用量通常較小,主要目的是程序猿在處理程序邏輯時使用;而Unity的資源,是經過Unity的C++層,分配在Native堆內存上的那部份內存。舉個簡單的例子,經過UnityEngine命名空間中的接口分配的內存,將會經過Unity分配在Native堆;經過System命名空間中的接口分配的內存,將會經過Mono Runtime分配在Mono堆。 
這裏寫圖片描述編輯器

瞭解了分配與管理方式的區別,咱們再來看看回收的方式。如上文所說,Mono內存是經過GC來回收的,而Unity也提供了一種相似的方式來回收內存。不一樣的是,Unity的內存回收是須要主動觸發的。就比如說,咱們把垃圾扔在門口的垃圾桶裏,GC是天天來看一次,有垃圾就收走;而Unity則須要你打個電話給它,通知它有垃圾要回收,它纔會來。主動調用的接口是Resources.UnloadUnusedAssets()。其實GC也提供了一樣的接口GC.Collect() 
用來主動觸發垃圾回收,這兩個接口都須要很大的計算量,咱們不建議在遊戲運行時時不時主動調用一番,通常來講,爲了不遊戲卡頓,建議在加載環節來處理垃圾回收的操做。有一點須要說明的是,Resources.UnloadUnusedAssets()內部自己就會調用GC.Collect()。Unity還提供了另一個更加暴力的方式——Resources.UnloadAsset()來卸載資源,可是這個接口不管資源是否是「垃圾」,都會直接刪除,是一個很危險的接口,建議肯定資源不使用的狀況下,再調用該接口。函數

基於上述基礎知識,咱們再來看一下爲何會有資源的泄漏。首先和代碼側的泄漏同樣,因爲「存在該釋放卻沒有釋放的錯誤引用」,致使回收機制認爲目標對象不是「垃圾」,以致於不能被回收,這也是最多見的一種狀況。工具

針對資源,還有一種典型的泄漏狀況。因爲資源卸載是主動觸發的,那麼清除對資源引用的時機就顯得尤其重要。如今遊戲的邏輯趨於複雜化,同時若是有新成員加入項目組,也未必可以清楚地瞭解全部資源管理的細節,若是「在觸發了資源卸載以後,才清除對資源引用」,一樣也會出現內存泄漏了。 
遇上了資源回收 
遇上了資源回收 
錯過了資源回收 
錯過了資源回收

還有一種資源上的泄漏,是由於Unity的一些接口在調用時會產生一份拷貝(例如Renderer.Material參考https://docs.unity3d.com/ScriptReference/Renderer-material.html),若是在使用上不注意的話,運行時會產生較多的資源拷貝,形成內存的無故浪費。可是此類內存拷貝通常量較少,修復起來也比較簡單,這裏不作大篇幅的介紹。

修復內存泄漏

根據上文描述,咱們知道只要在回收到來以前,將引用解開就能夠避免內存泄漏了,彷佛是個很簡單的問題。可是因爲實際項目的邏輯複雜度每每超出想象,引用關係也不是簡單的一層兩層(有時候每每會多達十幾層,甚至數十層才鏈接到最終的引用對象),而且可能存在交叉引用、環狀引用等複雜狀況,單純從代碼review的角度,是很難正確地解開引用的。如何查找致使泄漏的引用,是修復泄漏的難點和重點,也是本文主要想介紹的部分,下面就針對如何查找引用介紹一些思路和方法。至於時序問題,比較簡單,在此不作贅述。

New Memory Profiler For Unity5

Unity的Memory Profiler一直就是一個被用戶詬病的地方,對於內存的使用量,被誰使用等信息,沒有很好的反映。Unity5做爲最新一代的Unity產品,對於這個弱點進行了一些補強,推出了新一代的內存分析工具,較好地解決了上述問題。可是沒有提供兩次(或屢次)內存快照的比較功能,這點比較遺憾。 
注:內存快照比較是尋找內存泄漏的經常使用手段,將兩次內存的狀態截取出來,進行比較,能夠清楚地發現內存的變化,尋找內存的增量與泄漏點。通常會在遊戲進關前以及出關後作兩次dump,其中新增的內存分配,能夠視爲泄漏。 
這裏寫圖片描述
這裏寫圖片描述

因爲是Unity官方的工具,網上有比較詳細的使用教程,在此不加贅述,能夠參考下列連接或Google: 
Unity-Technologies MemoryProfiler 
memoryprofiler intro 
因爲Unity5普及度及穩定性還有待提高,公司內廣泛仍是4.x的環境,那麼上述的新工具就不適用了。有的同窗說,升級一個5的工程來作Memory Profile嘛,這個固然也能夠,不過Unity5對於4的兼容性不太好,升級過程當中須要修改很多東西,維護兩個工程也是比較麻煩的事。

那麼,下面就給出兩個在Unity4環境下也可使用的泄漏追蹤工具。

Mono內存的放大鏡——Cube

Cube是 騰訊遊戲下的騰訊WeTest平臺上針對Unity項目的性能指標收集工具,經過Cube能夠較方便地獲取到遊戲的各項性能指標,爲性能優化提供了方向。同時Cube也是遊戲性能一個很好的衡量工具。微信號無法直接點開連接,因此點擊「閱讀原文」能夠進到工具頁面。(我真的不是在作廣告) 
這裏寫圖片描述 
這裏寫圖片描述
這裏咱們利用「MONO內存對象深度分析」的特色。該功能能夠容許用戶抓取某一時刻的Mono內存狀態,而且提供不一樣時刻內存狀態的比較,快速定位到新增的內存分配。

鑑於Cube官方已經給出了詳細的使用說明,就再也不贅述數據的抓取過程。這裏簡單聊一下如何經過Cube抓取的數據更好地追蹤和解決問題。

以下圖所示,假設咱們已經抓取了兩次數據(snapshot1 & snapshot2),而且進行比較,獲得兩次內存快照之間新增的分配數據。

這裏寫圖片描述

比較以後獲得以下圖所示的一系列數據,總結來講,就是在某個堆棧,分配了某個類型的對象,佔用xx內存。這樣的數據會有成千上萬條(上文所說,代碼中的內存分配,是很是細碎,而且數量極多的,在這裏獲得了驗證),而且其中有不少堆棧是重複的,由於每一次的內存分配(即便是同一處位置產生的分配),都會產生一條記錄。無序的數據影響了咱們對數據的處理,這裏咱們對數據作一些分析整理。

這裏寫圖片描述

咱們舉一些簡單的例子來講明處理的過程。

每一條記錄,都是通過一系列的函數調用(堆棧),最終分配了一些內存,用圖形化的方式表示爲:

這裏寫圖片描述 
讓咱們多加一些數據:

這裏寫圖片描述

經過對圖的觀察,咱們發現能夠把上述離散的圖整理成一棵樹:

這裏寫圖片描述

將全部數據都作一樣的歸類處理以後,能夠獲得一棵或多棵這樣的分配樹。這麼作的好處是: 
1) 根據函數,能夠將內存的分配作一個模塊的劃分,快速定位到相關的模塊。 
2) 能夠清晰地看到每一層函數的分配總量(如A函數總共分配4096+20+4096B),能夠根據佔用內存的多少決定修復的優先級。 
將對比以後的新增項一一清理以後,就能夠基本清除Mono內存的多餘分配和泄漏了。

順藤摸瓜——從Mono中尋找資源引用

在嘗試尋找資源引用,修復資源泄露以前,咱們須要先了解一下如何在Unity中定位資源泄漏。 
咱們須要使用Unity自帶的Memory Profiler(注意不是上文說的Unity5的新Profiler,是老的殘疾版Profiler)。舉個簡單的例子,在Unity編輯器環境下運行遊戲工程,通過「大廳」頁面,進入到「單局」。此時打開Unity Profiler,切換到Memory並作一次內存採樣(具體請參考https://docs.unity3d.com/Manual/ProfilerMemory.html,不贅述)。 在採樣的結果中(其中包含採樣時刻內存中全部的資源),點開Assets->Texture2D,若是其中能夠看到有「大廳」UI使用的貼圖(以下圖),那麼咱們能夠定義這張UI貼圖,屬於資源上的泄漏。

這裏寫圖片描述 
爲何說這種狀況就屬於資源泄漏呢,由於這張UI貼圖,是在「大廳」時申請的,可是在「單局」時,它已經不被須要了,但是它還在內存中。這種在不須要的時候,卻還存在的內存佔用,就是上文咱們定義的內存泄漏。

那麼在平時項目中,咱們如何找到這些泄漏的資源呢? 
最直觀的方法,固然也是最笨的方法,就是在每次遊戲狀態切換的時候,作一次內存採樣,而且將內存中的資源一一點開查看,判斷它是不是當前遊戲狀態真正須要的。這種方法最大的問題,就是耗時耗力,資源數量太多眼睛容易看花看漏。

這裏介紹兩種討巧的方法: 
1) 經過資源名來識別。即在美術資源(如貼圖、材質)命名的時候,就將其所屬的遊戲狀態放在文件名中,如某貼圖叫作BG.png,在大廳中使用,則修改成OG_BG.png(OG = OutGame)。這樣在一坨IG(IG=InGame)資源裏面,混入了一個OG,能夠很容易地識別出來,也方便利用程序來識別。這麼作還有一個好處,能夠強化美術對資源生命週期的認識,在製做資源,特別是規劃UI圖集時,能夠有一個指導意義。 
2) 經過Unity提供的接口Resources.FindObjectsOfTypeAll()進行資源的Dump,能夠根據需求Dump貼圖、材質、模型或其餘資源類型,只須要將Type做爲參數傳入便可。Dump成功以後咱們將結果保存成一份文本文件,這樣能夠用Beyond Compare對屢次Dump以後的結果進行比較,找到新增的資源,那麼這些資源就是潛在的泄漏對象,須要重點追查。 
結合上述的方法與思路,應該能夠輕鬆找到泄漏的資源了。

此時咱們再回頭看一下Unity Profiler,其實Unity提供了資源索引的查找功能,只不過該功能是以一個樹形結構的文原本展現的(以下圖)。上文曾提到過,Unity內部的引用關係每每是很是複雜的,可能須要經過十幾甚至幾十層的引用,才能找到最終的引用者,而且引用關係錯綜複雜,造成一張龐大的圖,此時光靠展開樹形結構來查找,幾乎是不可能的事了。

這裏寫圖片描述

防微杜漸,避免內存泄漏

介紹完對於Unity內存泄漏的追蹤方法,我還想往下多講一步,只要咱們在平時開發的過程多作思考,防微杜漸,內存泄漏是徹底能夠避免的。相對於等泄漏發生了再回頭來追查,平時多花點時間清理「垃圾」反而是更加高效的作法。 
落地到平時的開發流程中,在這裏提出幾點建議,歡迎各位大牛補充: 
1) 在架構上,多添加析構的abstract接口,提醒團隊成員,要注意清理本身產生的「垃圾」。 
2) 嚴格控制static的使用,非必要的地方禁止使用static。 
3) 強化生命週期的概念,不管是代碼對象仍是資源,都有它存在的生命週期,在生命週期結束後就要被釋放。若是可能,須要在功能設計文檔中對生命週期加以描述。 
相信你們出門旅遊,都有看過下圖相似的標語,做爲一名合格的程序猿,也應該可以處理好代碼中的「垃圾」,不要讓咱們的遊戲成爲一個「垃圾場」。

爲了不以上手遊性能方面對遊戲的負面影響,騰訊WeTest平臺下的Cube工具能夠幫助開發者發現遊戲內分類資源的一個佔用狀況,幫助在遊戲開發過程當中不斷改善玩家的體驗。目前功能還在免費開放中。點擊http://wetest.qq.com/cube/當即體驗!

相關文章
相關標籤/搜索