什么是热更新?
为什么要使用Lua进行热更新?
Unity是如何使用Lua进行热更新的?
知乎这个问题 的觉得下面已经给出了不少解释,其中一个回答 又很好的解释了为什么Unity没有原生的热更新方案,看完这个我又开始疑问编译器和解释器的区别是什么?静态语言和脚本语言的本质区别又是什么?这篇博客 给出了非常生形象的解释,然后,我们就可以深入到问题的核心:CIL,大佬也在博客 里进行了详细的解释
至此,前两个问题我的理解如下:
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 里面存储了什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 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 为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 using System;using System.Collections.Generic;namespace SLua { [LuaBinder(0) ] public class BindUnity { public static Action<IntPtr>[] GetBindList ( ) { Action<IntPtr>[] list= { Lua_UnityEngine_Rendering_ComputeQueueType.reg, Lua_UnityEngine_Rendering_CommandBuffer.reg, Lua_UnityEngine_Rendering_SphericalHarmonicsL2.reg, Lua_UnityEngine_RectOffset.reg, Lua_UnityEngine_Object.reg, Lua_UnityEngine_Component.reg, Lua_UnityEngine_SortingLayer.reg, Lua_UnityEngine_GradientColorKey.reg, Lua_UnityEngine_GradientAlphaKey.reg, Lua_UnityEngine_GradientMode.reg, Lua_UnityEngine_Gradient.reg, ...
可以看到都是一些 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 的封装如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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 LuaState ls = LuaState.get (l); ls.doString(script, "ValueTypeScript" );#endif }
一路调用到了 dobuffer 函数,而我们常用的 dofile 也调用到了 dobuffer, dobuffer 到底是做什么的?其源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public bool doBuffer (byte [] bytes, string fn, out object ret ) { 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); 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 代码,已经有博客 写了源码分析,但只说了结论,那我们就来根据结论和源码来倒推其背后的机制:
在 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 函数,其源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public object run (string entry ) { using (LuaFunction func = getFunction(entry)) { if (func != null ) return func.call(); } return null ; }public LuaFunction getFunction (string key ) { return (LuaFunction)this [key]; }public LuaFunction (LuaState l, int r ) : base (l, r ) { }public LuaVar (LuaState l, int r ) { state = l; valueref = r; }
首先是把入栈的luaState[“main”]转换为LuaFunction,然后我们看func.call() 的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 ; } 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,我们开始下一行:
local slider = GameObject.Find("Canvas/Slider" ):GetComponent(UI.Slider)local counttxt = GameObject.Find("Canvas/Count" ):GetComponent(UI.Text)
在 Lua_UnityEngine_GameObject.cs 脚本中我们可以找到如下函数;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [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之后,添加的代码会丢失,需要重新添加,这时你可以考虑重载默认的导出方法,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 接口,而是经过封装的,定义如下:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 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 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 ) { int objIndex = cache.add (o); if (isGcObject(o)) { objMap[o] = objIndex; #if SLUA_DEBUG || UNITY_EDITOR objNameDebugs[o] = getDebugName(o); #endif } return objIndex; }
然后我们继续下一句:
slider.onValueChanged:AddListener( function (v) class:init(v) counttxt.text=string .format ("cube:%d" ,v) end )
有了前面的分析,这里就可以顺藤摸瓜了,在Lua_UnityEngine_UI_Slider.cs 中,可以看到如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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