unity tolua跨语言对象引用原理和内存泄漏问题分析
原创作者游蓝海,转载请注明出处。
对于c#和lua这两种语言,都有自己的垃圾回收机制(gc),并且垃圾回收算法都是用的标记清扫方式,也就是说不管对象间的引用有多复杂,只要没有被对象根结点直接或间接引用,都是可以被当做垃圾清理掉。
还有一种简单粗暴的内存管理方式,是c++智能指针的引用计数。这种方式的回收效率最高,只要对象没有被引用就可以立马销毁。而标记清扫法则需要在合适的时机,遍历所有对象来检查垃圾。
但是引用计数有一个致命的缺点,就是循环引用。当两个对象相互引用时,两者的引用计数都会变成1,也就是构成了一个环,如果没有第三者来打破这个环,那么这两个对象就一直存在内存中了,变成了内存泄漏。
本文也是围绕循环引用,来探讨所引发的内存泄漏问题。要理解循环引用问题,我们要先弄清楚c#和lua相互引用的机制。
c#和lua是两个完全不一样的语言,任何一方都没法直接引用另一方的对象。
当对象从c#传递到lua的时候,tolua会将c#对象放到一个数组中,然后将数组索引作为c#对象的标识,并构造一个lua userdata对象记录下这个索引。这个userdata就是c#对象在lua中的代理,每当lua中访问到这个userdata,所有的操作都会经过其元方法(wrap函数)转发到c#,然后通过索引去对象数组中查找到关联的c#对象,然后调用该对象的方法。
反过来也一样,c#端要引用lua对象,也会把lua对象放到一个lua数组中,然后把数组索引交给c#,构造一个c#端的lua代理对象(如LuaTable,LuaFunction)。
简而言之,跨语言引用的对象都是强引用。
如果一个lua对象引用了一个c#对象,比如一个按钮,然后又将自己的方法注册给了这个按钮,以此来监听按钮的点击事件,并在点击事件中做一些对按钮状态改变的操作。下面写上大致调用代码:
self.button = gameObject:GetComponent(typeof(Button))
self.button.onClick:AddListener(function()
self.isClicked = true
end)
这个时候,循环引用就产生了。self直接引用了button,button通过onClick方法引用了一个lua匿名函数,匿名函数通过闭包变量(upvalue)引用了self。
self -> button -> onClick -> LuaFunction -> self
对引用步骤分解一下:
如果self对象不再使用后,不做任何成员变量清理工作,那么这个环上所有的对象都会是内存泄漏。更严重一点,如果self上还记录了其他对象,那么内存泄漏将会更严重。
也就是说,所有被循环链上的结点直接或间接引用的对象,都将发生内存泄漏。
上面这个案例算是一个普遍现象,在实际使用过程中,可能会有更多意想不到的情况。比如可能出现问题的环节:
local button = xxx
button.onClick:AddListener(function()
button.enabled = false
end)
button -> function -> button
对于循环引用,只要能定位到问题,对环上任一环节进行手动断开,就可以解除掉了。比如,当self不再使用后,手动将button属性设置为nil,或者将button的onClick事件监听者清空。
循环引用防不胜防,在前期写代码的时候就要重视起来。为了安全起见,有几个建议:
更安全的方式是,借助一些技巧来监测内存泄漏