Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 內存是手游的硬傷——Unity游戲Mono內存管理與洩漏

內存是手游的硬傷——Unity游戲Mono內存管理與洩漏

編輯:關於Android編程

WeTest導讀

內存是游戲的硬傷,如果沒有做好內存的管理問題,游戲極有可能會出現卡頓,閃退等影響用戶體驗的現象。本文介紹了在騰訊游戲在Unity游戲開發過程中常見的Mono內存管理問題,並介紹了一系列解決的策略和方法。

內存是手游的硬傷

無論是游戲還是VR應用,內存管理都是其研發階段的重中之重。然而,90%以上的項目都存在不同程度的內存使用問題。就目前基於Unity引擎開發的移動游戲和移動VR游戲而言,內存的開銷無外乎以下三大部分:
1.資源內存占用;
2.引擎模塊自身內存占用;
3.托管堆內存占用。
今天我們將針對由Mono分配和管理的托管堆內存,介紹Unity游戲開發中面臨的Mono內存管理及洩漏問題。

什麼是Mono內存

對於目前絕大多數基於Unity引擎開發的項目而言,其托管堆內存是由Mono分配和管理的。“托管” 的本意是Mono可以自動地改變堆的大小來適應你所需要的內存,並且適時地調用垃圾回收(Garbage Collection)操作來釋放已經不需要的內存,從而降低開發人員在代碼內存管理方面的門檻。
Unity游戲在運行時的內存占用情況可以用下圖表示:
這裡寫圖片描述vcyoveLO9rKi1MvQ0EMjtPrC67XEo6zU2kFuZHJvaWTPtc2zyc+jrNPOz7e1xGxpYsS/wrzPwrTm1Nq1xGxpYm1vbm8uc2/OxLz+o6y+zcrHbW9ub9TaQW5kcm9pZM+1zbPJz7XEyrXP1qGjQyO0+sLrzai5/W1vbm+94s721rTQ0KOsy/nQ6NKqtcTE2rTm19TIu9KyysfTyW1vbm/AtL340NC31sXkudzA7aOsz8LD5r7NvenJ3NK7z8Jtb25vtcTE2rTmudzA7bLfwtTS1LywxNq05tC5wqm31s72oaM8L3A+DQo8aDIgaWQ9"mono內存管理策略">Mono內存管理策略

Mono通過垃圾回收機制(Garbage Collect,簡稱GC)對內存進行管理。Mono內存分為兩部分,已用內存(used)和堆內存(heap),已用內存指的是mono實際需要使用的內存,堆內存指的是mono向操作系統申請的內存,兩者的差值就是mono的空閒內存。當mono需要分配內存時,會先查看空閒內存是否足夠,如果足夠的話,直接在空閒內存中分配,否則mono會進行一次GC以釋放更多的空閒內存,如果GC之後仍然沒有足夠的空閒內存,則mono會向操作系統申請內存,並擴充堆內存,具體如下圖所示。
這裡寫圖片描述

通過上文可知,GC的主要作用在於從已用內存中找出那些不再需要使用的內存,並進行釋放。Mono中的GC主要有以下幾個步驟:
1.停止所有需要mono內存分配的線程。
2.遍歷所有已用內存,找到那些不再需要使用的內存,並進行標記。
3.釋放被標記的內存到空閒內存。
4.重新開始被停止的線程。
除了空閒內存不足時mono會自動調用GC外,也可以在代碼中調用GC.Collect()手動進行GC,但是,GC本身是比較耗時的操作,而且由於GC會暫停那些需要mono內存分配的線程(C#代碼創建的線程和主線程),因此無論是否在主線程中調用,GC都會導致游戲一定程度的卡頓,需要謹慎處理。另外,GC釋放的內存只會留給mono使用,並不會交還給操作系統,因此mono堆內存是只增不減的。

Mono內存洩漏分析

Mono是如何判斷已用內存中哪些是不再需要使用的呢?是通過引用關系的方式來進行的。Mono會跟蹤每次內存分配的動作,並維護一個分配對象表,當GC的時候,以全局數據區和當前寄存器中的對象為根節點,按照引用關系進行遍歷,對於遍歷到的每一個對象,將其標記為活的(alive)。
這裡寫圖片描述

如上圖所示,假設A是處於全局數據區的一個對象,那麼在GC的時候將作為根節點進行遍歷,由於B、C、D對象都可以由A遍歷到,因此被標記為活的,E、F對象則沒有被標記。注意,由於引用關系是單向的,A引用了B並不代表B也引用了A,所以遍歷也只能單向進行。

由於GC以全局數據區和當前寄存器中的對象為根節點進行遍歷,所以對象的被標記意味著該對象可以通過全局對象或者當前上下文訪問到,而沒有被標記的對象則意味著該對象無法通過任何途徑訪問到,即該對象“失聯”了,GC最終會將所有“失聯”的對象內存進行回收,上圖中的E和F將會在GC過程中被回收。

既然mono已經有了完善的GC機制,那是否還會存在內存洩漏呢?答案是肯定的,只是此處的內存洩漏需要重新定義一下,我們把對象已經不再需要使用卻沒有被GC回收的情況稱為mono內存洩漏。Mono內存洩漏會使空閒內存減少,GC頻繁,mono堆不斷擴充,最終導致游戲內存占用的升高。下圖就是一個mono內存洩漏的例子。
這裡寫圖片描述

解決辦法

對於mono內存洩漏,一般只能通過猜測+不斷修改代碼測試的方法來修復問題,效率很低,騰訊Wetest平台的Cube工具提供了mono內存快照對比的功能,並包括對象分配堆棧,對象引用關系等詳細信息,是定位mono內存洩漏問題的一大利器。下面結合具體的代碼嘗試使用Cube定位mono內存洩漏問題。
首先我們定義類A,並在A的構造函數中申請了一塊int[1000]大小的內存。
這裡寫圖片描述

接著我們定義A類型的靜態變量objectA,在游戲界面上繪制一個按鈕,並在按鈕點擊事件中給objectA賦值,此時新生成了new int[1000]對象,並由objectA引用。
這裡寫圖片描述

使用Cube的mono內存檢測功能,並在按鈕按下之前和按下之後分別進行一次快照,對比兩次快照,查看快照間新增對象。
這裡寫圖片描述
這裡寫圖片描述

可以看到,按鈕按下前後新增的最大對象即為代碼中生成的new int[1000]對象,並且該對象被引用的次數為1,為了查看詳細的引用關系,下載快照文件snapshot2,其中有這樣兩行數據:
這裡寫圖片描述

第一行說明在OnGUI函數中生成了一個A類型的對象,其指針為1533098928,第二行說明在OnGUI()->A:.cotr()中生成了一個Int32[]類型的對象,並且該對象被指針為1533098928的對象引用。即new int[1000]對象被objectA引用,這也是導致new int[1000]對象無法被GC回收的原因。而objectA本身是一個靜態對象,是GC的根節點,因此沒有對象引用。
如果需要生成的new int[1000]對象被回收怎麼做呢?很簡單,將objectA.a設置為null,沒有了objectA對其的引用,自然會被GC回收了。需要說明的是,將objectA.a設置為null只是斷絕了引用關系,真正對象的回收要等到GC的時候才會進行,Cube在獲取內存快照的時候會首先進行一次GC,防止由於沒有及時調用GC導致的誤判。

游戲中大部分mono內存洩漏的情況都是由於靜態對象的引用引起的,因此對於靜態對象的使用需要特別注意,盡量少用靜態對象,對於不再需要的對象將其引用設置為null,使其可以被GC及時回收,但是由於游戲代碼過於復雜,對象間的引用關系層層嵌套,真正操作起來難度很大。可以首先使用Cube工具進行分析,根據mono內存趨勢找出洩漏的具體場景,然後再使用快照對比功能進行詳細分析。

騰訊游戲品質管理團隊專門打造的工具“Cube”目前已經可以使用,“Cube”可以幫助開發者發現Unity手游內分類資源的占用情況,尤其是對Unity游戲場景中的FPS、CPU、PSS的變化趨勢重點關注,幫助在Unity游戲開發過程中不斷改善玩家的體驗。目前功能免費開放中。

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