游戏开发:Unity中Lua造成的堆内存泄露问题

Posted 侑虎科技

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了游戏开发:Unity中Lua造成的堆内存泄露问题相关的知识,希望对你有一定的参考价值。


作者知乎:https://www.zhihu.com/people/ho1dthedoor,同时作者也是参与者,UWA欢迎更多开发朋友加入,这个舞台有你更精彩!



起因

上半年项目开始使用进行性能分析检测。在Lua项的检查中,引用已经被Destroyed的Unity Object,以致数量一直在上升,由此判断,项目中Lua的使用存在造成C#堆内存泄漏的问题。


问题分析与应对

项目采用的热更新方案是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点击后的逻辑endlogin.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)清理所有注册的事件/委托。


验证手段

做完以上修改后,Lua引用已经Destroy对象导致的堆内存泄露问题基本上修复完毕,项目会定期跑UWA  GOT Online的Lua测试进行监控


UWA会显示并统计已经Destroy对象的数量,而并没有列出具体哪个Lua文件,哪行代码,哪个Lua对象造成了问题。因此,还得有自己的工具来验证和定位问题。


(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)。并且可以反向列出该对象的引用链,直到Registy或_G为止。这样就可以详细的定位是哪个Lua对象造成了问题。具体工具的写法可以参考:https://github.com/yaukeywang/LuaMemorySnapshotDump




也欢迎大家来积极参与,简称“US”,代表你和我,代表UWA和开发者在一起!




近期精彩回顾

游戏开发基础

以上是关于游戏开发:Unity中Lua造成的堆内存泄露问题的主要内容,如果未能解决你的问题,请参考以下文章

Mono源码阅读-GC造成内存泄露问题

为啥lua语言中使用全局变量就会造成内存泄漏

为啥lua语言中使用全局变量就会造成内存泄漏呢??

Lua与Unity的内存优化技术

Unity游戏开发 | 浅谈Lua和C#中的闭包

Unity开发日记--Lua开发游戏UI界面