Xlua原理分析 四

avatar
作者
筋斗云
阅读量:0

前面已经介绍了Xlua的通信原理,这篇主要记录Xlua如何做到Hotfix的。

我们项目就用到Xlua的Hotfix特性,周更用Lua去修改代码。版本内用C#开发。这点我觉得是Xlua比toLua强大的重要特性之一。

如何使用Hotfix本篇不介绍了,看Xlua教程懂得都懂,着重于原理部分。

一、如何进行Hotfix

先上测试代码:

    void Update()         {             if (++tick % 50 == 0)             {                 Debug.Log(">>>>>>>>Update in C#, tick = " + tick);                 TestHotFixLog("C#");             }         }          public void TestHotFixLog(string str)         {             Debug.Log("TestHotFixLog:" + str);         }         void OnGUI()         {             if (GUI.Button(new Rect(10, 10, 300, 80), "Hotfix"))             {                 luaenv.DoString(@"                 xlua.hotfix(CS.XLuaTest.HotfixTest, 'Update', function(self)                     self.tick = self.tick + 1                     if (self.tick % 50) == 0 then                         print('<<<<<<<<Update in lua, tick = ' .. self.tick)                         self:TestHotFixLog('lua')                     end                 end)             ");             }         }

使用反编译编译Library\ScriptAssemblies\Assembly-CSharp.dll。可以看到这段

可以清晰的看到,反编译后是生成了一些委托,如果委托函数有值就不走原函数。

看看DelegateBridge 的结构:

对应上面的Update的这个函数,又看到了熟悉的压栈操作,通过这样的方式可实现热修

C#端的基础原理搞清后。看看xlua.hotfix都干了什么事。

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

cs对应改的C#类,跟上面的反编译脚本一致。

field一个字符串

func方法。

可以看到他这里拼接了字符串,然后去向C#的委托去传递这个方法。

xlua.access(cs, cflag .. '__Hotfix0_'..k, f) -- at least one  这里对应上述修改的__Hotfix0_Update

后面1-99 是修改了重载函数,造成了一定的性能损失。

PS:我们项目不允许C#代码使用同名重载函数,会出现很多意外的问题,可能就跟这里有关

再刨个根吧,看看xlua.access的实现:

[MonoPInvokeCallback(typeof(LuaCSFunction))]         public static int XLuaAccess(RealStatePtr L)         {             try             {                 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();                 }                  if (type == null)                 {                     return LuaAPI.luaL_error(L, "xlua.access, can not find c# type");                 }                  string fieldName = LuaAPI.lua_tostring(L, 2);                  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)                     {                         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);             }             catch (Exception e)             {                 return LuaAPI.luaL_error(L, "c# exception in xlua.access: " + e);             }         }

这里是使用了type获取元数据进行调用。

至此,xlua的hotfix原理已经清晰了。

util.hotfix就是先执行一遍lua的函数体,然后再执行一遍hotfix。所以可以执行原函数

--和xlua.hotfix的区别是:这个可以调用原来的函数 local function hotfix_ex(cs, field, func)     assert(type(field) == 'string' and type(func) == 'function', 'invalid argument: #2 string needed, #3 function needed!')     local function func_after(...)         xlua.hotfix(cs, field, nil)         local ret = {func(...)}         xlua.hotfix(cs, field, func_after)         return unpack(ret)     end     xlua.hotfix(cs, field, func_after) end

二、如何生成程序集

  1. Generate Code
    这一步主要根据是根据C#类中需要支持热更的方法生成其对应的委托方法,但是并不是每个方法对应一个委托,而是根据调用参数和返回参数公用委托。这块之前有详细介绍代码,就不复述了。
  2. Hotfix Inject
    这一步主要是对Unity编译出的Dll中的C#类添加判断条件,以此来选择调用Lua中的修复方法还是直接执行C#代码

这一步是在Unity为C#代码生成完对应dll之后,由XLua再来对dll注入一些判断条件式来完成是否进行Lua调用的行为。
判断方法很简单,检查对应类静态字段是否有DelegateBridge对象。
实现如下:

bool injectMethod(MethodDefinition method, HotfixFlagInTool hotfixType)         {             var type = method.DeclaringType;                          bool isFinalize = (method.Name == "Finalize" && method.IsSpecialName);             //__Gen_Delegate_Imp 方法引用             MethodReference invoke = null;              int param_count = method.Parameters.Count + (method.IsStatic ? 0 : 1); 			//根据返回值和参数个数类型和方法全名找对应的C#方法             if (!findHotfixDelegate(method, out invoke, hotfixType))             {                 Error("can not find delegate for " + method.DeclaringType + "." + method.Name + "! try re-genertate code.");                 return false;             }              if (invoke == null)             {                 throw new Exception("unknow exception!");             }  #if XLUA_GENERAL             invoke = injectAssembly.MainModule.ImportReference(invoke); #else             invoke = injectAssembly.MainModule.Import(invoke); #endif 			//插入的类静态字段,用来标记对应的方法是否有对应的Lua注入             FieldReference fieldReference = null; 			//方法中的变量定义             VariableDefinition injection = null; 			//IntKey前面InjectType设置过,没有泛型参数并且是同一个程序集             bool isIntKey = hotfixType.HasFlag(HotfixFlagInTool.IntKey) && !type.HasGenericParameters && isTheSameAssembly;             //isIntKey = !type.HasGenericParameters; 			             if (!isIntKey)             { 				//新建变量,看起来跟重载函数有关系                 injection = new VariableDefinition(invoke.DeclaringType);                 method.Body.Variables.Add(injection); 				//luaDelegateName 是个string方法名称 				//获取这个方法对应的委托名,因为有重载方法存在,所以之前已经注入的过的方法会在这边获取时候计数加1,                 //比如第一个重载获取的是__Hotfix0,那么下一个重载会是__Hotfix1,判断是否注入就是是否设置对应FieldReference。                 var luaDelegateName = getDelegateName(method); 				//一般不error,除非超过 MAX_OVERLOAD 100个。                 if (luaDelegateName == null)                 {                     Error("too many overload!");                     return false;                 } 				//创建对应的静态Field名字就是上面取到的luaDelegateName                 FieldDefinition fieldDefinition = new FieldDefinition(luaDelegateName, Mono.Cecil.FieldAttributes.Static | Mono.Cecil.FieldAttributes.Private,                     invoke.DeclaringType);                 type.Fields.Add(fieldDefinition);                 fieldReference = fieldDefinition.GetGeneric();             }              bool ignoreValueType = hotfixType.HasFlag(HotfixFlagInTool.ValueTypeBoxing); 			 //IL插入位置,现在定位的是方法体的第一行             var insertPoint = method.Body.Instructions[0]; 			//获取IL处理器             var processor = method.Body.GetILProcessor(); 			//构造函数换个位置插。先不管了             if (method.IsConstructor)             {                 insertPoint = findNextRet(method.Body.Instructions, insertPoint);             }              Dictionary<Instruction, Instruction> originToNewTarget = new Dictionary<Instruction, Instruction>();             HashSet<Instruction> noCheck = new HashSet<Instruction>();              while (insertPoint != null)             {                 Instruction firstInstruction; 				//isIntKey这边用到的是Xlua中的AutoIdMap,这边只对最基础的功能做分析,这边就分析基础的注入了。                 if (isIntKey)                 {                     firstInstruction = processor.Create(OpCodes.Ldc_I4, bridgeIndexByKey.Count);                     processor.InsertBefore(insertPoint, firstInstruction); 					//调用方法                     processor.InsertBefore(insertPoint, processor.Create(OpCodes.Call, hotfixFlagGetter));                 }                 else                 { 					//创建第一条IL语句,获取类的静态Field压入方法栈中,其实就是之前luaDelegateName获取的字段(换句话说这里就是创建诸如 __Hotfix0_Start;)                     firstInstruction = processor.Create(OpCodes.Ldsfld, fieldReference);                     //插入insertPoint之前                     processor.InsertBefore(insertPoint, firstInstruction);                     //创建并插入IL,获取栈顶的值并压入到对应的变量中,injection就是我们之前创建的新建变量                     processor.InsertBefore(insertPoint, processor.Create(OpCodes.Stloc, injection));                     //创建并插入IL,压入变量体中的值到栈                     processor.InsertBefore(insertPoint, processor.Create(OpCodes.Ldloc, injection));                 } 				//创建跳转语句,为false时候直接跳转insertPoint,                 //这边OpCodes.Brfalse看起来是布尔值判断,其实也会判断是否为null                 var jmpInstruction = processor.Create(OpCodes.Brfalse, insertPoint);                 processor.InsertBefore(insertPoint, jmpInstruction);                  if (isIntKey)                 { 					//创建当前指令参数数据源并后续调用                     processor.InsertBefore(insertPoint, processor.Create(OpCodes.Ldc_I4, bridgeIndexByKey.Count)); 					//创建委托函数对象                     processor.InsertBefore(insertPoint, processor.Create(OpCodes.Call, delegateBridgeGetter));                 }                 else                 { 					//创建并插入IL,再次压入变量的值,因为上面做完判断后,栈顶的值就会被弹出,所以这边要再次压入                     processor.InsertBefore(insertPoint, processor.Create(OpCodes.Ldloc, injection));                 } 				//成员函数比静态函数多了个参数,即自身,这步是压栈参数个数                 for (int i = 0; i < param_count; i++)                 {                     if (i < ldargs.Length)                     {                         processor.InsertBefore(insertPoint, processor.Create(ldargs[i]));                     }                     else if (i < 256)                     {                         processor.InsertBefore(insertPoint, processor.Create(OpCodes.Ldarg_S, (byte)i));                     }                     else                     {                         processor.InsertBefore(insertPoint, processor.Create(OpCodes.Ldarg, (short)i));                     }                     if (i == 0 && !method.IsStatic && type.IsValueType)                     {                         processor.InsertBefore(insertPoint, processor.Create(OpCodes.Ldobj, type));                                              } 					 //对值类型进行Box                     if (ignoreValueType)                     {                         TypeReference paramType;                         if (method.IsStatic)                         {                             paramType = method.Parameters[i].ParameterType;                         }                         else                         {                             paramType = (i == 0) ? type : method.Parameters[i - 1].ParameterType;                         }                         if (paramType.IsValueType)                         {                             processor.InsertBefore(insertPoint, processor.Create(OpCodes.Box, paramType));                         }                     }                 } 				//创建并插入IL,调用invoke方法,因为之前已经压入injection的值,DelegateBridge的对象                 processor.InsertBefore(insertPoint, processor.Create(OpCodes.Call, invoke)); 				//如果不是结构体,或者isFinalize,从当前方法返回,并将返回值(如果存在)从调用方的计算堆栈推送到被调用方的计算堆栈上。                 if (!method.IsConstructor && !isFinalize)                 {                     processor.InsertBefore(insertPoint, processor.Create(OpCodes.Ret));                 } 				                 if (!method.IsConstructor)                 {                     break;                 }                 else                 { 					//普通方法,加入返回操作                     originToNewTarget[insertPoint] = firstInstruction;                     noCheck.Add(jmpInstruction);                 } 				//寻找下一个插入位置                 insertPoint = findNextRet(method.Body.Instructions, insertPoint);             } 			//结构体的处理             if (method.IsConstructor)             {                 fixBranch(processor, method.Body.Instructions, originToNewTarget, noCheck);             } 			//isFinalize的处理             if (isFinalize)             {                 if (method.Body.ExceptionHandlers.Count == 0)                 {                     throw new InvalidProgramException("Finalize has not try-catch? Type :" + method.DeclaringType);                 }                 method.Body.ExceptionHandlers[0].TryStart = method.Body.Instructions[0];             }             if (isIntKey)             {                 bridgeIndexByKey.Add(method);             }             return true;         }

     

参考:

xlua hotfix分析

https://zhuanlan.zhihu.com/p/68907610/

OpCodes指令

https://www.cnblogs.com/chenxiaoran/archive/2012/11/19/2776807.html

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!