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

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

起因

上半年项目开始使用UWA GOT Online进行性能分析检测。在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来检查。

情况二: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)清理所有注册的事件/委托。


验证手段

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


这是侑虎科技第659篇文章,感谢作者唐崇供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

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