SLua入门学习(一)——热更新原理及SLua的Demo剖析
发表于2018-04-23
很多地方都需要使用Lua来进行热更新,这个系列主要是给大家介绍SLua入门学习,对于Lua语法这里就不多说了,先从热更新的原理机制开始给大家进行介绍。
什么是热更新?
为什么要使用Lua进行热更新?
Unity是如何使用Lua进行热更新的?
至此,前两个问题的理解如下:
1、作为 Unity 游戏来说,热更新即在游戏运行的过程中,编译并运行修改后的新代码
2、因为Ios 通过设置内存 No eXecute 限制了 JIT 的使用,而 Lua 可以通过 AoT 等方式编译运行
第三个问题,目前的方案是SLua, SLua使用起来非常简单方便,但不搞清楚其实现机制,用起来总是会心里没底,而SLua没有完全开源,其源码也少有注释,硬啃源码还是比较吃力
所以,在细究SLua的原理之前,得知道 Lua 和 C/C++ 通常是如何交互的,这篇博客给出了不错的解释,Lua 和 C 交互的核心就是栈,Lua 库也提供了大量 API 用来在 C 中对栈进行操作从而实现 Lua 和 C 的数据交换和函数调用。
有了这些前置知识,我们开始看 Slua Unity 的第一个Demo
public class Circle : MonoBehaviour { LuaSvr svr; LuaTable self; LuaFunction update; [CustomLuaClass] public delegate void UpdateDelegate(object self); UpdateDelegate ud; void Start () { svr = new LuaSvr(); svr.init(null, () => { self = (LuaTable)svr.start("circle/circle"); update = (LuaFunction)self["update"] ; ud = update.cast<UpdateDelegate>(); }); } void Update () { if (ud != null) ud(self); } }
首先是 LuaSvr, LuaSvr 其实是对 Lua_State 的一个封装, 而 Lua_State 在这篇博客有详细的解释,主要是管理一个lua虚拟机的执行环境, 通过名为 L 的 int 指针作为 ref
接下来是 svr.init 其实是将 UnityEngine 的一些常用函数压栈以便接下来在 Lua 中调用,在 Editor 中具体调用如下:
IntPtr L = mainState.L; LuaObject.init(L); if (!UnityEditor.EditorApplication.isPlaying) { doBind(L); doinit(mainState, flag); complete(); mainState.checkTop(); }
LuaObject.init(L)我们先跳过,其中 dobind
static internal void doBind(IntPtr L) { var list = collectBindInfo (); int count = list.Count; for (int n = 0; n < count; n++) { Action<IntPtr> action = list[n]; action(L); } }
调用了 list 中所有的委托,现在看看 list 里面存储了什么
static List<Action<IntPtr>> collectBindInfo() { List<Action<IntPtr>> list = new List<Action<IntPtr>>(); #if !SLUA_STANDALONE #if !USE_STATIC_BINDER Assembly[] ams = AppDomain.CurrentDomain.GetAssemblies(); List<Type> bindlist = new List<Type>(); for (int n = 0; n < ams.Length;n++ ) { Assembly a = ams[n]; Type[] ts = null; try { ts = a.GetExportedTypes(); } catch { continue; } for (int k = 0; k < ts.Length; k++) { Type t = ts[k]; if (t.IsDefined(typeof(LuaBinderAttribute), false)) { bindlist.Add(t); } } } bindlist.Sort(new System.Comparison<Type>((Type a, Type b) => { LuaBinderAttribute la = System.Attribute.GetCustomAttribute( a, typeof(LuaBinderAttribute) ) as LuaBinderAttribute; LuaBinderAttribute lb = System.Attribute.GetCustomAttribute( b, typeof(LuaBinderAttribute) ) as LuaBinderAttribute; return la.order.CompareTo(lb.order); })); for (int n = 0; n < bindlist.Count; n++) { Type t = bindlist[n]; var sublist = (Action<IntPtr>[])t.GetMethod("GetBindList").Invoke(null, null); list.AddRange(sublist); } #else var assemblyName = "Assembly-CSharp"; Assembly assembly = Assembly.Load(assemblyName); list.AddRange(getBindList(assembly,"SLua.BindUnity")); list.AddRange(getBindList(assembly,"SLua.BindUnityUI")); list.AddRange(getBindList(assembly,"SLua.BindDll")); list.AddRange(getBindList(assembly,"SLua.BindCustom")); #endif #endif return list; }
可以看到,收集了所有带 LuaBinderAttribute 修饰的 object 并排序然后通过反射获取其 GetBindList 方法里存储的委托,然后插入 list 返回, 查引用则可以看到:
以 BindUnity 为例:
可以看到都是一些 Unity 的常用接口的注册函数,以 Lua_UnityEngine_Camera_StereoscopicEye.reg 为例:
[UnityEngine.Scripting.Preserve] public class Lua_UnityEngine_Camera_StereoscopicEye : LuaObject { static public void reg(IntPtr l) { getEnumTable(l,"UnityEngine.Camera.StereoscopicEye"); addMember(l,0,"Left"); addMember(l,1,"Right"); LuaDLL.lua_pop(l, 1); } }
到这里,已经很接近 lua 的原始接口了, getEnumTable 和 addMember 的封装如下:
public static void getEnumTable(IntPtr l, string t) { newTypeTable(l, t); } public static void newTypeTable(IntPtr l, string name) { string[] subt = name.Split('.'); LuaDLL.lua_pushglobaltable(l); foreach(string t in subt) { LuaDLL.lua_pushstring(l, t); LuaDLL.lua_rawget(l, -2); if (LuaDLL.lua_isnil(l, -1)) { LuaDLL.lua_pop(l, 1); LuaDLL.lua_createtable(l, 0, 0); LuaDLL.lua_pushstring(l, t); LuaDLL.lua_pushvalue(l, -2); LuaDLL.lua_rawset(l, -4); } LuaDLL.lua_remove(l, -2); } } protected static void addMember(IntPtr l, int v, string name) { LuaDLL.lua_pushinteger(l, v); LuaDLL.lua_setfield(l, -2, name); }
到这里,就都是 lua 库提供的对栈进行操作的 API 了, 其具体作用在前面的链接或者其他 lua 文档都可以查到了,LuaObject.init(L) 的作用也很清楚了——为 lua 注入一些通用的方法, dobind 则是注入 UnityEngine 常用的一些方法
doinit(mainState, flag) 则是打开了一些 c# 与 lua 交互所需要的库,具体的看不了源码就深究了,但其中的一个函数
LuaValueType.reg(L.L); public static void reg(IntPtr l) { #if !LUA_5_3 && !SLUA_STANDALONE // lua implemented valuetype isn't faster than raw under non-jit. LuaState ls = LuaState.get(l); ls.doString(script, "ValueTypeScript"); #endif }
一路调用到了 dobuffer 函数,而我们常用的 dofile 也调用到了 dobuffer, dobuffer 到底是做什么的?其源码如下:
public bool doBuffer(byte[] bytes, string fn, out object ret) { // ensure no utf-8 bom, LuaJIT can read BOM, but Lua cannot! bytes = CleanUTF8Bom(bytes); ret = null; int errfunc = LuaObject.pushTry(L); if (LuaDLL.luaL_loadbuffer(L, bytes, bytes.Length, fn) == 0) { if (LuaDLL.lua_pcall(L, 0, LuaDLL.LUA_MULTRET, errfunc) != 0) { LuaDLL.lua_pop(L, 2); return false; } LuaDLL.lua_remove(L, errfunc); // pop error function ret = topObjects(errfunc - 1); return true; } string err = LuaDLL.lua_tostring(L, -1); LuaDLL.lua_pop(L, 2); throw new Exception(err); }
没错,我们看到了 JIT,这里就是编译并运行 lua 代码的,首先是处理一些编码的问题先不管,然后将错误处理函数入栈并返回 index, 然后通过 luaL_loadbuffer 载入并解析这段代码,如果解析成功,会把结果压到栈 L 中
接下来就是运行了,正如前面链接的博客解释的差不多:
函数调用流程是先将函数入栈,参数入栈,然后用lua_pcall调用函数,此时栈顶为参数,栈底为函数,所以栈过程大致会是:参数出栈->保存参数->参数出栈->保存参数->函数出栈->调用函数->返回结果入栈
而这里稍有区别,是从头运行整个 bytes 中包含的所有 lua 代码,将整块 lua 代码看作一个无参函数, 参照 lua_pcall 的定义,其中参数 LuaDLL.LUA_MULTRET 表示所有的返回值都会入栈, 如果运行出错则运行错误处理函数并将 error message 入栈, 运行成功则移除掉错误处理函数并取出返回值,lua_pcall 的具体信息和使用可以参考这篇博客
而说了这么多,这里的 doInit 函数到底是干嘛的呢?
我们可以看到 doString 的 script 参数其实是一个好长字符串,里面存储的都是常用的数学库的 lua 实现, doInit 做的实际上就是运行了这一大托 lua 代码,声明了一大堆数学函数,方便之后在 lua 中使用,原因应该是为了提高效果,具体可以看 UWA 的这篇博客
回到 doInit, 接下来就是 complete 回调了没什么好说的了,其余的代码有了上面的基础也都很好理解了,总结下,其实 LuaSvr 就是一个封装了 Unity 常用接口、数学库和一些通用方法的 Lua_State
接下来我们看这个 example 的 lua 代码,已经有博客写了源码分析,但只说了结论,那我们就来根据结论和源码来倒推其背后的机制:
-- Circle.cs里先dofile把文件加载到global,然后通过luaState[key]把global的值压栈给c#使用 -- c#把入栈的luaState["main"]转换为LuaFunction,然后根据ref压栈并pcall之,得到classtable入栈,然后c#把此table转为LuaTable, -- 就可以通过LuaTable里的ref访问register中的class这个实例数据了。 function main()
在 complete 回调中有这样一句 self = (LuaTable)svr.start("circle/circle"); ,其定义为:
public object start(string main) { if (main != null) { mainState.doFile(main); return mainState.run("main"); } return null; }
对,出现了 doFile, 再往下就是前面的 dobuffer, 通过 lua_getglobal 将文件解析并存入了栈中,至于这里为什么说是 global 我不太清楚,再往下就是DLL了看不了源码,存在多个 Lua_State 的时候到底怎么处理还没搞清楚,这个以后再说…
接下来mainState.run("main") 则是运行 lua 中的 main 函数,其源码如下:
public object run(string entry) { using (LuaFunction func = getFunction(entry)) { if (func != null) return func.call(); } return null; } public LuaFunction getFunction(string key) { //把入栈的luaState["main"]转换为LuaFunction return (LuaFunction)this[key]; } public LuaFunction(LuaState l, int r) : base(l, r) { } // base constructor public LuaVar(LuaState l, int r) { state = l; valueref = r; }
首先是把入栈的luaState[“main”]转换为LuaFunction,然后我们看func.call() 的源码:
public object call() { int error = LuaObject.pushTry(state.L); if (innerCall(0, error)) { return state.topObjects(error - 1); } return null; } bool innerCall(int nArgs, int errfunc) { bool ret = pcall(nArgs, errfunc); LuaDLL.lua_remove(L, errfunc); return ret; } public bool pcall(int nArgs, int errfunc) { if (!state.isMainThread()) { Logger.LogError("Can't call lua function in bg thread"); return false; } //转换为 LuaFunction 时构造函数存储的 valueref LuaDLL.lua_getref(L, valueref); if (!LuaDLL.lua_isfunction(L, -1)) { LuaDLL.lua_pop(L, 1); throw new Exception("Call invalid function."); } LuaDLL.lua_insert(L, -nArgs - 1); if (LuaDLL.lua_pcall(L, nArgs, -1, errfunc) != 0) { LuaDLL.lua_pop(L, 1); return false; } return true; }
main 函数的运行初始化了一个名为 class 的 Table 并且返回,c# 从栈顶取得该 table 并转换,这里可以看到 LuaTable 的构造函数:
public LuaTable(LuaState state) : base(state, 0) { LuaDLL.lua_newtable(L); valueref = LuaDLL.luaL_ref(L, LuaIndexes.LUA_REGISTRYINDEX); }
通过 luaL_ref 将 table 存入注册表并返回引用,注册表用来在 c 中存放 lua 的全局信息, 同环境变量、UpValue 一起用来在 c 中保存状态,具体解析可以看这篇博客,luaL_ref 是实例分析则可以看这篇博客
OK,我们开始下一行:
-- 先取得GameObject类型表,然后使用里面注册的Find函数(其实是lclosure),然后调用此lclosure, c#层对应的Find函数被调用,把找到的GameObject实例对象存入ObjectCach中 -- 同时作为ud压入栈,并设置GameObject实例表为该ud的元表,这样ud就可以使用GameObject实例表的Getcomponent函数(lclosure), -- 同理的得到一个元表为UI.Slider实例表的ud,赋值给slider,counttxt类似 local slider = GameObject.Find("Canvas/Slider"):GetComponent(UI.Slider) local counttxt = GameObject.Find("Canvas/Count"):GetComponent(UI.Text)
在 Lua_UnityEngine_GameObject.cs 脚本中我们可以找到如下函数;
[SLua.MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))] [UnityEngine.Scripting.Preserve] static public int Find_s(IntPtr l) { try { System.String a1; checkType(l,1,out a1); var ret=UnityEngine.GameObject.Find(a1); pushValue(l,true); pushValue(l,ret); return 2; } catch(Exception e) { return error(l,e); } } ... addMember(l,SLua.MyGameObject.Find_s); ...
在前面提过 reg 注册函数中通过 AddMenber 的形式注入 Lua, 而在这里, Find_s 使用的是重载后的,官方 wiki 解释如下:
有时我们需要在默认动态生成的导出函数中增加一些自己的代码,之前你需要在生成的wrapper文件里手动添加对应的代码,但这样每次重新make之后,添加的代码会丢失,需要重新添加,这时你可以考虑重载默认的导出方法,例如:
namespace SLua { [OverloadLuaClass(typeof(GameObject))] public class MyGameObject : LuaObject { [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))] public static int Find_s(IntPtr l) { UnityEngine.Debug.Log ("GameObject.Find overloaded my MyGameObject.Find"); try { System.String a1; checkType(l,1,out a1); var ret=UnityEngine.GameObject.Find(a1); pushValue(l,true); pushValue(l,ret); return 2; } catch(Exception e) { return error(l,e); } } } }
这样GameObject.Find方法的导出方法会调用到上面Find_s函数中,你可以任意添加自己的代码,在最终的wrapper文件中,也会使用上述方法作为Find方法的导出实现。
现在我们看 AddMember 函数
protected static void addMember(IntPtr l, LuaCSFunction func) { checkMethodValid(func); pushValue(l, func); string name = func.Method.Name; if (name.EndsWith("_s")) { name = name.Substring(0, name.Length - 2); LuaDLL.lua_setfield(l, -3, name); } else LuaDLL.lua_setfield(l, -2, name); }
这里 _s 的区分处理,在另外一篇博客找到的答案如下:
- 只有函数指针位置的部分,在Lua中定义成了Table变量内的函数,例如:cube:AddCommponent
- 在函数指针名的末尾部分以_s结尾的,在Lua中定义成了元表变量内的函数,例如:GameObject.CreatePrimitive
- 在添加成员时,包含了类似于"transform"字符串的,在Lua中定义成了Table变量内的键值对属性,例如:cube.transform
然后我们回到 Find_s 函数,注意这里的 pushValue 并非原生的 lua 接口,而是经过封装的,定义如下:
public static void pushValue(IntPtr l, UnityEngine.Object o) { if (o == null) LuaDLL.lua_pushnil(l); else pushObject(l, o); } public static void pushObject(IntPtr l, object o) { ObjectCache oc = ObjectCache.get(l); oc.push(l, o); } internal void push(IntPtr l, object o) { push(l, o, true); } internal void push(IntPtr l, object o, bool checkReflect) { int index = allocID (l, o); if (index < 0) return; bool gco = isGcObject(o); #if SLUA_CHECK_REFLECTION int isReflect = LuaDLL.luaS_pushobject(l, index, getAQName(o), gco, udCacheRef); if (isReflect != 0 && checkReflect && !(o is LuaClassObject)) { Logger.LogWarning(string.Format("{0} not exported, using reflection instead", o.ToString())); } #else //同时作为ud压入栈,并设置GameObject实例表为该ud的元表,看不了源码,但只能是这句了 LuaDLL.luaS_pushobject(l, index, getAQName(o), gco, udCacheRef); #endif } internal int allocID(IntPtr l,object o) { int index = -1; if (o == null) { LuaDLL.lua_pushnil(l); return index; } bool gco = isGcObject(o); bool found = gco && objMap.TryGetValue(o, out index); if (found) { if (LuaDLL.luaS_getcacheud(l, index, udCacheRef) == 1) return -1; } index = add(o); return index; } internal int add(object o) { //把找到的GameObject实例对象存入ObjectCach中 int objIndex = cache.add(o); if (isGcObject(o)) { objMap[o] = objIndex; #if SLUA_DEBUG || UNITY_EDITOR objNameDebugs[o] = getDebugName(o); #endif } return objIndex; }
然后我们继续下一句:
-- onValueChaned是UI.Slider实例表中注入的一个属性表,取属性会调用属性表里的第一个函数(lclosure),c#对应的get函数被调用,一个元表为SliderEvent实例表的ud入栈 -- 而SliderEvent实例表.__parent = UnityEvent_float实例表,后者注入了AddListenner函数(lclosure),调用它会调用c#对应函数,c#中把lua传过来的lfunction转为 -- LuaDelegate ld,接着实例化一个UnityAction<float>的委托,委托里会调用ld,并且委托会被Add到前面的ud在c#中的UnityEvent<float>实例中, -- 这样,当c#的onValueChanged时就会调用该委托,进而调用ld,进而通过ld里的ref调用register里的lfunction,即下面那个函数 slider.onValueChanged:AddListener( function(v) class:init(v) counttxt.text=string.format("cube:%d",v) end )
有了前面的分析,这里就可以顺藤摸瓜了,在Lua_UnityEngine_UI_Slider.cs 中,可以看到如下代码:
addMember(l,"onValueChanged",get_onValueChanged,set_onValueChanged,true); protected static void addMember(IntPtr l, string name, LuaCSFunction get, LuaCSFunction set, bool instance) { checkMethodValid(get); checkMethodValid(set); int t = instance ? -2 : -3; LuaDLL.lua_createtable(l, 2, 0); if (get == null) LuaDLL.lua_pushnil(l); else pushValue(l, get); LuaDLL.lua_rawseti(l, -2, 1); if (set == null) LuaDLL.lua_pushnil(l); else pushValue(l, set); LuaDLL.lua_rawseti(l, -2, 2); LuaDLL.lua_setfield(l, t, name); }
这里大佬的分析已经很清楚了,我一小白就不再做解释了,因为还没有看过 lua 源码,因此也没法再深入去分析,下一篇再来分析具体的 lua 源码实现和过程中虚拟栈的内存结构
来自:https://blog.csdn.net/notmz/article/details/79645949