SLua入门学习(一)——热更新原理及SLua的Demo剖析

SLua入门学习(一)——热更新原理及SLua的Demo剖析

发表于2018-04-23

评论0

8k浏览

很多地方都需要使用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();

});

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 action = list[n];

action(L);

调用了 list 中所有的委托,现在看看 list 里面存储了什么

static List> collectBindInfo() {

List> list = new List>();

#if !SLUA_STANDALONE

#if !USE_STATIC_BINDER

Assembly[] ams = AppDomain.CurrentDomain.GetAssemblies();

List bindlist = new List();

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 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[])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的委托,委托里会调用ld,并且委托会被Add到前面的ud在c#中的UnityEvent实例中,

-- 这样,当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

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

标签:

sluaLUAListJavaScriptUnity游戏视觉