调用无返回值函数(lua访问image的SetNativeSize)
调用返回C#对象的函数(lua访问image的mainTexture)
通过Require\Dofile调用lua以及通过DoString执行DoString
前言
最近在看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与C#的交互主要是依托于lua栈,在相互调用的过程中,双方各自对到对方一个id,一个能索引到对应对象的id,然后对方根据这个id通过lua栈向对方传递操作行为,有真实对象所在的领域去执行具体的操作。大致流程如下图:
引用以及参考:
lua程序设计(第二版)[巴西]莱鲁 [2008-1]
Unity项目常见Lua解决方案性能比较_UWA—简单优化,优化简单!-CSDN博客
Lua内存泄漏应对方法_xocoder's coding life-CSDN博客_lua内存泄漏
如何编译各平台使用的库-以编译tolua为例_linxinfa的专栏-CSDN博客
【Unity游戏开发】tolua之wrap文件的原理与使用 - 马三小伙儿 - 博客园
用好Lua+Unity,让性能飞起来——Lua与C#交互篇_我动了谁的奶酪-CSDN博客_unity 纯lua
【Lua知识整理】——Lua栈_suhuaiqiang_janlay的专栏-CSDN博客_lua 栈
……