前言
在移动游戏发展到今天,热更技术已经有很多种渠道了,一种是使用lua,一种是c#更新dll,还有最新出来的TypeScript的热更新。这次我就简单的聊些使用率最多的lua热更新中的交互,也希望能说出一些交互上可以优化的东西。
知识大纲
- 函数
- 值类型
- 引用类型
- struct类型
- string类型
- lua和c的交互调用栈
- IntPtr类型
初始化
获取全局表
在lua初始化的时候会初始化一个luaEnv对象,他会去c里拿到一个LUA_REGISTRYINDEX这是一个registry的表的索引,这个表是c可以自由使用,但是lua代码不能访问他。
注册lua状态机
使用luaL_newstate创建了一个新的lua状态机(可以理解为lua虚拟机),主要用来为每一个lua线程创建独立的函数栈和线程栈,以及lua所需要的内存管理,字符串管理,gc管理。创建两个主要的数据结构lua_state和global_State。
- lua_State:主线程栈结构
- global_State:全局状态机,维护全局字符串,内存管理函数,gc等信息
加载lua标准库
使用luaL_openlibs用来加载lua的标准库到指定的lua状态机。
注册一个用来lua和c#交互的对象
通过创建一个ObjectTranslator对象来做lua和c#的对象交互,传入一个luaEnv对象,和一个lua状态机的指针。创建了一个弱value的一个table放在lua中。注册lua对象回收中__gc的回调方法。使用lua_pushlightuserdata创建一个自己管理的xlua标志位。注册所有被此应用所用的所有程序集到assemblies中
注册xlua全局表
translator.OpenLib(rawL);
import_type这个方法为lua中获取全局c#方法的回调函数
注册一个lua异常捕获
LuaAPI.lua_atpanic(rawL, StaticLuaCallbacks.Panic);使用这个方法注册一个在c#中去处置lua程序崩溃的方法
注册一个lua的print方法
LuaAPI.lua_pushstdcallcfunction(rawL, StaticLuaCallbacks.Print);LuaAPI.xlua_setglobal(rawL, "print")
注册一个print的方法注册到global全局表中的print
初始化Init脚本
注册一个全局的CS表,设置CS的元方法。设置xlua的元方法。
获取_G全局表
LuaAPI.xlua_getglobal(rawL, "_G");translator.Get(rawL, -1, out _G);获取_G
注册类
通过Wrap中的__Register的方法用来注册当前这个类的实例化函数,静态函数,属性,静态属性,实例化类的方法
注册函数
通过BeginObjectRegister在栈顶注册一个class.FullName名字的metatable表。给他塞入一个xlua的标志,覆盖两个关于“gc”和“tostring”的元方法。创建三个table,用来存放类的函数,设置属性方法,获取属性方法。调用RegisterFunc把所需要的函数方法注册进去。最后在结束的时候替换掉“index”和“newIndex”这两个元方法
注册静态函数
通过BeginClassRegister中把当前类的静态函数注册到table中,然后通过SetCSTable把当前类名的table放在cstable下,然后替换元方法__call为c#的函数,最后创建两个table用来放置静态属性;还是通过RegisterFunc把静态属性的获取和设置添加进去。
加载lua脚本
DoString
通过xluaL_loadbuffer编译lua脚本,通过lua_setfenv设置当前lua编译脚本为传进来的table的upvalue值,然后通过Lua_pcall执行编译之后的脚本,放回栈顶的对象。
Lua和c#交互
c#调用lua方法
在DoString之后把DoString所执行的脚本设置到一个luatable对象里面,然后通过这个luatable对象去lua栈里拿到luatable所在的table。在tryGetGetFuncByType发现我们要转换的类型不是我们定义的那些基础类型,所以通过GetObject去获取到一个转换委托ObjectCast这个委托函数里,然后通过需要转换到类型调用不同的转换函数,函数就是通过调用CreateDelegateBridge来创建一个action委托的,委托里包含了lua方法的索引。当函数调用的时候就是调用委托里的PCall来调用到lua中的方法。因为在tryGetGetFuncByType没有注册,所以使用了反射去创建当前类型的对象。
c#获取lua值类型
也是在tryGetGetFuncByType里把int ,bool,float这的值类型通过从lua栈里push出number,boolean,integer的值通过类型转换变换出来。
c#获取lua的table类型
当把table定义成一个list对象的时候,它也和调用函数一样回去判断有没有在tryGetGetFuncByType里定义了这个list类型的ObjectCast,如果没有也是通过反射在genCaster里去创建这个对象,把lua栈内的值取出来。
c#获取lua的string
当需要在lua中使用到c#的string时,把string转成byte[]传到lua中。
当把lua中的string传到c#的时候,也是把string转成byte[]接收过来,转成string
lua调用函数
在init的时候生成的时候会设置import_type为CS的index方法中的查找到c#里的ImportType去根据className去translator.FindType找到当前这个className到类型,然后通过GetTypeId去找放回类型的唯一id,如果这个类型没有缓存,那就会去生成一个当前类型名字的metatable,然后通过DelayWrapLoader中注册的loader去加载生成的wrap类的Register生成的当前所需要的方法的metatable,给lua调用,然后把类型的id传到交互栈中。当调用这个方法的时候就可以通过在RegisterFunc的时候push进去的lua_pushstdcallcfunction来掉到这个函数了。
lua调用值类型
当lua需要设置值类型的时候,同样会定义一个属性赋值方法,通过函数调用的方法调用到这个方法,然后通过lua_tointeger去栈里拿到这个方法一起传过来的number类型的值,然后赋值过去。
当lua需要设置值的时候,还是定义一个属性获取的方法,通过函数调用先通过Get方法从userdata中获取到这个需要赋值的对象,然后通过UnPack来把这个需要赋值的属性取出来,然后修改掉对应的属性;最后通过push把属性塞到lua栈中
lua调用struct类型
当lua需要获取一个struct类型的值时,因为lua中没有struct这样的结构类型所以会把struct包成一个object传到lua栈中当userdata存下来。
当lua需要设置一个struct类型的值时,获取到栈内的userdata然后存成ObjectCast然后转成struct,取lua栈内第二个参数来设置struct的值
但是在xlua中vector和color这两种struct会做一次处理,会把里面的struct的结构内容给拆分出来,通过基础的值类型进行传递。
lua调用引用对象
通过addObject把创建出来的object存在objecttranslator中,返回一个存放的index,然后把这个index传到xlua_pushcsobj里去创建一个userdata并且把这个对象的方法,属性都传进去做metatable属性。然后就可以获取到这个userdata赋值给lua。
lua和c#相互引用对象的生命周期
通过上面知道在c#中需要被lua所调用到的引用类型对象都需要在objecttranslator中存起来,然后把所生成的index传到lua的userdata中去,然后给这个userdata设置了一个元表,修改了gc这个元方法,所以当lua的userdata不在被引用的时候就会触发lua的gc,然后调用到gc这个元方法,调用到ObjectTranslator中的collectObject然后保存列表中清理掉。
在c#获取到lua的对象需要创建一个object的对象,然后把lua中的reference通过luaL_ref获取到保存下来,然后当这个对象不在被引用的时候,调用到c#的dispose函数,然后在里面去LuaAPI.lua_unref释放掉这个lua对象的引用。
总结
通过上面的数据交互,我们可以知道,在lua和c#的交互,我们需要注意到引用类型的交互,因为他们会分别在lua端和c#端创建一个objcet和table(userdata)对象,而这两种对象都会产生gc压力,还有一个就是struct对象也是需要注意的,虽然在xlua中vextor会拆成基础值类型传递,但是大部分我们自定义的struct还是会被包装成一个object传递的,这里还产生了一个装箱操作,所以消耗会更大。