找回密码
 立即注册
查看: 539|回复: 0

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

[复制链接]
发表于 2020-12-30 10:25 | 显示全部楼层 |阅读模式
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中。
举例:
  1. button = GameObject.Find("LoginButton")
复制代码
应对方法:
禁止定义全局变量,给现有的全局变量前加载local声明。可以使用一些Lua静态语法检查的手段,如luacheck(https://github.com/mpeterv/luacheck)来检查。


情况二:Lua对象被一些全局的Table引用。我们每个UI面板都对应MVC结构,用了面向对象的概念。其中view在面板关闭时会直接置空,但ctrl和model都不会,它们都放在一个全局的管理类(table)。当model中持有了面板上的对象时,会出现对象销毁了,但model中的变量不为空的情况。
举例:
  1. -- login 对象放在全局持有的UI对象管理器中
  2. -- UI面板使用mvc结构,在UI销毁时,login的view字段会被赋值为空,而ctrl,model不会。
  3. login.model.button = GameObject.Find("LoginButton")
复制代码
应对方法:
将持有C#对象的变量,定义在会赋值为空的对象中,可以将示例中的代码改为
  1. login.view.button = GameObject.Find("LoginButton")
复制代码
情况三:Lua对象的function字段被赋值给了C#的事件/委托。比如UI控件的按钮点击事件。在LuaGC时,发现C#对象对其有引用,GC不掉。导致Lua中的对象通过Tolua引用住了C#对象,而C#对象又通过ToLUa引用Lua对象。
举例:
  1. --UGUI的Button组件提供了onClick事件
  2. login.view.loginButton = GameObject:Find("LoginButton"):GetComponent("UntiyEngine.UI.Button")
  3. login.view.onLoginButtonClicked = function()
  4. -- 处理loginButton点击后的逻辑
  5. end
  6. login.view.loginButton.onClick:AddListener(login.view.onLoginButtonClicked)
复制代码
应对方法:
(1)对于每一个提供给Lua注册事件/委托的C#类,都继承一个IClear接口,该接口内实现清理事件/委托。
(2)在MonoBehavior的OnDestroy函数内,调用IClear的接口。但要注意的是,这并不能保证所有的组件都是清理完毕,因为deactvie状态的组件,是不会触发OnDestroy的。因此需要手动的调用清理。
(3)提供一个清理GameObject Lua事件/委托的接口,该接口会找到GameObject上所有继承于IClear接口的类,执行清理操作。需要手动清理的GameObject都需要调用该函数。
  1. void ClearGameObject(UnityEngine.GameObject target)
  2. {
  3.     if(target == null) return;
  4.     var list = target.GetComponentsInChildren<IClear>(true);
  5.     foreach(var component in list)
  6.     {
  7.         component.Clear();
  8.     }
  9. }
复制代码
(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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-20 08:55 , Processed in 0.088940 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表