lua(tolua)与C#交互以及泄漏的整理与总结

前言

lua与C#交互通信原理

lua调用C#

调用无返回值函数(lua访问image的SetNativeSize)

调用返回C#对象的函数(lua访问image的mainTexture)

参考一个调用场景

C#调用lua

通过Require\Dofile调用lua以及通过DoString执行DoString

通过lua虚拟机对象获取对应的对象实例完成调用

Tolua中泄漏

1.table作为key。

2.C#持有lua对象使用完毕不执行释放接口。

简单了解一下lua的GC

总结



前言

最近在看lua泄漏的问题,接着就暴露出自己的一些问题,对于lua的认识更多的是停留在语法使用上,而对于lua如何产生泄漏,如何检查,如何优化,一直都没有一个清晰的认识,甚至对于lua与其他语言的交互也是没有什么认识。问题的本质是没有系统的了解过lua,也没有一定的知识储备。那本着一劳永逸的犯懒精神,集中时间和精力,去了解这门语言,之后再把自己的理解写成博客,似乎是一个性价比超高的选择。

lua与C#交互通信原理

Lua和C语言通信的主要方法是一个无所不在的虚拟栈。几乎所有的API的调用都会操作这个栈上的值。所有的数据交换,无论是Lua到C语言或C语言到Lua都通过这个栈来完成。此外,还可以用这个栈来保存一些中间结果。站可以解决Lua和C语言之间的两大差异,第一种差异是Lua使用垃圾收集,而C语言要求显示地释放内存;第二种是Lua使用动态类型,而C语言使用静态类型,同时,虚拟栈可以很好的处理静态类型与动态类型的相互转换。

如下图,unity C#先是生成tolua C#(warp文件),然后通过这些自动生成的tolua C#和tolua C交互,tolua C再借助lua栈与lua交互。

图二 C#与lua调用简图

lua调用C#

其主要逻辑就是两点,一个是lua内部通过持有translatorID,来确定是哪个C#对象,另一个是通过调用实例table所设置的metatable的接口来确定是哪个C#接口。通过warp.cs接口向lua注册相关逻辑,注册后的结构如下图所示。

当调用发生时,实例对象将其所持有的translatorId通过其metatable调用,通过lua栈,最终调用C#端注册的warp文件。

调用无返回值函数(lua访问image的SetNativeSize)

以下图中的对Image进行注册的warp文件为例,当lua端调用实例img(table)的SetNativeSize接口时,按照[方法索引,参数,参数数量]的顺序将数据压进lua栈(栈底是方法索引,栈顶是参数数量),此时的参数就是实例img(table)中的translatorId,当调用到C# Warp中的SetNativeSize方法时,会先检查参数数量(CheckArgsCount),接着通过进栈的translatorId,去ObjectTranslator中查找对应的image对象。

 

调用返回C#对象的函数(lua访问image的mainTexture)

再来看一看带返回值的情况,以下图中的对Image进行注册的warp文件为例,当lua端访问实例img(table)的mainTexture属性时,按照[方法索引,参数,参数数量]的顺序将数据压进lua栈(栈底是方法索引,栈顶是参数数量),此时的参数就是实例img(table)中的translatorId,当调用到C# Warp中的访问属性的方法时,先获得Image实例,然后获得mainTexture属性,然后进行压栈处理。

通过下图的调用顺序和代码可以很清楚的看到,压栈的操作顺序:先获取lua内存中的类型索引reference,然后将mainTexture对象加进ObjectTranslator中获取对应索引index。最后,调用tolua_pushnewudata接口,reference作为lua实例的原表的索引key,index用作lua实例的translatorId。

参考一个调用场景

接下来,以常见的写法gameobj.transform.position = pos进行分析,看lua层写下这一行代码,发生了什么。因为短短一行代码,却发生了非常非常多的事情,为了更直观一点,我们把这行代码调用过的关键luaApi以及ToLua相关的关键步骤列出来(以ToLua+cstolua导出为准,gameobj是GameObject类型,pos是Vector3):

第一步:.transform.position

UnityEngine_GameObjectWrap.get_transform    lua想从gameobj拿到transform,对应gameobj.transform
LuaDLL.luanet_rawnetobj         把lua中的gameobj变成c#可以辨认的id
ObjectTranslator.TryGetValue    用这个id,从ObjectTranslator中获取c#的gameobject对象
gameobject.transform            准备这么多,这里终于真正执行c#获取gameobject.transform了

ObjectTranslator.AddObject      给transform分配一个id,这个id会在lua中用来代表这个transform,
                                transform要保存到ObjectTranslator供未来查找
LuaDLL.luanet_newudata          在lua分配一个userdata,把id存进去,用来表示即将返回给lua的transform
LuaDLL.lua_setmetatable         给这个userdata附上metatable,让你可以transform.position这样使用它
LuaDLL.lua_pushvalue            返回transform,后面做些收尾
LuaDLL.lua_rawseti
LuaDLL.lua_remove

第二步:= pos

TransformWrap.set_position              lua想把pos设置到transform.position
LuaDLL.luanet_rawnetobj                 把lua中的transform变成c#可以辨认的id
ObjectTranslator.TryGetValue            用这个id,从ObjectTranslator中获取c#的transform对象
LuaDLL.tolua_getfloat3                  从lua中拿到Vector3的3个float值返回给c#
lua_getfield + lua_tonumber 3次         拿xyz的值,退栈
lua_pop
transform.position = new Vector3(x,y,z) 准备了这么多,终于执行transform.position = pos赋值了

C#调用lua

C#调用lua基本上就是3种:通过Require、DoFile;通过DoString执行DoString;通过lua虚拟机对象获取对应的对象实例,对对象实例进行操作。

通过Require\Dofile调用lua以及通过DoString执行DoString

通过lua虚拟机对象获取对应的对象实例完成调用

简单描述一下获取LuaFunction实例的步骤流程,核心逻辑是先将”testFunction”转换到fullPath,然后调用lua_getglobal接口来获取对象,接着就是通过toluaL_ref来获取reference索引,reference和luaState构成了LuaFunction对象的逻辑。

LuaFunction的Call逻辑主要是三步:BeginPCall、Pcall、EndPCall。先通过tolua_beginpcall传递refrence索引,然后通过lua_pcall传递参数和起始栈的id,这样就可以在lua层执行lua的方法,最后再根据起始栈id恢复当前lua栈到执行前的层级。

Tolua中泄漏

先明确在lua中的泄漏是什么,这种GC语言的泄漏大都是其引用关系没有正常释放,存在一个或者多个地方持有但不使用对象,这种情况下的对象不能被正常释放,且这种对象的数量不断的上升,我理解的泄漏是这种情景。

根据上述章节的内容去寻找会发生泄漏的场景,一般是2个:

1.table作为key。

重复性执行的逻辑中需要一直创建table,而这些新创建的table又在存储table中作为索引key,当这个创建的table不再使用时,存储table还持有新建的table,泄漏就发生了。

这种场景下的泄漏处理的方法其实也很简单,将存储table设置为弱引用table,这样lua在GC的时候就不会判定存储table还在持有已经不用的新建table。

具体细节可以参考这篇博文:Step By Step(Lua弱引用table)

2.C#持有lua对象使用完毕不执行释放接口。

在做业务逻辑开发的时候,大都会有这种场景:C#需要持有lua对象。比如tolua的框架下会定制一些UI工具类,通过设置luaFunction来完成对lua的事件回调。

在组件的使用周期完毕之后如果不对luaFunction进行释放,是有可能引起内存泄漏的。比如不断的注册lua的匿名回调函数,注册的函数会让C#持有refrence,但因为一直没有释放所以也就不会执行luaDll.toluaL_unref接口,那么lua就认为该lua函数对象是被使用的,又因为匿名函数在执行的时候,每注册一次都会生成一个新的函数对象,然后随着注册次数的增加,函数对象的数量也就一直增加,泄漏就这样发生了。

因此,要是想要规避这种情景下的泄漏,从两个地方着手:

1.C#工具使用周期结束执行luaFunction or luaTable的Dispose接口。

2.注册函数尽可能少的使用匿名函数

3.数学运算(Vector3/Quaternion)

严格的来说,这个并不算泄漏,因为最后那些临时对象会被GC掉,放到这里是因为,不加处理的数学运算,且调用频繁的话,会大大增加内存的增长率,使得GC操作变得相对频繁起来,所以,在某种程度上,它已经算是一种泄漏了。具体细节和优化,可以参考下面这篇文章

好Lua+Unity,让性能飞起来——Lua与C#交互篇_我动了谁的奶酪-CSDN博客_unity 纯lua

简单了解一下lua的GC

了解lua所采用的GC原理,对处理lua内存泄漏的问题,以及检查有很大的帮助。

Lua采用的是Mark-sweep算法:每次GC的时候,对所有对象进行一次扫描,如果该对象不存在引用,则被回收,反之则保存。在Lua5.0及其更早的版本中,Lua的GC是一次性不可被打断的过程,使用的Mark算法是双色标记算法(Two color mark),这样系统中对象的非黑即白,要么被引用,要么不被引用,这会带来一个问题:在GC的过程中如果新加入对象,这时候新加入的对象无论怎么设置都会带来问题,如果设置为白色,则如果处于回收阶段,则该对象会在没有遍历其关联对象的情况下被回收;如果标记为黑色,那么没有被扫描就被标记为不可回收,是不正确的。在Lua5.1后,Lua都采用分布回收以及三色增量标记清除算法(Tri-color incremental mark and sweep)

其基本的原理伪代码,参考书中原文为:

每个新创建的对象颜色设置为白色

//初始化阶段

遍历root节点中引用的对象,从白色置为灰色,并且放入到灰色节点列表中

//标记阶段

while(灰色链表中还有未扫描的元素):

从中取出一个对象,将其置为黑色

遍历这个对象关联的其他所有对象:

if 为白色

标记为灰色,加入到灰色链表中(insert to the head)

//回收阶段

遍历所有对象:

if 为白色,

没有被引用的对象,执行回收

else

重新塞入到对象链表中,等待下一轮GC

这是一些关于一些GC的文章:

深入探究Lua的GC算法

常见的GC回收算法及其含义c

总结

Lua与C#的交互主要是依托于lua栈,在相互调用的过程中,双方各自对到对方一个id,一个能索引到对应对象的id,然后对方根据这个id通过lua栈向对方传递操作行为,有真实对象所在的领域去执行具体的操作。大致流程如下图:

引用以及参考:

lua程序设计(第二版)[巴西]莱鲁 [2008-1]

Unity项目常见Lua解决方案性能比较_UWA—简单优化,优化简单!-CSDN博客

lua内存泄漏检测工具原理及设计 - 知乎

云风的 BLOG: 一个 Lua 内存泄露检查工具

Lua内存泄漏应对方法_xocoder's coding life-CSDN博客_lua内存泄漏

Lua 弱引用table - 天龙久传说 - 博客园

弱引用是什么?

【ToLua】C#和Lua的交互细节 - 知乎

如何编译各平台使用的库-以编译tolua为例_linxinfa的专栏-CSDN博客

【Unity游戏开发】tolua之wrap文件的原理与使用 - 马三小伙儿 - 博客园

用好Lua+Unity,让性能飞起来——Lua与C#交互篇_我动了谁的奶酪-CSDN博客_unity 纯lua

【Lua知识整理】——Lua栈_suhuaiqiang_janlay的专栏-CSDN博客_lua 栈

……

  • 5
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值