unity tolua跨语言对象引用原理和内存泄漏问题分析

unity tolua跨语言对象引用原理和内存泄漏问题分析

原创作者游蓝海,转载请注明出处。

垃圾回收概述

对于c#和lua这两种语言,都有自己的垃圾回收机制(gc),并且垃圾回收算法都是用的标记清扫方式,也就是说不管对象间的引用有多复杂,只要没有被对象根结点直接或间接引用,都是可以被当做垃圾清理掉。

还有一种简单粗暴的内存管理方式,是c++智能指针的引用计数。这种方式的回收效率最高,只要对象没有被引用就可以立马销毁。而标记清扫法则需要在合适的时机,遍历所有对象来检查垃圾。

但是引用计数有一个致命的缺点,就是循环引用。当两个对象相互引用时,两者的引用计数都会变成1,也就是构成了一个环,如果没有第三者来打破这个环,那么这两个对象就一直存在内存中了,变成了内存泄漏。

本文也是围绕循环引用,来探讨所引发的内存泄漏问题。要理解循环引用问题,我们要先弄清楚c#和lua相互引用的机制。

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对button的引用是强引用,只要self不被lua回收,button就无法被c#回收
  • button对self也是强引用(通过onClick和匿名函数间接构成的),只要button不被c#回收,self就无法被lua回收

如果self对象不再使用后,不做任何成员变量清理工作,那么这个环上所有的对象都会是内存泄漏。更严重一点,如果self上还记录了其他对象,那么内存泄漏将会更严重。

也就是说,所有被循环链上的结点直接或间接引用的对象,都将发生内存泄漏。

上面这个案例算是一个普遍现象,在实际使用过程中,可能会有更多意想不到的情况。比如可能出现问题的环节:

  • 自定义的c#组件,通过事件的方式来通知lua。类似于上文提到的button模式
  • 自定义的c#组件,引用了一个lua端对象,这个lua对象中记录有一些c#对象
  • lua对象的成员引用了c#对象,而c#对象的成员引用了该lua对象
  • lua端向c#注册事件的时候,又在闭包中引用了c#对象
local button = xxx
button.onClick:AddListener(function()
   button.enabled = false
end)

button -> function -> button

手动打破引用环

对于循环引用,只要能定位到问题,对环上任一环节进行手动断开,就可以解除掉了。比如,当self不再使用后,手动将button属性设置为nil,或者将button的onClick事件监听者清空。

循环引用防不胜防,在前期写代码的时候就要重视起来。为了安全起见,有几个建议:

  • c#端自定义组件,如果有lua代理对象类型的成员,在OnDestroy的时候,将相应的成员设置为null。其实,将所有对象类型的成员都设置为null是个万无一失的方式,可以避免"连坐"
  • lua端对象在不使用的时候,将c#对象成员变量都设置为nil。如果成员变量中某个对象中也包含有c#对象,应该将该成员也设置为nil。

更安全的方式是,借助一些技巧来监测内存泄漏

  • 查询tolua创建的c#对象数组,判断哪些对象还在内存中。但是,不好分析是哪里引用的
  • 结合上一条,遍历lua的全局命名空间(_G)和lua注册表,检查c#对象被哪个lua变量在引用。如果被闭包引用就比较麻烦,要查所有函数的闭包变量
  • 建议在测试模式下,所有lua端对象在创建的时候,加入到一个弱表中。只要监测弱表中存在的对象是否发生泄漏,就能解决大部分问题

你可能感兴趣的:(Unity,Lua)