游戏开发:Unity中Lua造成的堆内存泄露问题
1.起因上半年项目开始使用UWA GOT Online进行性能分析检测。在Lua项的检查中,引用已经被Destroyed的Unity Object数量一直在上升,由此判断,项目中Lua的使用存在操造成C#堆内存泄漏的问题。
2.问题分析与应对
项目采用的热更新方案是ToLua,ToLua给C#对象分配ID存在一个字典里(objectsBackMap),Lua层通过id访问对应的对象。
当Unity的Object被销毁时,并没有机制会通知到lua。此时,如果引用该对象的lua变量没有通过LuaGC掉(LuaGC会通知ToLua的字典清理对应数据),则这个已经被Destroy的对象就一直被引用住了。项目中的Lua变量没有被LuaGC掉的情况有以下几种情况:
情况一:Lua对象是全局变量,直接放在_G中。
举例:
button = GameObject.Find("LoginButton")应对方法:
禁止定义全局变量,给现有的全局变量前加载local声明。可以使用一些Lua静态语法检查的手段,如luacheck(https://github.com/mpeterv/luacheck)来检查。
情况二: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")情况三: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)清理所有注册的事件/委托。
3.验证手段
做完以上修改后,Lua引用已经Destroy对象导致的堆内存泄露问题基本上修复完毕,项目会定期跑UWA Online的Lua测试进行监控。
不过UWA工具毕竟是付费的。它会显示并统计已经Destroy对象的数量,而并没有列出具体哪个lua文件,哪行代码,哪个lua对象造成了问题。因此,还得有自己的工具来验证和定位问题。
(1)查看是否有引用已经Destroy对象。
Unity重写了UnityEngine.Object类的 Equals方法,如果已经被destroyed的Object equals null 返回true。可以对ToLua的objectsBackMap进行遍历,非空且Equals null的对象,即为已经Destroy的对象。可以将该类对象收集到一个列表中,通过Uniyt的编辑器代码列出。
(2)查看Lua内存工具。
可以从Lua的Registry或者_G开始往下递归查找,找到所有为null userdata的对象(null userdata,在ToLua方案中表示是一个C#对象,并且Equals null)。并且可以反向列出该对象的引用链,直到Registy或_G为止。这样就可以详细的定位是哪个lua对象造成了问题。具体工具的写法可以参:https://github.com/yaukeywang/Lspan class="invisible">uaMemorySnapshotDump
页:
[1]