unity内存泄漏分析实践

PerfDog更新初探:

最近发现PerfDog更新神速,再来探秘一波.
首先看看说明:
详细更新说明
总结一下,
1.大体是完善了应用的中文版,之前的一些英文提示对一些英语不熟的同学不太友好;
2.增加了一些大厂最新机型的适配。
3.增强了web平台的兼容性,现在在移动端也有很好的适配了,以前的话移动端打开web平台会出现很多适配方面的问题;需要注意这里的兼容是官网和社区的兼容哦,也就是web后台的浏览器兼容
来看一下对比,左面更新后,右面更新前,以前明显是自适应,现在做了移动端适配;
unity内存泄漏分析实践_第1张图片
不过遗憾的是现在只可以在移动端注册和逛社区,还不能打开报告展示的界面,不过基本完全也可以满足需求啦;

接下来分享下以前的一份测试内存泄漏经历

内存泄漏分析

背景:
手机性能还不错,综合评分可以算是高端机,一般手游开高特效都没问题;
unity内存泄漏分析实践_第2张图片
本次测试为跑新手指引,游戏架构采用的是重度Xlua,几乎所有逻辑都是lua编写,UI采用FGUI;
使用perfdog查看了一下整体数据,流畅度一般,但是内存有问题。
unity内存泄漏分析实践_第3张图片
测试时常是30分钟,考虑到内存一直在上升,有很大可能内存泄漏;
于是继续查看游戏里运行的详细数据,这里可以使用工具unity Profiler,UWA,UPA等等,我这里使用的是UWA;
发现留存堆内存果然持续上升,发生内存泄漏;
unity内存泄漏分析实践_第4张图片
继续分析,查看代码消耗的内存;
从性能堆栈中可以发现,其分配主要为FGUI所致
unity内存泄漏分析实践_第5张图片
锁定了FGUI,接下来分析具体函数,这个函数是FGUI的主要函数之一,它的逐步升高,也说明UI的数量在不断加大,
unity内存泄漏分析实践_第6张图片
确定了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

你可能感兴趣的:(性能,游戏,优化)