Xlua原理分析 四

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

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

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

一、如何进行Hotfix

先上测试代码:

cs 复制代码
    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都干了什么事。

cs 复制代码
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的实现:

cs 复制代码
[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。所以可以执行原函数

cs 复制代码
--和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对象。

实现如下:

cs 复制代码
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

相关推荐
躺下睡觉~10 小时前
Unity-Transform类-父子关系
java·unity·游戏引擎
躺下睡觉~10 小时前
Unity-Transform类-缩放和看向
unity·游戏引擎
君莫愁。12 小时前
【Unity】检测鼠标点击位置是否有2D对象
unity·c#·游戏引擎
咩咩觉主13 小时前
Unity实战案例全解析:PVZ 植物卡片状态分析
unity·c#·游戏引擎
蓝裕安16 小时前
伪工厂模式制造敌人
开发语言·unity·游戏引擎
谢泽浩20 小时前
Unity 给模型贴上照片
unity·游戏引擎
z2014z20 小时前
Unity Resource System 优化笔记
unity·游戏引擎
王维志20 小时前
Unity 高亮插件HighlightPlus介绍
unity·游戏引擎
zaizai100721 小时前
我的demo保卫萝卜中的技术要点
unity
菌菌巧乐兹1 天前
Unity 百度AI实现无绿幕拍照抠像功能(详解版)
人工智能·百度·unity