当前位置: 首页 > news >正文

Xlua原理分析 四

前面已经介绍了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 + 1if (self.tick % 50) == 0 thenprint('<<<<<<<<Update in lua, tick = ' .. self.tick)self:TestHotFixLog('lua')endend)");}}

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

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

看看DelegateBridge 的结构:

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

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

xlua.hotfix = function(cs, field, func)if func == nil then func = false endlocal tbl = (type(field) == 'table') and field or {[field] = func}for k, v in pairs(tbl) dolocal cflag = ''if k == '.ctor' thencflag = '_c'k = 'ctor'endlocal f = type(v) == 'function' and v or nilxlua.access(cs, cflag .. '__Hotfix0_'..k, f) -- at least onepcall(function()for i = 1, 99 doxlua.access(cs, cflag .. '__Hotfix'..i..'_'..k, f)endend)endxlua.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)endxlua.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_GENERALinvoke = injectAssembly.MainModule.ImportReference(invoke);
#elseinvoke = 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名字就是上面取到的luaDelegateNameFieldDefinition 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看起来是布尔值判断,其实也会判断是否为nullvar 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));}//对值类型进行Boxif (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

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 虚拟机ubuntu22.04找不到ttyUSB*端口
  • Windows系统之环境变量
  • Lumos学习王佩丰Excel第十讲:Sumif函数
  • .NET未来路在何方?
  • ei会议论文是什么级别
  • 登录相关功能的优化【JWT令牌+拦截器+跨域】
  • 研0 冲刺算法竞赛 day27 P1090 [NOIP2004 提高组] 合并果子 / [USACO06NOV] Fence Repair G
  • linux 进程 inode 信息获取
  • Java 面试常见问题之——final,finalize 和 finally 的不同之处
  • Java IO与NIO的对比与高级用法
  • python-打分(赛氪OJ)
  • 书生大模型实战营第三期——入门岛——Git基础知识
  • 【Android】四大组件(Activity、Service、Broadcast Receiver、Content Provider)、结构目录
  • DataX迁移数据到StarRocks超大表报too many version问题记录
  • 深度学习入门(二):常见概念(重点:泛化误差)
  • CSS相对定位
  • es6--symbol
  • JavaScript HTML DOM
  • Kibana配置logstash,报表一体化
  • laravel5.5 视图共享数据
  • MySQL数据库运维之数据恢复
  • oldjun 检测网站的经验
  • Python_OOP
  • Python学习之路13-记分
  • SpriteKit 技巧之添加背景图片
  • 给新手的新浪微博 SDK 集成教程【一】
  • 互联网大裁员:Java程序员失工作,焉知不能进ali?
  • 基于Vue2全家桶的移动端AppDEMO实现
  • 开发了一款写作软件(OSX,Windows),附带Electron开发指南
  • 聊聊redis的数据结构的应用
  • 七牛云假注销小指南
  • 前端之Sass/Scss实战笔记
  • 设计模式(12)迭代器模式(讲解+应用)
  • 要让cordova项目适配iphoneX + ios11.4,总共要几步?三步
  • 06-01 点餐小程序前台界面搭建
  • Java性能优化之JVM GC(垃圾回收机制)
  • 选择阿里云数据库HBase版十大理由
  • ​ 轻量应用服务器:亚马逊云科技打造全球领先的云计算解决方案
  • ​无人机石油管道巡检方案新亮点:灵活准确又高效
  • ‌U盘闪一下就没了?‌如何有效恢复数据
  • # Swust 12th acm 邀请赛# [ E ] 01 String [题解]
  • #HarmonyOS:基础语法
  • #vue3 实现前端下载excel文件模板功能
  • #我与Java虚拟机的故事#连载09:面试大厂逃不过的JVM
  • (4)事件处理——(7)简单事件(Simple events)
  • (delphi11最新学习资料) Object Pascal 学习笔记---第13章第6节 (嵌套的Finally代码块)
  • (pojstep1.1.2)2654(直叙式模拟)
  • (附源码)ssm教材管理系统 毕业设计 011229
  • (附源码)计算机毕业设计ssm电影分享网站
  • (一)认识微服务
  • (原創) 博客園正式支援VHDL語法著色功能 (SOC) (VHDL)
  • (转)c++ std::pair 与 std::make
  • (转)清华学霸演讲稿:永远不要说你已经尽力了
  • .axf 转化 .bin文件 的方法
  • .NET Core Web APi类库如何内嵌运行?