關於 Unity 項目中的 Mono 堆內存泄露


關於 Unity 項目中的 Mono 堆內存泄露

題記:這是補一篇應該在將近一年前就應該寫的記錄,今天終於補上。

內存泄露是一個老話題了,之前我專門寫過一篇 排查 Lua 虛擬機內存泄露 的文章,並且附帶了一個工具來查找 Lua 中具體的內存泄露。但是這只是整個 Unity 項目中內存泄漏的一小部分,C# 代碼中一般內存泄露可能會更加嚴重。

我們之前發現無論在 Profiler 還是工具測試,隨着戰斗的增加,總體內存都是一直在增長,很明顯是有了內存泄露。為了首先能夠徹底檢測到底是哪里出現了泄露,找了很多的工具,也參看了很多的文章,最終感覺首要的問題是需要完整的 C# 內存的快照明細,Mono 提供了一個過時的 HeapShot,看起來就是做這件事情的,但是離直接拿到項目里去使用還有不少的距離。在最后快要放棄打算自己 Hook Mono 的 API 編寫時,發現了騰訊的 WeTest Cube 工具。其中有一項是專門用來打印詳細內存快照的功能,根據自己的需要來選取內存快照,並且給出測試期間的整體內存走勢,使用也很簡單,在需要測試的手機上安裝 WeTest Cube 軟件(注意:手機必須要 Root),然后通過 Cube 里面選擇設置,然后啟動游戲即可。

啟動后在想要打印內存快照的地方,點擊屏幕上的 "快照" 按鈕,就會在后台生成快照,多點幾份然后再雙擊該按鈕退出測試,然后上傳數據完成,最后生成的快照數據需要再從 Cube 的網頁報告端下載。

由於游戲項目較大,下載后的快照解壓縮后基本都在 40+Mb,內容是 json 格式的文本,存取了每一項內存數據的地址,類型,引用鏈:

{"ptr":-1721623792,"type":"Texture2D","size":16,"stack":"|UnityEngine.GUIStyleState:ProduceGUIStyleStateFromDeserialization (UnityEngine.GUIStyle,intptr)|UnityEngine.GUIStyle:InternalOnAfterDeserialize ()|UnityEngine.GUIUtility:GetDefaultSkin ()|UnityEngine.GUI:DoSetSkin (UnityEngine.GUISkin)|UnityEngine.GUI:set_skin (UnityEngine.GUISkin)|UnityEngine.GUIUtility:BeginGUI (int,int,int)","reference":"|-1721577968"}

ptr - 內存地址
type - 數據類型
size - 內存大小
stack - 該內存被分配時的調用堆棧
reference - 被引用的地址,如果這里有多個,就是被多個其它的地址引用了

所以,依然在主城中打印一份快照,進入戰斗打印一份快照,再回到主城打印一份快照,自己編寫一個小工具解析兩份主城的快照,並且將第二份新增的部分輸出成一個單獨的文件;然后再編寫一個小工具將新增部分根據數據類型 type 來歸類,將同類型的 size 相加,最后生成一個文件,里面兩列:類型大小。然后用 Excel 打開並按照 大小 降序排列,便可以直接看到那些新增的內存,根據項目情況分析新增部分從 類型大小 兩個角度來講是否應該出現就立即分析出潛在的泄露,然后把泄露的 類型 拿到原始的快照(第二次主城快照)中去搜索,然后查看該類型到底被什么地方引用,一直追溯下去,便能找到最終的引用點,說明是因為“它”的引用才造成泄露。

接下來的事情就是去項目中實際分析代碼,查看創建和釋放的地方是否有紕漏,需要比較耐心的去項目代碼中查找和分析,其實這都是苦活,臟活兒,要的都是耐心,你需要從成千上萬條數據中揪出可能的泄露。

后來做完這次優化,我再次感受到,出現這些泄露的原因歸根結底還是代碼不夠規范引起的,不注重必要的初始化和釋放成對調用等,大致總結起來有以下幾點:

  • 錯誤使用了過多的 readonly,並且引用了其他的類型,甚至項目引用,最終形成引用環,並且環的某一個節點又被全局對象引用,造成“蝌蚪的尾巴被拴住”的情況,因此連帶了很多的對象無法釋放。
  • 每一個類應該是有 Initialize 就有 Release,但實際情況大多是有Initialize 而沒有 Release
  • 注意 Coroutine,如果使用了全局的 MonoBehaviour 對象 AStartCoroutine 其他對象 B 的函數,那么這時 B 對象會被 A 對象引用,如果協程為正確執行結束在無線等待,那么 B 也會永遠被 A 對象引用而不得釋放,即使 StopAllCoroutine 也不行。
  • 慎用 Delegate,這時大家經常使用的功能,但是也容易造成內存泄露,如果只是 += 而沒有在合適的地方 -=,那么代理的方法所屬的對象是會被一直引用而無法釋放的。

總結起來很簡單,這次優化就大致發現這幾條,但是都不經意間,日積月累,引起了比較嚴重的問題,所以不要小看代碼規范,和框架的遵守。

下面是項目優化前后的內存走勢,總時長相同。

  • 優化前:
    優化前

  • 優化后:
    優化后

以上可以看出,經過一輪粗獷優化后內存依然保持小幅的總體增長,但是比優化前的總體大小已經大幅降低,並且漲幅也更加平緩。接下來可能會需要更大的精力和更多的手段去查找剩下的“一點點”內存泄露。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM