unity內存泄漏分析實踐

內存泄漏分析

背景:
手機性能還不錯,綜合評分可以算是高端機,一般手遊開高特效都沒問題;
在這裏插入圖片描述
本次測試爲跑新手指引,遊戲架構採用的是重度Xlua,幾乎所有邏輯都是lua編寫,UI採用FGUI;
使用perfdog查看了一下整體數據,流暢度一般,但是內存有問題
在這裏插入圖片描述
30分鐘,考慮到內存一直在上升,有很大可能內存泄漏;
於是繼續查看遊戲裏運行的詳細數據,這裏可以使用工具unity Profiler,UWA,UPA等等,我這裏使用的是UWA;
發現留存堆內存果然持續上升,發生內存泄漏;
在這裏插入圖片描述
繼續分析,查看代碼消耗的內存;
從性能堆棧中可以發現,其分配主要爲FGUI所致
在這裏插入圖片描述
鎖定了FGUI,接下來分析具體函數,這個函數是FGUI的主要函數之一,它的逐步升高,也說明UI的數量在不斷加大,
在這裏插入圖片描述
確定了fgui存在內存泄漏,接下來需要查看項目具體代碼了;
在項目中可以使用Lua Profiler分析查看具體的lua函數消耗;
最後發現是引用的fgui對象沒有寫local,導致在Lua中始終保留fgui對象的引用,將會導致其無法被釋放;
因爲涉及到具體代碼,就不放出來了,這裏簡述下原理:
在大部分Lua插件中,都會存在類似的機制:爲了防止lua訪問C#某個對象時,該對象可能已經被C#層的GC回收掉。所以會在C#層維護一個Cache來引用那些被Lua訪問過的C#層對象,防止該對象被GC。
然而在Lua中始終保留某個C#層對象的引用,將會導致其無法被釋放,當這樣的引用越來越多,就會導致C#層的內存泄漏。

由於代碼已經被修復了,所以舉個例子怎麼用lua profiler查看這樣類似的情況,這個時候用到了lua profiler的Destroy null values統計;
這裏是lua代碼

function main()
    cube = GameObject.CreatePrimitive(PrimitiveType.Cube)
  ......

然後切換場景,是用工具檢測得到被引用對象爲:Cube,如圖:
在這裏插入圖片描述
具體引用鏈爲:
在這裏插入圖片描述
在切換場景時,雖然場景中已經沒有了Cube對象,但對象池中還有,導致仍然有引用而無法GC。這個時候Cube對象是一個作爲UnityEngine.Object爲空,而作爲System.Object不爲空的對象,原因就是Lua對其的引用不爲空,會導致泄漏。解決方案也較爲簡單,將Cube變量申明爲local局部變量,解除引用即可;

接下來說明一些常見的Lua變量沒有被LuaGC掉的情況

1.Lua對象是全局變量,直接放在_G中
例如:
button = GameObject.Find("LoginButton")
應對方法:
禁止定義全局變量,給現有的全局變量前加載local聲明。可以使用一些Lua靜態語法檢查的手段,如Luacheck來檢查。
2.Lua對象被一些全局的Table引用
我們每個UI面板都對應MVC結構,用了面向對象的概念。其中view在面板關閉時會直接置空,但Ctrl和Model都不會,它們都放在一個全局的管理類(Table)。當Model中持有了面板上的對象時,會出現對象銷燬了,但Model中的變量不爲空的情況。
例如:

-- login 對象放在全局持有的UI對象管理器中
-- UI面板使用mvc結構,在UI銷燬時,login的view字段會被賦值爲空,而ctrl,model不會。
login.model.button = GameObject.Find("LoginButton")

應對方法:
將持有C#對象的變量,定義在會賦值爲空的對象中,可以將示例中的代碼改爲:

login.view.button = GameObject.Find(“LoginButton”)
3.Lua對象的function字段被賦值給了C#的事件/委託
比如UI控件的按鈕點擊事件。在LuaGC時,發現C#對象對其有引用,GC不掉。導致Lua中的對象通過Tolua引用住了C#對象,而C#對象又通過ToLua引用Lua對象。
例如:

--UGUI的Button組件提供了onClick事件
login.view.loginButton = GameObject:Find("LoginButton"):GetComponent("UntiyEngine.UI.Button")
login.view.onLoginButtonClicked = function()
-- 處理loginButton點擊後的邏輯
end
login.view.loginButton.onClick:AddListener(login.view.onLoginButtonClicked)

應對方法:
(1)對於每一個提供給Lua註冊事件/委託的C#類,都繼承一個IClear接口,該接口內實現清理事件/委託。
(2)在MonoBehavior的OnDestroy函數內,調用IClear的接口。但要注意的是,這並不能保證所有的組件都是清理完畢,因爲deactvie狀態的組件,是不會觸發OnDestroy的。因此需要手動的調用清理。
(3)提供一個清理GameObject Lua事件/委託的接口,該接口會找到GameObject上所有繼承於IClear接口的類,執行清理操作。需要手動清理的GameObject都需要調用該函數

void ClearGameObject(UnityEngine.GameObject target)
{
    if(target == null) return;
    var list = target.GetComponentsInChildren<IClear>(true);
    foreach(var component in list)
    {
        component.Clear();
    }
}

(4)提供一個新的Destroy函數全局替換Unity原生的銷燬GameObject接口。該函數在做真正銷燬前,通過(3)清理所有註冊的事件/委託。

除此以外,我們還可以自己寫一些工具幫助查找問題,

例如:
1.查看是否有引用已經Destroy的對象:Unity重寫了UnityEngine.Object類的 Equals方法,如果已經被destroyed的Object equals null 返回true,可以對ToLua的objectsBackMap進行遍歷,非空且Equals null的對象,即爲已經Destroy的對象。可以將該類對象收集到一個列表中,通過Unity的編輯器代碼列出
2.查看Lua內存工具
可以從Lua的Registry或者_G開始往下遞歸查找,找到所有爲null userdata的對象(null userdata,在ToLua方案中表示是一個C#對象,並且Equals null)。並且可以反向列出該對象的引用鏈,直到Registry或_G爲止。這樣就可以詳細的定位是哪個Lua對象造成了問題。具體工具的寫法可以參考:https://github.com/yaukeywang/LuaMemorySnapshotDump
一些unity引擎細節優化技巧

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章