Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 深入淺出再談Unity內存洩漏

深入淺出再談Unity內存洩漏

編輯:關於Android編程

WeTest導讀

本文通過對內存洩漏(what)及其危害性(why)的介紹,引出在Unity環境下定位和修復內存洩漏的方法和工具(how)。最後提出了一些避免洩漏的方法與建議。

在之前推送的文章,已經對騰訊游戲在Unity游戲開發過程中常見的Mono內存管理問題進行了介紹,收到了很多用戶的反饋,希望能夠更全面的介紹關於unity內存管理的問題。本期微信推送騰訊WeTest團隊邀請到了公司中資深的測試專家Arthuryu,對Unity內存洩漏進行一個更加系統的介紹。

內存洩漏及其危害

相信各位程序猿們或多或少都會聽到過內存洩漏這個名詞,但是對於一些新手猿來說,或許不是很了解。內存洩漏?是內存漏出來了麼?和霸氣側漏一樣麼?讓我們先來看一下wikipedia的定義:

這裡寫圖片描述vcC0veLKzc/C1eK49rao0uWhozwvcD4NCjxwPsTatObQucKpo6y/ydLUzajL173iys3OqjxzdHJvbmc+JmxkcXVvO73o0vjQ0Meusru7uSZyZHF1bzs8L3N0cm9uZz6ho9TavMbL47v6tcS2/r341sbKwL3nwO+jrLLZ1/fPtc2zvs3Kx9L40NCju8O/0ruxyrT7v+6jrLa8ysfSu7TOxNq05rXEyerH66O7tvjE46Osvs3Kx9K7uPbTptPDs8zQ8qGjvLQ8c3Ryb25nPsTjPC9zdHJvbmc+z/I8c3Ryb25nPtL40NC0+7/uIDwvc3Ryb25nPj0gPHN0cm9uZz7TptPDs8zQ8jwvc3Ryb25nPs/yPHN0cm9uZz6y2df3z7XNszwvc3Ryb25nPsnqx+s8c3Ryb25nPsTatOY8L3N0cm9uZz6ho7WxyLujrNTavMbL47v6ysC959bQo6zO0sPH0OjSqrjQ0Luy2df3z7XNs6Os0vLOqsv7ysfSu7j2srvK1cD7z6K1xNL40NCjrMTjvejBy7bgydnE2rTmo6zE477N1rvQ6NKqu7m72LbgydnE2rTmoaPEx8O0ztLDx7/J0tTX3L3h0rvPwqOsPHN0cm9uZz7E2rTm0LnCqbXEvPK1pbao0uWjrL7NysfJ6sfrwcvE2rTmo6zItMO709DU2rjDys23xbXEyrG68srNt8U8L3N0cm9uZz6hozwvcD4NCjxwPsjnufvE49fcyse0+7/utviyu7u5x66jrMTHw7TS+NDQwO+1xMeuvs3UvcC01L3J2aOs1+7W1bW81sLG5Mv7yMvSqr3ox67KsaOsvs3O3seuv8m96MHLoaPP1sq1yfq77tbQo6zS+NDQzqrBy7Hcw+LO3seuv8m906Osvs274bDR19zKx73ox66yu7u5tcTIy8CtyOu62sP7taWjrLK71Nm96Mv7x66ju7b4stnX98+1zbPU8rj8vNPQ17LQo6zL+7vh1rG90yZsZHF1bzvX9sHLxOMmcmRxdW87o6yy2df3z7XNs72ru+HWsb3Ta2lsbLX006bTw7PM0PKho9PJtMu/ydLUv7Sz9qOsxNq05tC5wqm1xM6juqbQ1NPr0c/W2NDUo6w8c3Ryb25nPsjnufuz1tD40LnCqaOsvavS8sTatObVvNPDuf2087b4tbzWwtOm08OxwMCjPC9zdHJvbmc+oaO1sci70LnCqbu509DG5Mv7tcTOo7qmo6zA/cjnxNq05rG7zt7Tw7bUz/PVvNPDo6y1vNbCvdPPwsC0tcTE2rTmt9bF5NDo0qq4/LjftcTKsbzks8mxvqOstNO2+NTss8nTzs+3tcS/qLbZtci1yKGjPC9wPg0KPHA+PGltZyBhbHQ9"這裡寫圖片描述" src="/uploadfile/Collfiles/20160826/20160826091735240.png" title="\" />

Unity中的內存洩漏

在對內存洩漏有一個基本印象之後,我們再來看一下在特定環境——Unity下的內存洩漏。大家都知道,游戲程序由代碼和資源兩部分組成,Unity下的內存洩漏也主要分為代碼側的洩漏和資源側的洩漏,當然,資源側的洩漏也是因為在代碼中對資源的不合理引用引起的。

代碼中的洩漏 – 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的一些接口在調用時會產生一份拷貝,如果在使用上不注意的話,運行時會產生較多的資源拷貝,造成內存的無端浪費。但是此類內存拷貝一般量較少,修復起來也比較簡單,這裡不做大篇幅的介紹。

修復內存洩漏

根據上文描述,我們知道只要在回收到來之前,將引用解開就可以避免內存洩漏了,似乎是個很簡單的問題。但是由於實際項目的邏輯復雜度往往超出想象,引用關系也不是簡單的一層兩層(有時候往往會多達十幾層,甚至數十層才連接到最終的引用對象),並且可能存在交叉引用、環狀引用等復雜情況,單純從代碼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工具可以幫助開發者發現游戲內分類資源的一個占用情況,幫助在游戲開發過程中不斷改善玩家的體驗。目前功能還在免費開放中。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved