ToLua是很多项目都使用的lua框架,为了更好地使用,需要对框架有比较清晰的认识。
简介
- 所有的Lua框架,都是为了解决热更的问题。但无论代码框架怎么设计,都避免不了需要和CSharp侧交互。而一旦出现交互,就需要了解整体的结构和运作流程,才能更好地使用。
- ToLua的整体结构,主要是以表的形式存在,大致如下:
表类型 | 说明 | 内容 |
---|---|---|
_G | 全局表,可直接操作 | 可自定义设置,如: _G[“TestPanel”] = { } |
-10000 (LUA_REGISTRYINDEX) |
注册表,只能通过ref和unref操作 | tolua在这里定义了多类表,常用的有: 4(LUA_RIDX_UBOX):存放lua引用C#对象的userdata 5(LUA_RIDX_FIXEDMAP):存放被C#引用的对象,如table、function 7(LUA_RIDX_PACKVEC3):指向lua的Vector3.New方法 26(LUA_RIDX_LOADED):存放C#类对应的lua表 另外,注册表还存放了C#的类结构绑定到lua侧的信息,如GameObject、Component等类在C#的各种属性、方法等。 |
- 通过上表,可以对ToLua的结构有一个基本的概念。接下来将对整个框架进行分析,从而对ToLua能有一个更加全面的认识。
初始化
创建LuaState
- Lua状态机创建方法主要实现如下
public LuaState()
{
...
InitTypeTraits();
InitStackTraits();
L = LuaNewState();
LuaException.Init(L);
stateMap.Add(L, this);
OpenToLuaLibs();
ToLua.OpenLibs(L);
OpenBaseLibs();
...
}
- InitTypeTraits()
- 注册常用类型检查函数。
- InitStackTraits()
- 注册常用类型数据入栈和栈上数据弹出解析函数 。
- OpenToLuaLibs()
- 打开指定的标准库,注册方法到全局表,包括package、preload、loadlib、seeall、module、require等。
- 创建int64和uint64的table,注册到registry表中的LUA_RIDX_INT64和LUA_RIDX_UINT64中。
- 注册其他到全局表,如:Mathf、tolua等。
- ToLua.OpenLibs(L)
- AddLuaLoader方法,初始化loader,将lua侧的package_loaders[2]绑定为CSharp的ToLua.Loader方法。lua侧执行require时,会遍历所有loader(即 package_loaders)尝试加载,因此package_loaders[2],即CSharp的ToLua.Loader会加载对应的lua脚本,所以可以通过修改ToLua.Loader的流程来自定义加载。
- 往全局表中注册print、dofile、loadfile方法。
- 往tolua表中注册isnull(判断是否为Lua侧的nil或者CSharp侧null)、typeof、tolstring(CSharp侧的数据转lua字符串)、toarray。
- OpenBaseLibs()
- 注册System、LuaInterface、UnityEngine模块,其中List和Dictionary没有注册创建方法。
- 创建Layer表到全局表中,将LayerMask的层级添加到Layer表中。
- 初始化反射相关,加载mscorlib和UnityEngine程序集并缓存,注册反射相关方法到tolua表中,包括findtype、loadassembly、getmethod、getconstructor、gettypemethod、getfield、getproperty、createinstance。
- 注册CSharp反射相关Wrap的初始化方法,key值为tolua.reflection。
- 创建完成后,lua侧大致为:
_G(即 -10002) = {
Mathf = {
NextPowerOfTwo = tolua.c 中的 mathf_nextpoweroftwo 方法,
ClosestPowerOfTwo = tolua.c 中的 mathf_closestpoweroftwo 方法,
IsPowerOfTwo = tolua.c 中的 mathf_ispoweroftwo 方法,
GammaToLinearSpace = tolua.c 中的 mathf_gammatolinearspace 方法,
LinearToGammaSpace = tolua.c 中的 mathf_lineartogammaspace 方法,
Normalize = tolua.c 中的 mathf_normalize 方法,
NULL = NULL
}
tolua = {
gettime = tolua.c 中的 tolua_gettime 方法,
typename = tolua.c 中的 tolua_bnd_type 方法,
setpeer = tolua.c 中的 tolua_bnd_setpeer 方法,
getpeer = tolua.c 中的 tolua_bnd_getpeer 方法,
getfunction = tolua.c 中的 tolua_bnd_getfunction 方法,
initset = tolua.c 中的 tolua_initsettable 方法,
initget = tolua.c 中的 tolua_initgettable 方法,
int64 = tolua.c 中的 tolua_newint64 方法,
uint64 = tolua.c 中的 tolua_newuint64 方法,
initset = tolua.c 中的 tolua_initsettable 方法,
gettag = &gettag ,
settag = &settag ,
version = 版本号信息(如:1.0.7),
isnull = CSharp侧的 ToLua.IsNull 方法(用于lua侧判断CSharp对象是否为空),
typeof = CSharp侧的 ToLua.GetClassType 方法,
tolstring = CSharp侧的 ToLua.BufferToString 方法,
toarray = CSharp侧的 ToLua.TableToArray 方法,
NULL = NULL
}
print = CSharp侧的 ToLua.Print 方法,
dofile = CSharp侧的 ToLua.DoFile 方法,
loadfile = CSharp侧的 ToLua.LoadFile 方法,
...
}
初始化相关库
- lua侧需要使用的相关库函数,要进行初始化
- luaopen_pb
- luaopen_struct
- luaopen_lpeg
- cjson(可选)
- …
绑定
- CSharp的相关方法要进行绑定,即注册到lua侧
protected virtual void Bind()
{
LuaBinder.Bind(luaState);
DelegateFactory.Init();
LuaCoroutine.Register(luaState, this);
}
public static class LuaBinder
{
...
public static void Bind(LuaState L)
{
...
L.BeginModule("UnityEngine");
UnityEngine_ComponentWrap.Register(L);
UnityEngine_TransformWrap.Register(L);
...
L.BeginModule("Events");
L.RegFunction("UnityAction", UnityEngine_Events_UnityAction);
L.EndModule();
...
L.EndModule();
L.BeginPreLoad();
L.AddPreLoad("UnityEngine.MeshRenderer", LuaOpen_UnityEngine_MeshRenderer, typeof(UnityEngine.MeshRenderer));
L.AddPreLoad("UnityEngine.BoxCollider", LuaOpen_UnityEngine_BoxCollider, typeof(UnityEngine.BoxCollider));
...
L.EndPreLoad();
...
}
...
- LuaBinder.Bind 主要的方法有:
- L.BeginModule(…)
- 调用lua的tolua_beginmodule方法,将CSharp中的namespace转化为table表示。如:namespace为A.B,则会创建table A存到全局表中,table B存到table A中,B.name = “A.B”。
- L.EndModule()
- 调用lua的tolua_endmodule方法,将栈上的table弹出(beginmodule执行完后栈上会留下当前table),结束module创建,并更新stringbuffer。
- XXXWrap.Register(L)
- XXXWrap:CSharp侧的类生成的Wrap文件,用于将CSharp类注册到lua中。生成格式为 命名空间_类名 + Wrap,如:命名空间为A.B,类名为C,则生成的Wrap为 A_B_CWrap。
- L.BeginClass(…)
- 如果基类没有注册过,则在Lua侧创建一个table,放到registry表(索引为LUA_REGISTRYINDEX)中。
- 在CSharp建立table和基类的映射关系(metaMap、typeMap、genericSet)。
- 调用lua的tolua_beginclass方法,创建一个table(tb_loaded),放到registry表中的已加载表(索引为LUA_RIDX_LOADED)中,key值为命名空间+类名,如:A.B.C。
- 如果当前类没有创建过table,则创建一个新的table(tb_type)。
- 将基类table设置为当前类table的metatable。
- 注册元方法__call为class_new_event,主要实现获取元表里的New方法,并将参数传入后调起。
- 设置__index和__newindex元方法。
- CSharp注册__gc元方法为CSharp的Collect方法。
- 在CSharp侧建立table和当前类的映射关系。
- L.EndClass()
- 调用lua的tolua_endclass方法,将当前类的table(tb_type)设置为LUA_RIDX_LOADED中table(tb_loaded)的metatable,再将LUA_RIDX_LOADED中table(tb_loaded)设置到当前module的table中,key值为类名,即module中不会直接创建当前类的table,而是持有当前类table的引用。因此获取数据时,则是通过查找LUA_RIDX_LOADED中table(tb_loaded),触发当前类table(tb_type)的__index方法获取数据的。
- L.RegFunction(…)
- 将CSharp类方法转成LuaCSFunction,再获取方法指针。
- 调用lua的tolua_function,将方法注册到当前类的table中,key值为方法名。
- L.RegVar(…)
- 将CSharp类的public变量和属性转成get和set的LuaCSFunction,再获取方法指针。
- 调用lua的tolua_variable,创建get和set的table,存入当前类的table中,key值为lightuserdata的&gettag和&settag。
- 把get和set的方法存入各自的table中,key值为属性名。
- L.BeginPreLoad()
- 获取全局表中的package,找到表里的preload,将preload表入栈。
- L.AddPreLoad(…)
- CSharp侧将类型type加入preLoadMap字典中,值为对应的LuaCSFunction。
- 将LuaCSFunction压入preload表中,key值为命名空间 + 类名,如:A.B。
- 调用lua的tolua_addpreload方法,将命名空间作为module在全局表上创建对应的table。
- 在CSharp侧将命名空间加入moduleSet,保证相同命名空间只创建一次table。
- L.EndPreLoad()
- 将package和preload出栈。实际上并没有直接注册lua表,而是等到调用对应的LuaCSFunstion时,再通过BeginPreModule和EndPreModule进行注册。
- L.BeginEnum(…)
- 调用lua的tolua_beginenum,创建一个table(tb_loaded)加入loaded中。
- 创建一个table(tb_enum),放入registry表中。
- 设置table(tb_enum)的name、__index、__newindex、__gc。
- 在CSharp侧建立table(tb_enum)和当前类的映射关系。
- 完成BeginEnum后,将Enum的每个值作为变量注册到table(tb_enum)中,设置get方法。
- 所有Enum变量注册完后,注册IntToEnum方法到table(tb_enum)中,可将int转成lua侧的Enum表示。
- L.EndEnum()
- 将当前Enum的table(tb_enum)设置为LUA_RIDX_LOADED中table(tb_loaded)的metatable,再将LUA_RIDX_LOADED中table(tb_loaded)设置到当前module的table中,key值为Enum名。
- 完成EndEnum后,设置CSharp的ypeTraits
.Check和StackTraits .Push。
- L.BeginModule(…)
- 以UnityEngine.Component和UnityEngine.Space为例,进行绑定后,Lua侧的大致表结构如下
_G = {
UnityEngine =
table = {
__index = tolua.c中的module_index_event,
.name = "UnityEngine",
Component = LUA_REGISTRYINDEX表中LUA_RIDX_LOADED表的UnityEngine.Component,
Space = LUA_REGISTRYINDEX表中LUA_RIDX_LOADED表的UnityEngine.Space,
...
}
metatable = table
...
}
-10000(即 LUA_REGISTRYINDEX) = {
...
26(即 LUA_RIDX_LOADED) {
UnityEngine.Component
table = {},
metatable = ref_Component
UnityEngine.Space
table = {},
metatable = ref_SpaceEnum
},
...
ref_Component(分配的id,UnityEngine.Component)
table = {
.name = "UnityEngine.Component",
ref = ref_Component分配的id,
&tag = 1,
GetComponent = CSharp的UnityEngine_ComponentWrap.GetComponent,
TryGetComponent = CSharp的UnityEngine_ComponentWrap.TryGetComponent,
...
New = CSharp的UnityEngine_ComponentWrap._CreateUnityEngine_Component,
&gettag = {
transform = CSharp的UnityEngine_ComponentWrap.get_transform,
gameObject = CSharp的UnityEngine_ComponentWrap.get_gameObject,
tag = CSharp的UnityEngine_ComponentWrap.get_tag
},
&settag = {
tag = CSharp的UnityEngine_ComponentWrap.set_tag
},
__call = tolua.c中的class_new_event,
__index = tolua.c中的class_index_event,
__newindex = tolua.c中的class_newindex_event,
__gc = CSharp的LuaState.Collect,
__eq = CSharp的UnityEngine_ComponentWrap.op_Equality,
__tostring = CSharp的ToLua.op_ToString
},
metatable = ref_UObject
ref_UObject(分配的id,UnityEngine.Object)
table = {
...
},
metatable = {
...
}
ref_SpaceEnum(分配的id,UnityEngine.Space)
table = {
.name = "Space"
__index = tolua.c中的enum_index_event,
__newindex = tolua.c中的enum_newindex_event,
__gc = CSharp的LuaState.Collect,
},
...
}
...
- lua 侧检查某个 C# 类、方法、属性、字段是否有生成 Wrap ,可以通过以下方式:
local function TypeWrapped(typeName)
local wrap = false
local wrapType = _G
local result = pcall(function()
for key in string.gmatch(typeName, "[^%.]+") do
wrapType = wrapType[key]
end
wrap = wrapType ~= nil and wrapType ~= _G
end)
if not wrap then
wrapType = nil
end
return wrap, wrapType
end
local function PropertyOrFieldWrapped(wrapType, memberName)
local wrap = false
if wrapType ~= nil then
local gettag = wrapType[tolua.gettag]
if gettag ~= nil and gettag[memberName] ~= nil then
wrap = true
end
end
return wrap
end
local function MethodWrapped(wrapType, methodName)
local wrap = false
if wrapType ~= nil and wrapType[methodName] ~= nil then
wrap = true
end
return wrap
end
- DelegateFactory 对委托相关的类型进行管理
public class DelegateFactory
{
public static void Init()
{
Register();
}
public static void Register()
{
dict.Clear();
dict.Add(typeof(System.Action), factory.System_Action);
dict.Add(typeof(UnityEngine.Events.UnityAction), factory.UnityEngine_Events_UnityAction);
...
DelegateTraits<System.Action>.Init(factory.System_Action);
DelegateTraits<UnityEngine.Events.UnityAction>.Init(factory.UnityEngine_Events_UnityAction);
...
TypeTraits<System.Action>.Init(factory.Check_System_Action);
TypeTraits<UnityEngine.Events.UnityAction>.Init(factory.Check_UnityEngine_Events_UnityAction);
...
StackTraits<System.Action>.Push = factory.Push_System_Action;
StackTraits<UnityEngine.Events.UnityAction>.Push = factory.Push_UnityEngine_Events_UnityAction;
...
}
}
-
DelegateFactory.Init
- 完成委托类型的注册
- dict.Add(…)
- 在CSharp侧,将委托类型和委托创建方法作为key-value加入字典中
- DelegateTraits
.Init(…) - 在CSharp侧,使用泛型记录委托创建方法,可直接调用
- TypeTraits
.Init(…) - 在CSharp侧,使用泛型将委托的类型检查方法添加到对应类型的Check,Check方法只返回true或者false,不返回对象,多用于重载变量类型检查
- StackTraits
.Push = … - 在CSharp侧,使用泛型将委托的入栈方法添加到对应类型的Push
- StackTraits也有Check,但返回的一个对应的T
- dict.Add(…)
- 委托创建方法,继承LuaDelegate,流程为
- 先创建LuaDelegate对象,将LuaFunction设置到LuaDelegate的func中
- 设置LuaDelegate的method为CSharp的Call或CallWithSelf
- 以LuaFunction的ref为key,将LuaDelegate以弱引用形式(WeakReference)缓存到delegateMap中
- 完成委托类型的注册
-
DelegateTraits
- Init:在CSharp侧,将各种委托类型的委托创建方法加入字典中
访问对象
Lua访问CSharp对象
- Lua侧获取一个CSharp侧对象,常用有几种方式
- CSharp侧将对象注册到Lua的对应table。
- 使用CSharp的静态方法,如:GameObject.Find 等。
- 使用对象的CSharp的new创建。
- …
- 无论使用那种方式,最终都是通过 ToLua.PushUserData 方法来建立映射关系。
public static void PushUserData(IntPtr L, object o, int reference)
{
int index;
ObjectTranslator translator = ObjectTranslator.Get(L);
// 检查CSharp是否已经对这个对象建立了索引
if (translator.Getudata(o, out index))
{
// 将索引对应的userdata压到Lua栈上
if (LuaDLL.tolua_pushudata(L, index))
{
return;
}
// 如果lua侧已经没有这个userdata,则移除CSharp侧的 ObjectTranslator.objects[index].obj,但此index还保持,等lua侧gc触发才能真正移除。
// 因为lua侧gc是延迟触发的,移除LUA_RIDX_UBOX表中的userdata后,在lua侧gc触发前可能同一个对象又重新关联到lua侧,需要分配一个新的index,避免旧的index触发CSharp侧GC方法错误移除正在使用的对象。
translator.Destroyudata(index);
}
// CSharp侧为object创建索引index_GameObject
index = translator.AddObject(o);
// 在Lua侧创建userdata,userdata的值设置为index_GameObject
LuaDLL.tolua_pushnewudata(L, reference, index);
}
- CSharp调用lua的tolua_pushnewudata创建userdata,主要流程为
- 创建一个userdata结构(userdata_GameObject),值为index_GameObject,类型为LUA_TUSERDATA。
- 将ref_GameObject设置为userdata_GameObject的metabtable。
- 将userdata_GameObject存到LUA_REGISTRYINDEX表中的LUA_RIDX_UBOX表,key值为index_GameObject,和userdata_GameObject的值一致。
- 保留userdata_GameObject在栈顶(tolua_pushudata方法也是从LUA_RIDX_UBOX表中查找userdata_GameObject并压入栈,即userdata_GameObject位于栈顶)。
- 此时,Lua栈顶放着userdata_GameObject,而我们要对此GameObject进行操作时,如获取GameObject的activeSelf,则
- 查找TestPanel表的GameObject_A,拿到userdata_GameObject。
- 调用userdata_GameObject.activeSelf,即查询metatable的__index。
- 找到CSharp的UnityEngine_GameObjectWrap.get_activeSelf方法并调用。
- ToLua.ToObject调用lua的tolua_rawnetobj方法,查询当前栈上的userdata的值,即CSharp侧的GameObject对象对应的index_GameObject。
- 从ObjectTranslator中获取对应的GameObject,拿到activeSelf值。
- 调用lua_pushboolean将activeSelf传到lua侧。
- 示例代码大致如下:
-- TestPanel.lua
local state = TestPanel.GameObject_A.activeSelf
// UnityEngine_GameObjectWrap.cs
...
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int get_activeSelf(IntPtr L)
{
object o = null;
try
{
o = ToLua.ToObject(L, 1);
UnityEngine.GameObject obj = (UnityEngine.GameObject)o;
bool ret = obj.activeSelf;
LuaDLL.lua_pushboolean(L, ret);
return 1;
}
catch(Exception e)
{
return LuaDLL.toluaL_exception(L, e, o, "attempt to index activeSelf on a nil value");
}
}
...
// ToLua.cs
...
public static object ToObject(IntPtr L, int stackPos)
{
int udata = LuaDLL.tolua_rawnetobj(L, stackPos);
if (udata != -1)
{
ObjectTranslator translator = ObjectTranslator.Get(L);
return translator.GetObject(udata);
}
return null;
}
...
- Lua侧此时的结构大致如下所示
_G = {
TestPanel = {
GameObject_A : userdata_GameObject(从 LUA_RIDX_UBOX 中获取)
...
}
}
-10000(即 LUA_REGISTRYINDEX) = {
...
4(即 LUA_RIDX_UBOX) = {
__mode = v (弱表,value为弱引用)
index_GameObject(CSharp侧的一个UnityEngine.GameObject,index为ObjectTranslator分配)= userdata_GameObject {
(头部 Udata*){
...
tt = LUA_TUSERDATA
metatable = LUA_REGISTRYINDEX.ref_GameObject
}
(用户自定义数据 user domain*){
index_GameObject
}
}
}
ref_GameObject(分配的id,UnityEngine.GameObject)
table = {
...
&gettag = {
...
activeSelf = CSharp的UnityEngine_GameObjectWrap.get_activeSelf,
...
},
},
metatable = ref_UObject
...
}
- 当我们的TestPanel不再使用时,TestPanel表会删除(TestPanel = nil),此时整个table会变成可回收,而表里的变量也同样会标记成可回收,即userdata_GameObject也触发了luaL_unref方法,移除了userdata_GameObject引用,此时userdata_GameObject只有在 LUA_RIDX_UBOX 中有引用,此时的结构如下:
Lua侧
_G = {
}
-10000(即 LUA_REGISTRYINDEX) = {
...
4(即 LUA_RIDX_UBOX) = {
index_GameObject(CSharp侧的一个UnityEngine.GameObject,index为ObjectTranslator分配)= userdata_GameObject {
(头部 Udata*){
...
tt = LUA_TUSERDATA
metatable = LUA_REGISTRYINDEX.ref_GameObject
}
(用户自定义数据 user domain*){
index_GameObject
}
}
...
}
...
}
CSharp侧
ObjectTranslator.objectsBackMap = {
{ go , index_GameObject },
...
}
ObjectTranslator.objects[index_GameObject] = go
- 由于 LUA_RIDX_UBOX 表为值弱表,所以当lua gc触发时,userdata_GameObject由于没有其他引用,所以会被回收,并调起 userdata_GameObject.metatable的.__gc (即 LUA_REGISTRYINDEX.ref_GameObject.__gc) 方法,从而调起CSharp的LuaState.Collect方法,删除 ObjectTranslator.objects 和 ObjectTranslator.objectsBackMap 的引用。
- CSharp侧的GC方法为:
// LuaState.cs
...
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
public static int Collect(IntPtr L)
{
int udata = LuaDLL.tolua_rawnetobj(L, 1);
if (udata != -1)
{
ObjectTranslator translator = GetTranslator(L);
translator.RemoveObject(udata);
}
return 0;
}
// ObjectTranslator.cs
...
public void RemoveObject(int udata)
{
//只有lua gc才能移除
object o = objects.Remove(udata);
Debug.LogFormat("lua gc RemoveObject, udata = {0}" , udata);
if (o != null)
{
if (!TypeChecker.IsValueType(o.GetType()))
{
RemoveObject(o, udata);
}
if (LogGC)
{
Debugger.Log("gc object {0}, id {1}", o, udata);
}
}
}
void RemoveObject(object o, int udata)
{
int index = -1;
if (objectsBackMap.TryGetValue(o, out index) && index == udata)
{
objectsBackMap.Remove(o);
}
}
- 可以看到,CSharp侧的GC流程为
- 调用lua侧的 tolua_rawnetobj 方法,获取对象对应的userdata对应的index。
- 移除CSharp侧 ObjectTranslator.objects 中的index。
- 如果 ObjectTranslator.objectsBackMap 中的index和要GC的index不一致,说明此对象已经绑定到新的index,不需要移除引用。
- 对于枚举对象,userdata 的 metatable 不为对应 Type 的 metatable,而是统一使用 System.Enum 对应的 metatable。
// ToLua.cs
...
public static void Push(IntPtr L, System.Enum e)
{
object obj = null;
int enumMetatable = LuaStatic.GetEnumObject(L, e, out obj);
PushUserData(L, obj, enumMetatable);
}
// LuaStatic.cs
...
public static int GetEnumObject(IntPtr L, System.Enum e, out object obj)
{
LuaState state = LuaState.Get(L);
obj = state.GetEnumObj(e);
return state.EnumMetatable;
}
// LuaState.cs
...
void OpenBaseLibs()
{
...
System_EnumWrap.Register(this);
...
EnumMetatable = metaMap[typeof(System.Enum)];
...
}
- 其中,枚举值 userdata 对象,经常需要获取其对应的值来进行逻辑处理,System_EnumWrap 中增加了 ToInt 方法,能将枚举对象转成 int 值。
local intValue = UnityEngine.Space.Self:ToInt()
- 为了释放已经销毁的UnityEngine.Object的引用关系,CSharp侧提供了一个 LuaState.StepCollect() 方法,放在Update中触发,可以检查已经销毁的对象,移除 ObjectTranslator.objectsBackMap 的已销毁对象,避免字典频繁扩容。
- 总的来说,lua侧对CSharp侧对象的引用,只有lua gc触发的时候,才能在CSharp侧对应释放掉引用。
CSharp访问Lua对象
- 项目中往往需要将部分逻辑放到CSharp侧编写,因此不可避免有时候会需要读取Lua侧的对象,以LuaTable表示,如:
- CSharp侧创建Lua的table(创建并注册数据给Lua侧使用)。
- Lua侧调用CSharp方法传入table。
- …
- 当我们要获取一个Lua侧的table时,通常为
stackPos = LuaDLL.abs_index(L, stackPos);
// 将栈上stackPos的lua table压到栈上
LuaDLL.lua_pushvalue(L, stackPos);
// 获取lua侧的table的引用
int reference = LuaDLL.toluaL_ref(L);
// CSharp侧获取对应的LuaTable
return LuaStatic.GetTable(L, reference);
- 其中,lua侧的 toluaL_ref 方法,先通过stackPos对应的值,查找 LUA_RIDX_FIXEDMAP 表中是否存在有这个key值,如果存在则返回对应的value,即table的reference。如果 LUA_RIDX_FIXEDMAP 表中不存在,则在 LUA_REGISTRYINDEX 中创建一个引用reference,将table作为key,reference作为value,存入 LUA_RIDX_FIXEDMAP 表,然后返回reference值,后续再获取就直接从 LUA_RIDX_FIXEDMAP 表中即可。
- 可以看出,lua侧并不会记录CSharp侧对table有多少引用,只保持一次引用,所以需要CSharp侧来管理引用情况。
// LuaState.cs
public LuaTable GetTable(int reference)
{
LuaTable table = TryGetLuaRef(reference) as LuaTable;
if (table == null)
{
table = new LuaTable(reference, this);
funcRefMap.Add(reference, new WeakReference(table));
}
RemoveFromGCList(reference);
return table;
}
- 在CSharp侧,LuaTable都在LuaState.funcRefMap中缓存,每次调用TryGetLuaRef,都会触发LuaBaseRef.AddRef,使LuaBaseRef.count的值+1,表示引用加1。此时的结构如下:
Lua侧
_G = {
}
-10000(即 LUA_REGISTRYINDEX) = {
...
5(即 LUA_RIDX_FIXEDMAP) = {
testtable = test_ref
}
test_ref = testtable (由lua侧的 luaL_ref 方法创建的映射)
...
}
CSharp侧
LuaState.funcRefMap = {
{ test_ref , new WeakReference(LuaTable) },
...
}
ObjectTranslator.objects[index_GameObject] = go
- 当CSharp侧不再使用此LuaTable,则会调用LuaTable.Dispose方法,释放对应的LuaTable。
// LuaBaseRef.cs
public virtual void Dispose()
{
--count;
if (count > 0)
{
return;
}
IsAlive = false;
Dispose(true);
}
...
public virtual void Dispose(bool disposeManagedResources)
{
if (!beDisposed)
{
beDisposed = true;
if (reference > 0 && luaState != null)
{
luaState.CollectRef(reference, name, !disposeManagedResources);
}
reference = -1;
luaState = null;
count = 0;
}
}
- 当调用Dispose时,会检查是否还有引用,count > 0,表示还有其他地方引用了这个对象。当没有引用了,则会调用luaState.CollectRef释放LuaTable,从LuaState.funcRefMap中移除,并调起lua侧的toluaL_unref移除引用。
- lua侧的toluaL_unref方法,通过获取 LUA_REGISTRYINDEX 表的 reference对应的table,将 LUA_RIDX_FIXEDMAP 表key值为table的value设为nil,即移除此key-value,然后再移除 LUA_REGISTRYINDEX 表对此table的引用。
- 然而,可以发现,当我们的LuaTable如果没有正确执行Dispose方法,则会出现count一直大于0的情况,则无法释放LuaTable,即便我们已经没有再使用它。因此,LuaBaseRef对此做了处理。
// LuaBaseRef.cs
~LuaBaseRef()
{
IsAlive = false;
Dispose(false);
}
- LuaBaseRef在析构函数中,也做了释放,将LuaTable(LuaBaseRef)加入LuaState.gcList中。在Update方法中,执行LuaState.Collect方法,释放CSharp侧的LuaTable和lua侧的对应table。
- 由于LuaTable在CSharp侧,是以弱引用存在于LuaState.funcRefMap中,所以当CSharp侧触发GC(Unity切换场景或主动执行System.GC.Collect方法等)时,由于CSharp侧对LuaTable没有直接引用持有,所以无论当前的引用count是多少,LuaTable都会被回收,进而触发LuaBaseRef的析构函数,实现了释放。
- 总的来说,LuaTable能释放的情况有以下情况
- LuaTable为局部变量,方法执行完成后就触发析构函数(Unity中才会触发局部变量的析构函数)释放(立即释放)。
- LuaTable为成员变量,手动执行了LuaTable.Dispose方法,LuaTable.count为0,luaState.CollectRef执行进行释放(立即释放)。
- LuaTable为MonoBehavior的成员变量,对应的GameObject销毁后,Mono对象没有其他引用,无论LuaTable.count为多少,都会触发LuaTable析构函数释放(延迟释放)。
- LuaTable为普通Class的成员变量,引用class的对象置为null后,无论LuaTable.count为多少,都会触发LuaTable析构函数释放(延迟释放)。
- LuaTable没有直接引用时,触发CSharp的GC,触发析构函数释放(立即释放)。
- 因此,为了保证能立即释放,有以下方式
- 不直接引用LuaTable,即保持所有LuaTable都为局部变量。
- 将LuaTable设为成员变量,则CSharp侧都通过此成员变量获取相同的table,而不再从lua侧重新检查获取,count只有1,只对此LuaTable执行Dispose。
- 不同对象从lua侧获取同一个table,都作为成员变量,对所有LuaTable都执行Dispose,保证count减少为0。
时序图
- Lua侧访问CSharp对象的时序图为
- CSharp访问Lua对象的时序图为
调用方法
Lua调用CSharp方法
- 初始化的时候,LuaBinder将CSharp的class对应创建了table存到了 LUA_REGISTRYINDEX 表中,而CSharp的class,对应的各种方法也已经进行了绑定,调用的时候,通过查找_G表,即可找到对应方法,以GameObject为例
_G = {
UnityEngine = {
GameObject = LUA_REGISTRYINDEX表中LUA_RIDX_LOADED表的UnityEngine.GameObject,
.name = "UnityEngine"
...
}
...
}
-10000(即 LUA_REGISTRYINDEX) = {
...
26(即 LUA_RIDX_LOADED) {
UnityEngine.GameObject
table = {},
metatable = ref_GameObject
UnityEngine.Space
table = {},
metatable = ref_SpaceEnum
},
...
ref_GameObject(分配的id,UnityEngine.GameObject)
table = {
...
Find = CSharp的UnityEngine_GameObjectWrap.Find,
},
metatable = ref_UObject
ref_UObject(分配的id,UnityEngine.Object)
table = {
...
},
metatable = {
...
}
...
}
- 当想要调用GameObject.Find时,在lua侧需要执行 UnityEngine.GameObject.Find(“test”),则最终会调用CSharp侧的UnityEngine_GameObjectWrap.Find,读取栈上的参数string,并调起UnityEngine.GameObject.Find,最后再把GameObject传回lua。
// UnityEngine_GameObjectWrap.cs
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int Find(IntPtr L)
{
try
{
ToLua.CheckArgsCount(L, 1);
string arg0 = ToLua.CheckString(L, 1);
UnityEngine.GameObject o = UnityEngine.GameObject.Find(arg0);
ToLua.PushSealed(L, o);
return 1;
}
catch (Exception e)
{
return LuaDLL.toluaL_exception(L, e);
}
}
...
CSharp调用Lua方法
- 和Lua对象一样,有时候也需要调用Lua侧的方法,主要是通过table获取,以LuaFunction表示,如:
- 直接通过方法名,找到对应表中的方法。
- 找到某个LuaTable,查找表中的方法。
- …
- 当我们要获取一个Lua侧的function时,通常为
// ToLua.cs
...
stackPos = LuaDLL.abs_index(L, stackPos);
// 将栈上stackPos的lua function压到栈上
LuaDLL.lua_pushvalue(L, stackPos);
// 获取lua侧的function的引用
int reference = LuaDLL.toluaL_ref(L);
// CSharp侧获取对应的LuaFunction
return LuaStatic.GetFunction(L, reference);
- 和LuaTable一样,function也会对应存入 LUA_RIDX_FIXEDMAP 表和 LUA_REGISTRYINDEX 表。销毁机制也和LuaTable一致。
委托
- Lua侧的业务,往往需要调用CSharp的逻辑,如加载某些资源。资源通过CSharp代码进行加载,加载完成后,就执行CSharp的回调。由于我们需要执行lua侧的后续逻辑,就需要设置lua侧的方法作为CSharp侧的回调,因此需要创建对应类型的委托。前面提到,DelegateFactory.Init 完成委托类型的初始化,其中 DelegateTraits 注册了各种类型的委托。通过调用 ToLua.CheckDelegate
即可创建对应委托方法,设置时的方法主要为:
// ToLua.cs
...
// 获取栈上的lua function
LuaFunction func = ToLua.ToLuaFunction(L, stackPos);
// 创建对应类型的静态方法委托
return DelegateTraits<T>.Create(func);
- 注意,所有的lua侧委托方法都是静态方法,如果方法中引用了自身table(self),即产生了闭包,则table释放的时候,需要将CSharp侧对此方法的引用置为null,table才能被lua gc回收。否则即便table已经在CSharp侧Dispose了,但由于委托方法还引用了table,而CSharp还引用着方法,导致table还是不能释放。
- lua方法对应委托创建时会存入 LuaState.delegateMap 中,同样以WeakReference(Delegate.Target)的形式存在。key值有两种情况
- 静态方法:key值为对应的LuaFunction的reference。
- 成员方法:key值为64位,高位32位为LuaFunction的reference,低位32位为LuaTable的reference。
- 可见,LuaTable和LuaFunction都会被LuaDelegate引用(LuaDelegate的变量),所以如果LuaDelegate还有引用,则LuaTable和LuaFunction也不能释放。而由于lua侧的委托方法都是静态方法,所以CSharp不会引用LuaTable,也避免了LuaTable无法释放引起的其他资源卸载问题。而当lua侧的function触发gc时,由于是静态方法,所以直接通过reference就能移除 LuaState.delegateMap 中的引用。
- 在场景加载完成后,由于CSharp侧的GC触发,LuaDelegate如果没有直接引用则会被释放,之后通过 LuaState.RefreshDelegateMap 方法,可以将delegateMap中已经释放的LuaDelegate移除,避免delegateMap频繁扩容。
重载
- CSharp中存在重载函数的情况,而Lua是弱语言类型,当我们在Lua侧调用一个CSharp方法时,如果此方法是重载的,则我们不能确定应该要调起哪一个对应的方法。因此在生成Wrap文件时,会对重载方法进行处理,通过检查参数个数和类型,来确定应该调用哪个方法。生成重载方法的主要流程为,获取class中所有同名方法,按一定规则进行排序,并确定需要进行类型判断的参数位置。
- 排序的规则如下:
- 无可选参数的方法排在前面。
- 成员方法非可选参数数量+1,参数数量不一样的,数量少的方法排在前面。参数数量一样的,最后一个非可选参数的类型不是object容器的方法排在前面。
- 成员方法总参数数量+1,数量小的方法排在前面。
- 第一个参数为基本类型(bool、byte、int等)的,排在前面,为object的排在后面。
- 从第二个参数开始,基本类型 > long > ulong > object。
- 确定需要进行类型判断的方法为:
- 参数数量不一样,不用进行类型判断。
- 参数数量一样的方法,逐一检查参数,相同的不用则进行类型判断,直到找到第一个不一样的,从这个参数起所有都需要进行类型判断检查。
- 前面提到,TypeTraits
.Init 初始化注册了检查类型的方法,通过 TypeTraits .Check 方法可以检查lua方法的参数类型,类型匹配则通过ToLua.ToObject读取对象,强转成对应的类型。 - 参数可以传委托方法,当我们没有重载或重载的参数数量不一样时,对应的委托类型参数则使用 ToLua.CheckDelegate
来创建。而当出现参数数量一致,且委托类型参数是需要检查类型的,则会通过ToLua.ToObject来获取对象。以System.Action类型为例,检查类型的方法和获取对象为:
// ToLua.cs
...
bool Check_System_Action(IntPtr L, int pos)
{
return TypeChecker.CheckDelegateType(typeof(System.Action), L, pos);
}
// TypeChecker.cs
...
static public bool CheckDelegateType(Type type, IntPtr L, int pos)
{
LuaTypes luaType = LuaDLL.lua_type(L, pos);
switch (luaType)
{
case LuaTypes.LUA_TNIL:
return true;
case LuaTypes.LUA_TUSERDATA:
int udata = LuaDLL.tolua_rawnetobj(L, pos);
if (udata != -1)
{
ObjectTranslator translator = ObjectTranslator.Get(L);
object obj = translator.GetObject(udata);
return obj == null ? true : type == obj.GetType();
}
return false;
default:
return false;
}
}
// ToLua.cs
...
public static object ToObject(IntPtr L, int stackPos)
{
int udata = LuaDLL.tolua_rawnetobj(L, stackPos);
if (udata != -1)
{
ObjectTranslator translator = ObjectTranslator.Get(L);
return translator.GetObject(udata);
}
return null;
}
- 可以看到,检查类型的方法只检查了userdata类型和nil,也就是说,System.Action类型的参数,必须是userdata类型才能匹配,即必须是Lua侧持有的CSharp侧对象。如果传入的参数为lua侧的方法时,则由于类型检查不匹配(LuaTypes.LUA_TFUNCTION)而无法调起。同样,ToLua.ToObject 方法也是直接通过 udata 获取 ObjectTranslator.objects 的对应对象,所以当出现这种情况时,lua侧的方法是不能正常调起的,这也是目前重载会遇到的问题。
- 总的来说,为了避免出现重载引起CSharp侧不能调起lua方法的问题,可以有以下方法:
- 避免使用重载函数。
- 增加新的重载函数,将所有原有的重载函数参数全组合到新的重载函数中,并将原来的重载函数逻辑整合到此新重载函数中,而原来旧的重载函数使用 [NoToLua] 特性,不生成Wrap,则在lua侧只有这个新的重载方法类型可以传入。
无GC传值
- 前面说到,Lua侧获取CSharp对象是通过userdata的形式,即获取对应的id,而CSharp侧将id和对象存入ObjectTranslator.objects中,由于 ObjectTranslator.objects 中存的对象类型为object,对于值类型的对象,使用object就会需要装拆箱的操作,还会产生gc,因此对于此类对象,则需要有无gc的传值方法,来提升效率。
- 对于大部分基本类型,如:bool、int,Lua和CSharp可以通过c的api进行交互,如:
- lua_pushboolean/lua_toboolean
- lua_pushinteger/lua_tointeger
- …
- 通过这些方法,将数据压到栈上,或从栈上读取,则实现了数据交互,而不会产生gc。而无gc传值也是基于这种方法,通过规定struct的数据类型和顺序,则可实现入栈和出栈的配套方法。尤其是对频繁调用的类型,如UnityEngine.Vector3,效率上会有很大的提升。
- 以Vector3为例,CSharp侧对Vector3创建了Push(将Vector3数据压入栈中)、Check(检查并创建Vector3对象)、To(获取Vector3对象)三个方法:
// ToLua.cs
...
public static void Push(IntPtr L, Vector3 v3)
{
LuaDLL.tolua_pushvec3(L, v3.x, v3.y, v3.z);
}
static public Vector3 CheckVector3(IntPtr L, int stackPos)
{
int type = LuaDLL.tolua_getvaluetype(L, stackPos);
if (type != LuaValueType.Vector3)
{
LuaDLL.luaL_typerror(L, stackPos, "Vector3", LuaValueTypeName.Get(type));
return Vector3.zero;
}
float x, y, z;
LuaDLL.tolua_getvec3(L, stackPos, out x, out y, out z);
return new Vector3(x, y, z);
}
public static Vector3 ToVector3(IntPtr L, int stackPos)
{
float x = 0, y = 0, z = 0;
LuaDLL.tolua_getvec3(L, stackPos, out x, out y, out z);
return new Vector3(x, y, z);
}
- LuaState.OpenBaseLuaLibs 方法,会调用lua侧的 tolua_openluavec3 方法,获取全局表里的 Vector3.New 方法,设置为 LUA_REGISTRYINDEX 表里的 LUA_RIDX_PACKVEC3,Vector3.Get 方法则设置为 LUA_REGISTRYINDEX 表里的 LUA_RIDX_UNPACKVEC3,即
_G = {
Vector3 = lua侧的Vector3.lua
...
}
-10000(即 LUA_REGISTRYINDEX) = {
...
"_LOADED" = {
UnityEngine.Vector3 = lua侧的Vector3.lua
}
7(即 LUA_RIDX_PACKVEC3) = Vector3.New
8(即 LUA_RIDX_UNPACKVEC3)= Vector3.Get
...
}
- lua侧的 tolua_getvec3 方法,会获取 LUA_RIDX_UNPACKVEC3 方法,调用 Vector3.Get 将lua侧的Vector3的x、y、z压入栈中。
- lua侧的 tolua_pushvec3 方法,会获取 LUA_RIDX_PACKVEC3 方法,将参数x、y、z传入,调用 Vector3.New 创建lua侧的Vector3。
- lua侧实现了Vector3对象,对UnityEngine.Vector3的一些常用计算方法在Vector3.lua中做,来避免频繁和CSharp交互。
泛型
- CSharp侧调用Lua侧方法时,LuaFunction提供了两种方法,可简化CSharp侧的调用方式,且不会产生GC。
- 无返回值:Call,支持9个参数以内(包括9个参数)的泛型调用。
- 一个返回值(泛型R1):Invoke,支持9个参数以内(包括9个参数)的泛型调用。
- Lua侧调用CSharp的泛型方法,此方法需要带泛型类型的参数,并且泛型类型需要限定为引用类型,否则工具方法不会生成Wrap代码,无法调用。
- 前面提到,LuaState.OpenBaseLibs中,List、Dictionary注册了Wrap,即可以在Lua侧调用对应的操作,但是没有注册创建的方法,所以无法在Lua侧创建此类对象。
- 如果没有提前生成Wrap代码,泛型方法可以通过反射方式,调用 Type.MakeGenericMethod 方法,创建泛型方法的泛型实例方法,再直接调起实例方法,即完成对泛型方法的调用。但是如果泛型类型为值类型,且CSharp侧没有调用过对应类型的泛型方法,则在 AOT 模式下,编译器不会提前编译出对应类型的方法,导致无法正常调用。可以通过增加强制编译方法,来保证对应类型代码会被编译。
private void Compile()
{
var o = new TestGeneric();
o.Test2<int>();
o.Test2<float>();
o.Test2<bool>();
...
o.Test3<object, int>();
o.Test3<object, float>();
o.Test3<object, bool>();
...
o.Test3<int, object>();
o.Test3<float, object>();
o.Test3<bool, object>();
}
反射
- 为了支持反射,CSharp侧提供了LuaReflection,通过 OpenLibs 方法注册反射相关方法。
- tolua.findtype:查找类名获取对应类型。
- tolua.loadassembly:加载指定名称的程序集。
- tolua.getmethod:获取类型对应的方法,封装成 LuaMethod,再转成userdata给lua侧,方便lua侧执行方法。
- tolua.getconstructor:获取类型对应的构造函数,封装成LuaConstructor,再转成userdata给lua侧。
- tolua.gettypemethod:获取类型对应的方法,带多参数。
- tolua.getfield:获取成员变量,封装成 LuaField,再转成userdata给lua侧,方便lua侧获取内容。
- tolua.getproperty:获取属性,封装成 LuaProperty,再转成userdata给lua侧,方便lua侧获取内容。
- tolua.createinstance:创建类型示例。
- tolua.reflection:初始化LuaMethod、LuaProperty、LuaField、LuaConstructor的Wrap文件注册,需要初始化后才能正常反射。
- 使用 tolua.findtype 方法,一般需要先添加 Assembly-CSharp 程序集。Assembly-CSharp 程序集没有在 LuaReflection 的构造函数中添加到list中,避免放在插件目录无法加载,需要可从lua代码 loadassembly。如果不清楚类型名,可在 CSharp 侧使用 Type.FullName 得到具体类名。
tolua.loadassembly("Assembly-CSharp")
tolua.findtype("XXXXX")
- 对于枚举对象,枚举对象的值需要用 GetHashCode 方法获取。反射获取流程如下
tolua.loadassembly("Assembly-CSharp")
local enumType = tolua.findtype('XXXEnum')
if enumType ~= nil and enumType.IsEnum then
-- 获取枚举
-- 方法一
local valueArray = System.Enum.GetValues(enumType)
for i = 0,valueArray.Length - 1 do
-- 枚举名
local enumKey = valueArray:GetValue(i):ToString()
-- 枚举值
local enumValue = valueArray:GetValue(i):GetHashCode()
end
-- 方法二
local nameArray = System.Enum.GetNames(enumType)
for i = 0,nameArray.Length - 1 do
-- 枚举名
local enumKey = nameArray[i]
-- 枚举值
local enumValue = System.Enum.Parse(enumType, enumKey):GetHashCode()
end
end
- 由于这里使用反射获取,会有装拆箱问题,所以最好调用一次后将结果缓存起来。
- 对于反射CSharp侧的方法、属性、字段、构造函数,则流程如下:
-- 初始化调用一次,注册反射相关Wrap(默认没有初始化,未调用则无法使用 LuaInterface 下的内容)
package.preload['tolua.reflection']()
local type = tolua.findtype("xxxx类名")
-- 方法调用
local method = tolua.gettypemethod(type, "xxxx方法名")
method:Call(参数)
-- 属性调用
local property = tolua.getproperty(type, "xxx属性名")
local instance = property:Get(实例对象, nil)
-- 变量调用
local field = tolua.getfield(type, "xxx变量名")
local field:Get(实例对象)
64位整型
- 前面提到,在CSharp的 OpenToLuaLibs 方法中,调用了lua侧的 tolua_openlibs 进行初始化库,其中对lua侧的int64和uint64进行了初始化。
- tolua_openint64
- tolua_openuint64
- 初始化完后,在lua侧的结构大致如下:
_G = {
...
int64 = LUA_REGISTRYINDEX 表的 LUA_RIDX_INT64 表
uint64 = LUA_REGISTRYINDEX 表的 LUA_RIDX_UINT64 表
}
26(即 LUA_RIDX_LOADED) {
...
int64 = LUA_REGISTRYINDEX 表的 LUA_RIDX_UINT64 表
},
-10000(即 LUA_REGISTRYINDEX) = {
...
20(即 LUA_RIDX_INT64) = {
__add = int64.c的 _int64add 方法
__sub = int64.c的 _int64sub 方法
__mul = int64.c的 _int64mul 方法
__div = int64.c的 _int64div 方法
__mod = int64.c的 _int64mod 方法
__unm = int64.c的 _int64unm 方法
__pow = int64.c的 _int64pow 方法
__tostring = int64.c的 _int64tostring 方法
tostring = int64.c的 _int64tostring 方法
__eq = int64.c的 _int64eq 方法
__lt = int64.c的 _int64lt 方法
__le = int64.c的 _int64le 方法
.name = "int64"
new = int64.c的 tolua_newint64 方法
equals = int64.c的 _int64equals 方法
tonum2 = int64.c的 _int64tonum 方法
__index = LUA_REGISTRYINDEX 表的 LUA_RIDX_INT64 表
}
27(即 LUA_RIDX_UINT64 = {
__add = uint64.c的 _uint64add 方法
__sub = uint64.c的 _uint64sub 方法
__mul = uint64.c的 _uint64mul 方法
__div = uint64.c的 _uint64div 方法
__mod = uint64.c的 _uint64mod 方法
__unm = uint64.c的 _uint64unm 方法
__pow = uint64.c的 _uint64pow 方法
__tostring = uint64.c的 _uint64tostring 方法
tostring = uint64.c的 _uint64tostring 方法
__eq = uint64.c的 _uint64eq 方法
__lt = uint64.c的 _uint64lt 方法
__le = uint64.c的 _uint64le 方法
.name = "uint64"
new = uint64.c的 tolua_newuint64 方法
equals = uint64.c的 _uint64equals 方法
tonum2 = uint64.c的 _uint64tonum 方法
__index = LUA_REGISTRYINDEX 表的 LUA_RIDX_UINT64 表
}
}
- 以int64为例,从CSharp侧传到Lua侧时,调用 tolua_pushint64 方法,创建userdata,将int64的值设为userdata的值,将 LUA_RIDX_INT64 设为userdata的metatable,即
userdata_int64 = {
(头部 Udata*){
...
tt = LUA_TUSERDATA
metatable = LUA_REGISTRYINDEX.LUA_RIDX_INT64
}
(用户自定义数据 user domain*){
int64的值
}
}
- 而从Lua侧传到CSharp侧时,则调用 tolua_toint64,lua侧的int64有几种格式,LUA_TNUMBER、LUA_TSTRING、LUA_TUSERDATA,将lua侧的数据解析成int64在传回CSharp使用。
协程
- 推荐使用lua的协程。
接口(Interface)
- 暂时不支持
- ToLuaExport.Generate 工具生成方法中, 除了 System.Collections.IEnumerator ,其他 Interface 都直接返回不处理。
热修复(暖更新)
- 尽管tolua的设计是为了让开发者能将绝大部分逻辑都在lua侧实现,但还是不可避免需要将部分逻辑放到CSharp侧编写,因此也就出现了需要线上修复CSharp代码的需求。
修复类型
- LuaInterface.InjectType 中定义了修复的各种类型:
- After(1 « 0):CSharp侧方法执行后执行Lua侧注册的方法。
- Before(1 « 1):CSharp侧方法执行前执行Lua侧注册的方法。
- Replace(1 « 2):CSharp侧方法不执行,替换为执行Lua侧注册的方法。
- ReplaceWithPreInvokeBase(1 « 3):CSharp侧方法不执行,替换为执行Lua侧注册的方法,并在Lua方法执行前,执行一次CSharp侧的的Base同名函数。
- ReplaceWithPostInvokeBase(1 « 4):CSharp侧方法不执行,替换为执行Lua侧注册的方法,并在Lua方法执行后,执行一次CSharp侧的的Base同名函数。
宏定义
- 热修复的相关代码,主要通过宏 ENABLE_LUA_INJECTION 来控制开关。
代码注入
- ToLuaInjection.Inject 方法,加载 “./Library/ScriptAssemblies/Assembly-CSharp.dll” 程序集,根据修复类型注入IL代码。
- 以某个类为参考,注入前代码如下:
public class TestInject : MonoBehaviour
{
private int m_Id = 1;
private void Log()
{
Debug.Log(m_Id);
}
private int GetId()
{
return m_Id;
}
}
- 通过查看 Assembly-CSharp.dll ,注入完成后的代码如下:
// Token: 0x0200000C RID: 12
public class TestInject : MonoBehaviour
{
// Token: 0x0600001C RID: 28 RVA: 0x00002D2C File Offset: 0x00000F2C
private void Log()
{
byte injectFlag = LuaInjectionStation.GetInjectFlag(414);
LuaFunction injectionFunction;
if (injectFlag != 0)
{
injectionFunction = LuaInjectionStation.GetInjectionFunction(414);
if (injectFlag > 1)
{
injectionFunction.Call<TestInject>(this);
if (injectFlag > 2)
{
return;
}
}
}
Debug.Log(this.m_Id);
if (injectFlag == 1)
{
injectionFunction.Call<TestInject>(this);
}
}
// Token: 0x0600001D RID: 29 RVA: 0x00002D80 File Offset: 0x00000F80
private int GetId()
{
byte injectFlag = LuaInjectionStation.GetInjectFlag(413);
LuaFunction injectionFunction;
if (injectFlag != 0)
{
injectionFunction = LuaInjectionStation.GetInjectionFunction(413);
if (injectFlag > 1)
{
int result = injectionFunction.Invoke<TestInject, int>(this);
if (injectFlag > 2)
{
return result;
}
}
}
int id = this.m_Id;
int result2 = id;
if (injectFlag == 1)
{
injectionFunction.Call<TestInject>(this);
}
return result2;
}
// Token: 0x04000015 RID: 21
private int m_Id = 1;
}
- 可以看到,经过Inject后,会对方法的逻辑进行修改,根据index获取修复类型,根据修复类型获取对应方法,再根据修复类型确定是替换还是插入执行。
- 对于一些私有的变量、方法等,如果没有提前生成Wrap文件,就需要使用反射获取。
初始化
- 当lua代码加载完成后,会回调执行 LuaState.Start 方法,如果开启了热修复,则会进行热修复的初始化流程:
- 设置 LUA_GLOBALSINDEX 表的 tolua_tag 为 tolua.c 中的 tolua_tag 方法(lightuserdata类型)。
- 执行 LuaState.DoFile 方法,加载并执行 “System/Injection/LuaInjectionStation.lua” 。
- 设置 LuaState.bInjectionInited 为 true。
相关说明
- LuaInjectionStation.lua
- 热修复lua侧启动入口,加载热修复需要的脚本:
- require “System.Injection.LuaInjectionBus”
- local bridgeInfo = require “System.Injection.InjectionBridgeInfo”
- 提供热修复lua侧主要方法:
- InjectByName
- 从 bridgeInfo 表中,查找是否有对应的 moduleName 表。
- 遍历lua侧进行热修复对应的table,检查方法是否在对应的 moduleName 表中。
- 调用CSharp侧的 LuaInjectStation.CacheInjectFunction 方法,将需要热修复的方法的index、修复类型injectFlag、修复方法func注册到CSharp侧。
- LuaInjectionStation.injectFunctionCache[index] = func
- LuaInjectionStation.injectionFlagCache[index] = injectFlag
- InjectByModule
- 获取对应module,检查 &tag 是否为1,为1表示CSharp类已经绑定到Lua侧。
- 使用 module[".name"],调用 InjectByName 方法进行热修复注册。
- 如果是替换类型(Replace、ReplaceWithPostInvokeBase、ReplaceWithPreInvokeBase),修改 module 的 __index 方法。
- 如果 module 中有替换类型的方法,则返回对应方法。
- 如果 module 中没有替换类型的方法,则调用原有的 __index 方法返回对应方法。
- InjectByName
- 热修复lua侧启动入口,加载热修复需要的脚本:
- LuaInjectionBus.lua
- 热修复脚本注册入口,所有需要注册热修复的脚本都需要在这里进行注册,如:
- require “System.Injection.ToLuaInjectionTestInjector”
- 热修复脚本注册入口,所有需要注册热修复的脚本都需要在这里进行注册,如:
- InjectionBridgeInfo.lua
- 记录CSharp侧能进行热修复的方法和对应的索引index。
时序图
- 热修复的时序图为
生成工具
- (待补充)
总结
- 总体来说,ToLua作为使用相对广泛的框架,对于以lua代码为主的项目,能很好地解决热更问题,并且复杂度比较低,比较容易上手。
- 通过整体的介绍,将对象的交互流程比较形象地展现出来,可以更清晰地了解数据的流向,也能更好地管理。
- ToLua主要是通过提前生成Wrap代码的方式,让lua侧能够调用CSharp侧的对象。因此如果没有生成Wrap,就需要通过反射来进行调用,效率会变低。
- 对于重载变量为委托的问题,不容易被发现,所以需要谨慎使用重载。
- 由于ToLua的设计理念是以lua为主体进行开发,因此在热修复上,还是有一些不足,例如:不能在lua侧创建List、Dictionary,没有注册int、float等类型不能直接使用,不支持接口等,需要开发者对框架有比较深入的了解,才能避免出现问题。