記憶體洩露是開發人員在項目研發過程中最常見也最不願遇到的問題。就目前來看,大家對于判斷項目是否存在記憶體洩露仍然存在一些誤區:
誤區一 我的項目進出場景前後記憶體回落不一緻,比如進入場景後,記憶體增加40MB,出來後下降30MB,仍有10MB記憶體沒有傳回給系統,即說明記憶體存在洩露情況。 誤區二 我的項目在進出場景前後,Unity Profiler中記憶體回落正常,但Android的PSS數值并沒有完全回落(出場景後的PSS值高于進場景前的PSS值),即說明記憶體存在洩露情況。
以上是我們遇到的開發團隊回報給我們的典型問題。相信大多數開發團隊都會遇到類似的情況。在此有必要說明一下,以上兩種情況均不能表明記憶體存在洩漏問題。即便記憶體在一段時間始終保持增長的趨勢,也不能簡單地判定其存在記憶體洩露。因為造成記憶體不能完全回落的情況有很多,比如資源加載後常駐記憶體以備後續使用、Mono堆記憶體的隻升不降等等,這些均可造成記憶體無法完全回落。一般來說,我們推薦的判斷記憶體是否洩漏的方法如下:
在我們進行過的項目深度優化過程中,資源洩漏是記憶體洩露的主要表現形式,其具體原因是使用者對加載後的資源進行了儲存(比如放到Container中),但在場景切換時并沒有将其Remove或Clear,進而無論是引擎本身還是手動調用Resources.UnloadUnusedAssets等相關API均無法對其進行解除安裝,進而造成了資源洩露。對于這種情況的排查相當困難,這是因為項目中的資源量過于巨大,洩露資源往往很難定位。是以,我們在UWA測評報告中對項目中的每個資源都進行了詳細的監控,并通過“生命周期”這一衡量名額讓大家可以清楚地了解到每個資源在項目運作過程中的使用範圍。

這樣,大家可以通過資源的“生命周期”屬性來快速檢視有哪些資源是“常駐”記憶體的,并且判斷該資源是“預加載”資源還是“洩露”資源。
同時,項目中所使用的總資源數量往往是成百上千的,讓大家逐個資源檢查過來是一件很費力的事情。是以,我們推出了資源的“場景比較”功能。建議大家通過以下兩種方式進行資源比較,以便更快地找到存在“洩露”問題的資源:
同種類型場景或同一場景進行比較 一般來說,同種場景或同一場景的資源使用應該是較為固定的,比如遊戲項目中的主城場景或主界面場景。通過比較不同時刻同一場景的資源資訊,可以快速幫你找到其資源使用的差異情況。這樣,你隻需判斷這些“差異”資源的存在是否合理,即可快速判定是否存在資源洩露,已經具體的洩露資源。
不同類型場景進行比較 除一些常駐資源外,不同類型的場景,其資源使用是完全不同的。比如,遊戲中主城和戰鬥副本的資源,除少部分常駐記憶體的資源外,二者使用的絕大部分資源應該是不一緻的。是以,通過比較兩種不同類型的場景,你可以直接檢視比較結果中的“共同資源”,并判斷其是否确實為預先設定好的常駐資源。如果不是,則它很可能是“洩露”資源,需要你進一步檢視項目的資源管理是否存在漏洞。
AssetBundle的管理不當也會造成一定的記憶體洩露,即上一場景中使用的AssetBundle在場景切換時沒有被解除安裝掉,而被帶入到了下一場場景中。對于這種情況,建議直接通過Profiler Memory中的Take Sample來對其進行檢測,通過直接檢視WebStream或SerializedFile中的AssetBundle名稱,即可判斷是否存在“洩露”情況。
承接上述“誤區二”中的說法,“Unity Profiler中記憶體回落正常,但Android的PSS數值并沒有完全回落”是有可能的,這是因為Unity Profiler回報的是引擎的真實配置設定的實體記憶體,而PSS中記錄的則包括系統的部分緩存。一般情況下,Android或iOS并不會及時将所有App解除安裝資料進行清理,為了保證下次使用時的流暢性,OS會将部分資料放入到緩存,待自身記憶體不足時,OS Kernel會啟動類似LowMemoryKiller的機制來查詢緩存甚至殺死一些程序來釋放記憶體。是以,并不能通過一兩次的PSS記憶體沒有完全回落來說明記憶體洩露問題。
我們推薦的測試方式是在兩個場景之間來回不停切換,比如主城和戰鬥副本間。理論上來說,多次切換同樣的場景,如果Profiler中顯示的Unity記憶體回落正常,那麼其PSS/Instrument的記憶體數值波動範圍也是趨于穩定的,但如果出現了PSS/Instrument記憶體持續增長的情況,則需要大家注意了。這可能有兩種可能:
Unity引擎自身的記憶體洩露問題。這種機率很小,之前僅在少數版本中出現過。 第三方插件在使用時出現了記憶體洩露。這種機率較大,因為Profiler僅能對Unity自身的記憶體進行監控,而無法檢測到第三方庫的記憶體配置設定情況。是以,在出現上述記憶體問題時,建議大家先對自身使用的第三方庫進行排查。
目前,Unity所使用的Mono版本中存在一個較大的問題,即記憶體一旦配置設定,則不會再傳回給系統。這就衍生出另外一個問題—— 無效的Mono堆記憶體。它是Mono所配置設定的堆記憶體,但卻沒有被真正利用上,是以稱之為“無效”。那麼,如何檢視我的項目中是否存在較大量的“無效堆記憶體”呢?
在UWA測評報告中,我們提供了記憶體随項目運作的配置設定情況,如下圖所示。其中,藍線和紫線的分離情況,反映了無效堆記憶體的配置設定大小。比如,圖中所選中時刻,藍線的Reserved Total為目前項目所占據的總實體記憶體,而紫線的Used Total為目前項目所使用的總實體記憶體,這說明目前項目中的空閑記憶體為57.1MB(200.4-143.3),而這其中主要由兩部分組成,空閑的Unity引擎記憶體和無效的Mono堆記憶體。其中,空閑的Unity記憶體為17.1MB(92.0-74.9),是以目前所選幀的無效Mono堆記憶體為40.0MB。并且,從圖中可以看出,藍線和紫線在運作過程中一直分得較開,這說明一直存在不小的Mono堆記憶體處于“無效”狀态。這是一件很浪費的事情,特别是對于記憶體寸土寸金的移動裝置而言。
那麼,我們應該如何避免或減少過多“無效堆記憶體”的配置設定呢?我們推薦的做法如下:
避免一次性堆記憶體的過大配置設定。Mono的堆記憶體也是“按需”逐漸進行配置設定的。但如果一次性開辟過大堆記憶體,比如New一個較大Container、加載一個過大配置檔案等,則勢必會造成Mono的堆記憶體直接沖高,是以研發團隊對堆記憶體的配置設定需要時刻注意; 避免不必要的堆記憶體開銷。UWA測評報告中将項目運作過程中堆記憶體配置設定Top10函數進行羅列,限于篇幅,我們不再此處進行一一贅述,研發團隊可以直接檢視之前一篇的記憶體優化相關文章。
在記憶體管理方面,還有一個大家必須關注的話題——資源備援。在我們測評過的大量項目中,95%以上的項目均存在不同程度的資源備援情況。所謂“資源備援”,是指在某一時刻記憶體中存在兩份甚至多份同樣的資源。導緻這種情況的出現主要有兩種原因:
同一份資源被打入到多份AssetBundle檔案中。舉個例子,同一張紋理被不同的NPC所使用,同時每個NPC被制作成獨立的AssetBundle檔案,那麼在沒有針對紋理進行依賴打包的前提下,就會出現該張紋理出現在不同的NPC AssetBundle檔案中。當這些AssetBundle先後被加載到記憶體後,記憶體中即會出現紋理資源備援的情況。對此,我們建議研發團隊在發現資源備援問題後,對相關AssetBundle的制作流程一定要進行檢查。
同時,我們在UWA測評中為每個資源引入了一個衡量名額——“數量峰值”。它指的是同一資源在同一幀中出現的最大數量。如果大于1,則說明該資源很可能存在 “備援資源”。大家可以通過這一列進行排序,即可立即檢視項目中的資源備援情況。
在Unity引擎中,當我們修改了一些特定GameObject的資源屬性時,引擎會為該GameObject自動執行個體化一份資源供其使用,比如Material、Mesh等。以Material為例,我們在研發時經常會有這樣的做法:在角色被攻擊時,改變其Material中的屬性來得到特定的受擊效果。這種做法則會導緻引擎為特定的GameObject重新執行個體化一個Material,字尾會加上(instance)字樣。其本身沒有特别大的問題,但是當有改變Material屬性需求的GameObject越來越多時(比如ARPG、MMORPG、MOBA等遊戲類型),其記憶體中的備援數量則會大量增長。如下圖所示,随着遊戲的進行,執行個體化的Material資源會增加到333個。雖然Material的記憶體占用不大,但是過多的備援資源卻為Resources.UnloadUnusedAssets API的調用效率增加了相當大的壓力。
一般情況下,資源屬性的改變情況都是固定的,并非随機出現。比如,假設GameObject受到攻擊時,其Material屬性改變随攻擊類型的不同而有三種不同的參數設定。那麼,對于這種需求,我們建議你直接制作三種不同的Material,在Runtime情況下通過代碼直接替換對應GameObject的Material,而非改變其Material的屬性。這樣,你會發現,成百上千的instance Material在記憶體中消失了,取而代之的,則是這三個不同的Material資源。其中的益處,對于能夠閱讀到這裡的你來說,應該已經不需要我多說了。:)
以上則是我們在記憶體優化工作中的經驗和心得,希望它對你的項目研發有所幫助。優化永遠沒有統一的标準方案,隻有最适合你項目的方案,希望大家可以活學活用,不要放過任何一處讓你感覺“不對勁”的地方。最後提醒大家——“勿以善小而不為,勿以惡小而為之”,共勉!
原文出處:侑虎科技
轉載請與作者聯系,同時請務必标明文章原始出處和原文連結及本聲明。