type
status
date
slug
summary
tags
category
icon
password
在Unity中,使用Lua作为热更手段的时候,会有一个Lua虚拟机在跑。
那么Lua和c#交互的时候,怎么保证lua引用了一个c#对象,c#对象为什么不会被释放,反过来同理。然后是什么情况下才会释放,以及有没有内存泄漏的风险。
接下来就让我们来一起探讨下。
本篇讲解主要以XLua为基础,并且阅读本文需要一定的基础
文章原创,转载需标明出处
📝 Lua和C#的GC协同
基础介绍
Lua的GC使用的是增量标记-清除算法。这种算法分为两个阶段:标记阶段和清除阶段。在标记阶段,Lua的GC会从root(根)出发,标记所有可达的对象。
C#虽然官方使用的分代垃圾收集算法。Unity中目前不是跑的.Net,使用的是IL2cpp的计数,即把c#编译后的IL代码,转换为c++代码,c++本身具有跨平台性。Unity官方在IL2cpp中实现了一套自己的垃圾收集器,它使用了一种称为"Boehm-Demers-Weiser"的收集算法。这种算法是一种保守的标记-清除垃圾收集算法。
总的来说都是基于标记清除的算法,接下来就介绍下一方持有另一方的对象的时候会发生什么情况。
Lua持有C#对象
lua调用c#对象的原理简要说明
lua中想要持有c#的引用类型,都会使用userdata类型。
那lua如何访问c#中的字段和方法的呢,答案就是
metatable
。XLua的实现中,会给每个被访问的c#类型,创建一个元表,元表里面会有记录所有的成员变量和属性的get、set,以及所有的方法。总所周知,开发时都会使用生成wrap文件的方式来增加lua访问c#的效率,而上述说到的访问手段都会在
wrap
文件中体现(这里的讲解不考虑没有wrap
走反射的情况)。例如以下。可以看到以上生成的wrap代码中有个
Utils.BeginObjectRegister
的方法调用。这个方法其实就是为这个类型创建好一个table,它里面有__gc和__tostring还有这个类型的set、get、method等调用的方法。这个table的作用就是用于给lua访问的c#对象时,作为创建出来的c#对象的userdata
的metatable
,这个table一个类型只有一个,一个c#对象对应一个userdata
(重复访问还是这一个),这样就解释了为什么lua中能访问的了c#的对象。看一头雾水没关系,这个可以看下这张图:看到这里的人肯定发现了这里有个__gc方法,那么这个__gc是做什么用的呢,这就要先介绍下lua的userdata了,以下引用chagpt的回答来简单介绍下。
在Lua中,userdata用于表示C语言中的任意数据。它允许Lua代码和C语言代码交互和共享数据。Lua中的userdata有两种类型:完全userdata(full userdata)和轻量userdata(light userdata)。
- 完全userdata(full userdata): 它是Lua中的一种复杂类型,可以带有元表,因此可以定义一些行为(比如算术运算、比较运算等)。完全userdata在Lua中表示为一个对象,其内容(C数据)存储在Lua的内存管理系统中,Lua的垃圾收集器会自动处理其生命周期。
- 轻量userdata(light userdata): 它是一种简单的C指针,本质上就是一个
void*
。轻量userdata不可以带有元表,不能定义行为,也不受Lua的垃圾收集器管理。轻量userdata主要用于表示一些简单的资源,如文件句柄、线程ID等
c#对象使用的就是full userdata,也就是这个userdata可以受到lua的gc管理,可以理解为userdata是lua中的一个可以垃圾回收的引用类型,当没有能够直接或者间接访问到这个userdata的时候(标记清除法),就被lua的gc回收掉,并且会调用元表中的__gc方法。
c#保持引用
lua访问c#对象时,XLua会把c#对象放入一个池子里,保持住这份引用,防止被c#的GC给释放掉,并且生成一个index数据绑定到userdata。然后后面lua调用c#的时候,先获取userdata的中的index值,然后在c#中通过这个index去池子中拿到这个对象,然后就可以调用了。
c#部分代码如下:
ObjectTranslator.cs
xlua.c源码如下:
所以可以看到当lua调用c#时,c#侧会缓存这个对象,并返回一个索引id,然后lua虚拟机生成一个userdata绑定这个索引id,并且给这个对象绑定上wrap里生成好的metatable。
这样当lua侧的这个userdata因为没有直接或者间接引用,而被lua虚拟机的GC释放掉的时候,会调用__gc方法,然后会从c#侧的对象池子中移除这个userdata对应的c#对象。
__gc对应的方法如下:
总的来说,lua引用c#对象能够保持引用和释放,主要是通过c#侧的对象池子来实现的,然后池子会返回一个索引id,userdata其实绑定的就是这个索引id。然后userdata释放的时候通过__gc调用绑定好的c#侧的释放对象的方法。
C#持有Lua引用类型数据
讲完lua持有c#对象时调用和GC的原理之后,再来讲讲c#持有lua的table和function,原理大体来说和上述讲解了lua持有c#差不多。
XLua在c#侧封装了
LuaTable
和LuaFunction
对象来作为调用lua的桥梁,这些C#调用lua的对象都继承了LuaBase
,一个lua引用类型数据对应一个LuaBase
。当c#需要调用lua的table或者function的时候,会再第一次生成这个对象,然后之后调用的时候直接用userdata转成对应的LuaBase
对象。C#调用lua的生成LuaTable和LuaFunction对象如下:
其中比较关键是
luaL_ref
函数,对应的C#函数如下:luaL_ref
和 luaL_unref
是 Lua C API 中的函数,它们用于在 Lua 的注册表(registry)中创建和删除引用。LuaIndexes.LUA_REGISTRYINDEX是一个特殊的索引值,它被用于访问Lua的注册表,是在XLua初始化的时候从lua中读取的。如果c#需要调用lua的table和function的对象,会先调用
luaL_ref
在lua的注册表持有住这个引用,并且返回一个索引值记在C#的LuaBase
对象中,避免被Lua虚拟机的GC释放掉。然后在这个
LuaBase
的Dispose函数中调用luaL_unref
释放掉注册表中持有的对象,如何没有调用Dispose
,当LuaBase
被GC掉的时候,会调用析构函数,最终也会调到Dispose
,释放掉lua注册表的lua引用数据。C#代码如下:
综上所述,c#调用lua其实也是会在被调用方里用一个全局的表来注册持有住对象,防止被GC释放,然后如果c#侧的
LuaBase
被释放的时候,也会从lua侧的注册表中删除对应的引用数据。Lua和C#相互引用(包含间接)
说到相互引用的情况,可以看一个开发中很常见的例子,例如一个lua table中持有一个Unity的Button对象,然后给这个Button添加table中的一个绑定了这个table的function,也就是一个带了self的闭包函数,会在C#生成一个
DelegateBridge
对象实现对这个闭包函数的桥接调用,如图下所示。
这是一个典型的具有相互的引用的示例,这种情况下,如果不做一些接触相互引用的操作,是不会被释放的。因为lua的table持有了Button的引用,Button又持有
DelegateBridge
的引用,然后DelegateBridge
又持有lua闭包函数的引用,闭包函数里带了table的引用,这样就形成了循环引用。如何打破这种循环引用,让GC能够回收这些对象呢。
其实很简单,就是c#对象接触对
DelegateBridge
的引用,或者lua的table对象解除的对C#的Button对象的引用。管理技巧
对于解除类似上述的lua和c#相互引用的情况,有很多运用情景,以下就谈谈作者的看法。
- lua中管理UGUI的组件,并且能够方便的解除相互引用,如下面示例代码所示,其中名为ui的lua table的组件是从C#的ObjectBinding中序列化数据中读取的。
这里示例情景是从Lua中移除c#类的引用,当然也可以从lua中移除c#的引用,主要还是看哪边去处理更加方便。如果lua中的table类对c#引用很多很分散,并且c#只是一两处引用了这个table,那么也是可以从c#处打断这个相互引用。
例如之前笔者由于不懂这块概念,做的以lua脚本为核心的技能系统中,就出现了相互引用的情况,其实可以完全在框架设计上就能处理掉这些问题,比如销毁技能的时候在c#层打破引用,但是由于经验不足,导致后期处理带了大量的工作。所以了解这个原理是非常重要的。
🤗 总结
综上所述,了解这个c#和lua相互引用下的GC原理是非常重要的,因为它涉及到程序的内存管理和优化。如果不理解 Lua 和 C# 中垃圾回收的原理,可能会导致内存泄漏和性能问题。相互引用的情况也需小心处理,否则会影响程序的内存使用情况。因此,程序员需要了解这些原理和技巧,以便在编写代码时正确处理对象引用和内存管理。
- Author:有理fan
- URL:https://unifan.top/article/game_gc_3
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!