0922学习 agile Posted on Sep 25 2023 面试 xlua lua ###Xlua 热补丁 --- ```lua xlua.hotfix = function(cs, field, func) if func == nil then func = false end local tbl = (type(field) == 'table') and field or {[field] = func} for k, v in pairs(tbl) do local cflag = '' --构造方法 if k == '.ctor' then cflag = '_c' k = 'ctor' end local f = type(v) == 'function' and v or nil xlua.access(cs, cflag .. '__Hotfix0_'..k, f) -- at least one pcall(function() for i = 1, 99 do -- 重载方法 xlua.access(cs, cflag .. '__Hotfix'..i..'_'..k, f) end end) end //在热更新中可以修改私有变量 xlua.private_accessible(cs) end ``` --- ![Snip20230925_2.png](https://tools.nxcloud.club:12500/images/2023/09/25/Snip20230925_2.png) - 构造方法的key为`_c__Hotfix0_octor` - 普通方法的key为`__Hotfix0_key` - 重载方法的key为`__Hotfix{%d}_key` --- ```C# //xlua.access方法会找到这个字段,给这个字段赋值 private static DelegateBridge __Hotfix1_TestOut; public int TestOut(int a, out double b, ref string c) { //如果该类被标记为hotfix,执行了hotfix injux in editor后,会在每一个打了hotfix标签的类方法中,插入如下方法,判断是否存在DelegateBridge,如果有,就执行DelegateBridge逻辑,也就是有热更,就执行热更新代码 DelegateBridge _Hotfix0_TestOut = __Hotfix0_TestOut; if (_Hotfix0_TestOut != null) { //__Gen_Delegate_Imp17这个方法在DelegateBridge中,得事先生成 return _Hotfix0_TestOut.__Gen_Delegate_Imp17(this, a, out b, ref c); } b = a + 2; c = "wrong version"; return a + 3; } ``` --- - _Hotfix0_TestOut代码的注入是通过`injury`这个按钮调用`./Tools/XLuaHotfixInject.exe`注入 --- ####`xlua.access:StaticLuaCallbacks.XLuaAccess` --- ```C# public static int XLuaAccess(RealStatePtr L) { ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L); //获取类型 Type type = getType(L, translator, 1); object obj = null; if (type == null && LuaAPI.lua_type(L, 1) == LuaTypes.LUA_TUSERDATA) { obj = translator.SafeGetCSObj(L, 1); if (obj == null) { return LuaAPI.luaL_error(L, "xlua.access, #1 parameter must a type/c# object/string"); } type = obj.GetType(); } //获取field名 string fieldName = LuaAPI.lua_tostring(L, 2); //private static DelegateBridge __Hotfix1_TestOut; 类似这样的字段 BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; if (LuaAPI.lua_gettop(L) > 2) // set { //判断是否是字段 var field = type.GetField(fieldName, bindingFlags); if (field != null) { //给DelegateBridge赋值 field.SetValue(obj, translator.GetObject(L, 3, field.FieldType)); return 0; } //属性的情况 var prop = type.GetProperty(fieldName, bindingFlags); if (prop != null) { prop.SetValue(obj, translator.GetObject(L, 3, prop.PropertyType), null); return 0; } } else { //设置值 var field = type.GetField(fieldName, bindingFlags); if (field != null) { translator.PushAny(L, field.GetValue(obj)); return 1; } var prop = type.GetProperty(fieldName, bindingFlags); if (prop != null) { translator.PushAny(L, prop.GetValue(obj, null)); return 1; } } return LuaAPI.luaL_error(L, "xlua.access, no field " + fieldName); } ``` --- ###`hotfix_ex` :想调用修复之前原先的方法,可以通过`hotfix_ex` --- ```lua local function hotfix_ex(cs, field, func) --断言,检查参数 assert(type(field) == 'string' and type(func) == 'function', 'invalid argument: #2 string needed, #3 function needed!') --创建中间函数,就是上文提到的A local function func_after(...) --先将需要热更修复的方法设置为nil,那么再调用方法时候会执行的就是之前方法 xlua.hotfix(cs, field, nil) --执行func,就是上文提到的functionB local ret = {func(...)} ---重新将需要热更修复的方法设置为中间函数A xlua.hotfix(cs, field, func_after) return unpack(ret) end --设置需要热更修复为中间函数A xlua.hotfix(cs, field, func_after) end ``` --- ```lua local util=require 'util' util.hotfix_ex(CS.HotFixMgr,'SetParent',function(self) self:SetParent() self.Cube.transform:SetParent(self.Capsule.transform); self.Cube.transform.localPosition=CS.UnityEngine.Vector3.zero; end) ``` - 在新的方法体中调用`self:SetParent()`就可以执行C#原来的方法 - 不能在`xlua.hotfix`中再调用当前要修改的方法,只能在`hotfix_ex`中调用 --- ###lua优化 --- ####使用局部变量 --- - Lua采用了一种类似于寄存器的虚拟机模式 - Lua用栈来储存其寄存器。每一个活动的函数,Lua都会其分配一个栈,这个栈用来储存函数里的活动记录 - 假设`a和b为local`变量,`a = a + b`的预编译会产生一条指令 ``` ;a是寄存器0 b是寄存器1 ADD 0 0 1 ``` - 若`a和b都没有声明为local变量`,则预编译会产生如下指令 ``` GETGLOBAL 0 0 ;get a GETGLOBAL 1 1 ;get b ADD 0 0 1 ;do add SETGLOBAL 0 0 ;set a ``` --- ####字符串拼接使用`table.concat` - Lua的string是内部复用的 - 当我们创建字符串的时候,Lua首先会`检查内部`是否已经有`相同的字符串`了,如果有`直接返回一个引用`,如果没有才创建 - Lua中string的`比较和赋值`非常地快速,因为只要比较引用是否相等、或者直接赋值引用就可以了 - 这个机制使得Lua在拼接字符串上开销比较大,Lua先要`遍历查找是否有相同的字符串`,然后会把后者整个`拷贝然后拼接` - `table.concat`只会`创建一块buffer`,然后在此拼接所有的字符串,实际上是在用table模拟buffer - `..`则每次拼接都会`产生一串新的字符串`,`开辟一块新的buffer` --- ####table --- - 每个Lua表的内部包含两个部分:`数组部分`和`哈希部分` - 数组部分以从`1到一个特定的n之间的整数作为键`来保存元素 - 所有其他元素(`包括在上述范围之外的整数键`)都被存放在`哈希部分`里 - 哈希表本质是一个数组,它利用哈希算法将键转化为数组下标 - 我们把一个新键值赋给表时,若数组和哈希表已经满了,则会触发一个再哈希rehash - 首先会在内存中分配一个新的长度的数组,然后将所有记录再全部哈希一遍,将原来的记录转移到新数组中 - 新哈希表的长度是最接近于所有元素数目的2的乘方 - 当创建一个空表时,数组和哈希部分的长度都将初始化为0,即不会为它们初始化任何数组 ```lua local a = {} for i=1,3 do a[i] = true end ``` - 执行上面这段代码时在Lua中发生了什么 - 最开始,Lua创建了一个空表a,在第一次迭代中`,a[1] = true`触发了一次`rehash` - Lua将数组部分的长度设置为`2^0`,即`1`,哈希部分仍为`空` - 在第二次迭代中,`a[2]=true`再次触发了rehash,将数组部分长度设为`2^1`,即`2` - 最后一次迭代,又触发了一次rehash,将数组部分长度设为`2^2`,即`4` ```lua a = {} a.x = 1; a.y = 2; a.z = 3 ``` - 与上一段代码类似,只是其触发了三次表中哈希部分的rehash而已 - `只有三个元素的表,会执行三次rehash`;然而有`一百万个元素的表`仅仅只会执行`20次rehash`而已,因为`2^20 = 1048576 > 1000000` - 你有很多非常多的很小的表需要创建时,你可以`将其预先填充以避免rehash`。比如:`{true,true,true}` - 当需要创建非常多的`小size`的表时,应`预先填充好表的大小` --- ###3R原则 - `减量化(reducing),再利用(reusing)和再循环(recycling)`三种原则的简称 --- ####`reducing` --- ```lua polyline = { { x = 10.3, y = 98.5 }, { x = 10.3, y = 18.3 }, { x = 15.0, y = 98.5 }, --... } --可以改成这样,减少字段数量 polyline = { { 10.3, 98.5 }, { 10.3, 18.3 }, { 15.0, 98.5 }, --... } --进一步这样,减少table数量 polyline = { x = { 10.3, 10.3, 15.0, ...}, y = { 98.5, 18.3, 98.5, ...} } ``` --- ####`reusing`:缓存起来使用 --- ```lua function memoize (f) local mem = {} -- 缓存化表 setmetatable(mem, {__mode = "kv"}) -- 设为弱表 -- f缓存化后的新版本 return function (x) local r = mem[x] if r == nil then --没有之前记录的结果? r = f(x) --调用原函数 mem[x] = r --储存结果以备重用 end return r end end --新函数的使用方式与老的完全相同,但是如果在加载时有很多重复的字符串,性能会得到大幅提升 loadstring = memoize(loadstring) local t = {} for i = 1970, 2000 do t[i] = os.time({year = i, month = 6, day = 14}) end local t = {} local aux = {year = nil, month = 6, day = 14} for i = 1970, 2000 do aux.year = i; t[i] = os.time(aux) end ``` --- ####`Recycling`:垃圾回收 --- - 通过Lua的`collectgarbage`函数来控制垃圾回收器 --- ####在lua中引用c#的object,代价昂贵 - `gameobj.transform`:如果`.transform`只是临时返回一下,后面根本没引用,会很快被lua释放掉,导致后面每次`.transform`一次,都可能意味着一次`分配userdata和gc` --- ####频繁调用的函数,参数的数量要控制 - lua调用c#的性能,除了跟参数类型相关外,也跟参数个数有很大关系 - lua的`pushint/checkint`,还是c到c#的参数传递,参数转换都是最主要的消耗,而且是逐个参数进行 --- ####优先使用static函数导出,减少使用成员方法导出 ```C# public static void GetPos2(GameObject obj, out float x, out float y, out float z){ ... } public void GetPos(GameObject obj, out float x, out float y, out float z) { ... } //GetPos方法的调用 //需要多获取一发gen_to_be_invoked,调用translator.FastGetCSObj XLuaTest.LuaBehaviour gen_to_be_invoked = (XLuaTest.LuaBehaviour)translator.FastGetCSObj(L, 1); UnityEngine.GameObject _obj = (UnityEngine.GameObject)translator.GetObject(L, 2, typeof(UnityEngine.GameObject)); float _x; float _y; float _z; gen_to_be_invoked.GetPos( _obj, out _x, out _y, out _z ); LuaAPI.lua_pushnumber(L, _x); LuaAPI.lua_pushnumber(L, _y); LuaAPI.lua_pushnumber(L, _z); //GetPos2方法的调用 //直接调用XLuaTest.LuaBehaviour //效率更高 UnityEngine.GameObject _obj = (UnityEngine.GameObject)translator.GetObject(L, 1, typeof(UnityEngine.GameObject)); float _x; float _y; float _z; XLuaTest.LuaBehaviour.GetPos2( _obj, out _x, out _y, out _z ); LuaAPI.lua_pushnumber(L, _x); LuaAPI.lua_pushnumber(L, _y); LuaAPI.lua_pushnumber(L, _z); ``` - 实例方法调用会多一发获取实例,`translator.FastGetCSObj` ###合理利用out关键字返回复杂的返回值 ```C# public Vector3 GetPos(GameObject obj) { return obj.transform.position; } public void GetPos(GameObject obj, out float x, out float y, out float z) { var p = obj.transform.position; x = p.x; y = p.y; z = p.z; } //返回vector3最后调用的xlua_pack_float3 //其实也是push三个参数 //但流程多了好多 translator.PushUnityEngineVector3(L, gen_ret); IntPtr buff = LuaAPI.xlua_pushstruct(L, 12, UnityEngineVector3_TypeID); if (!CopyByValue.Pack(buff, 0, val)) LuaAPI.xlua_pack_float3(buff, offset, field.x, field.y, field.z) //GetPos(GameObject obj, out float x, out float y, out float z) //直接push值过去 LuaAPI.lua_pushnumber(L, _x); LuaAPI.lua_pushnumber(L, _y); LuaAPI.lua_pushnumber(L, _z); ``` --- ###注意lua拿着c#对象的引用时会造成c#对象无法释放,这是内存泄漏常见 --- - c# object返回给lua,是通过dictionary将lua的userdata和c# object关联起来,只要lua中的userdata没回收,c# object也就会被这个dictionary拿着引用,导致无法回收 - 最常见的就是gameobject和component,如果lua里头引用了他们,即使C#进行了Destroy,只要lua还有引用,就不会被回收 --- 0928学习 0921学习