# 前言

之前我们项目,首先是出的台服版本,后来国内版本是专门有组建一个程序团队进行开发维护的。

因此国内版包括打包、热更等都与这边有不少差异。

例如代码热更,台服使用的 XLua ,而国内版则用的 InjectFix。

我开始接手项目之后,前些天在国内版进行了几次代码热更后,发现 InjectFix 是真的方便,不像 XLua,想热更代码还得把原本整个 C# 写的方法全翻译成 Lua 才行。

于是就有将台服版也改成 InjectFix 的计划。

其实,由于国服本来就已经在使用 InjectFix 这个例子,因此直接将国服热更方式 “挪” 过来,也不是很困难。
关键在于『风险』,于是在实际动手之前,花了两天时间简单了解其原理,然后又动手操作了一下,也有个概念。不然如果不明白这个热更是怎么整出来的,也不敢随便用。

最后斟酌了一番,感觉更换还是有一定需要的,毕竟现在台服及其衍生版本,都是采用 XLua,每次涉及到需要代码热更的地方,除了不得已的情况,很多时候都是直接采取『不解决』的处理方式。

而『不得已』的热更情况,则经常代码量和也比较麻烦,用 XLua 重写整个方法,往往需需要浪费大量时间。

# 原理

根据相关文档说明,其实现方式大概为:

  • 将判断代码注入源代码,判断代码都是一个模式,即调用 IFix.Core.DLL 中的判断方法,传入方法 ID
  • 若判断通过,则使用自己实现的 IL 解释器执行补丁代码 (VirtualMachine.cs)
  • 跳过原代码的执行

# 源代码

首先来看一下源代码:

public class WaiteUiActive : MonoSwitch {
	// Use this for initialization
	void Start () {
		
	}
    protected override void  OnDisable()
    {
        Main.Inst.GlobalEvent.DispatchEvent(eNameUI.WaitUIUnActiveFinsh);
        Destroy(this);
    }
}

源码是很简单的一个脚本,可以说是基本上没有什么操作。

# InjectFix 注入后

// Token: 0x02001B1D RID: 6941
public class WaiteUiActive : MonoSwitch
{
	// Token: 0x0600B6BE RID: 46782 RVA: 0x00572BAC File Offset: 0x00570DAC
	private void Start()
	{
		if (IFix.WrappersManagerImpl.IsPatched(41003))
		{
			IFix.WrappersManagerImpl.GetPatch(41003).__Gen_Wrap_0(this);
			return;
		}
	}
	// Token: 0x0600B6BF RID: 46783 RVA: 0x00572BD0 File Offset: 0x00570DD0
	protected override void OnDisable()
	{
		if (IFix.WrappersManagerImpl.IsPatched(41004))
		{
			IFix.WrappersManagerImpl.GetPatch(41004).__Gen_Wrap_0(this);
			return;
		}
		Main.Inst.GlobalEvent.DispatchEvent(eNameUI.WaitUIUnActiveFinsh, new object[0]);
		Object.Destroy(this);
	}
	// Token: 0x0600B6C1 RID: 46785 RVA: 0x00572C28 File Offset: 0x00570E28
	public void <>iFixBaseProxy_OnDisable()
	{
		base.OnDisable();
	}
}

在被注入后,可以看到每个方法最开始,都被加入一条 if 判断,判断该方法是否存在补丁。

方法 IsPatched 代码:

// Token: 0x0600C686 RID: 50822 RVA: 0x005D6A74 File Offset: 0x005D4C74
		public static bool IsPatched(int id)
		{
			return id < ILFixDynamicMethodWrapper.wrapperArray.Length && ILFixDynamicMethodWrapper.wrapperArray[id] != null;
		}

上面说过,其调用的方法是内部实现了的一个自己的 IL 解释器 virtualMachine ,若有补丁的情况下,则将补丁代码放进自己的解释器执行,并跳过原本代码。

public void __Gen_Wrap_0(object P0)
		{
			Call call = Call.Begin();
			if (this.anonObj != null)
			{
				call.PushObject(this.anonObj);
			}
			call.PushObject(P0);
			this.virtualMachine.Execute(this.methodId, ref call, (this.anonObj != null) ? 2 : 1, 0);
		}

其实 InjectFix 最重要的就是这个 virtualMachine,它负责解释执行 C# 补丁代码,过程

  • 注:这里贴的虽然是反编译的代码,只是由于方便查看注入后的 DLL ,InjectFix 实际上也提供了源码的,在项目 InjectFix\Source\VSProj 路径下的工程。

普通已有方法是如此,还有新增字段之类的操作呢?
这个直接通过反编译是看不出来的,因为新增的在原有 DLL 中本身就没有,是由 IFix CodeTranslator 直接生成指令,热更代码中的指令就可以进行读取。

# XLua 注入情况

经过上边的分析,于是想到 XLua 是不是也是类似方法,于是后面又看了下 XLua 的反编译:

// Token: 0x02001B0A RID: 6922
public class WaiteUiActive : MonoSwitch
{
	// Token: 0x0600ABE0 RID: 44000 RVA: 0x004AA5C0 File Offset: 0x004A87C0
	private void Start()
	{
		__XLua_Gen_Delegate0 _Hotfix0_Start = WaiteUiActive.__Hotfix0_Start;
		if (_Hotfix0_Start != null)
		{
			_Hotfix0_Start(this);
			return;
		}
	}
	// Token: 0x0600ABE1 RID: 44001 RVA: 0x004AA5EC File Offset: 0x004A87EC
	protected override void OnDisable()
	{
		__XLua_Gen_Delegate0 _Hotfix0_OnDisable = WaiteUiActive.__Hotfix0_OnDisable;
		if (_Hotfix0_OnDisable != null)
		{
			_Hotfix0_OnDisable(this);
			return;
		}
		Main.Inst.GlobalEvent.DispatchEvent(eNameUI.WaitUIUnActiveFinsh, new object[0]);
		Object.Destroy(this);
	}
	// Token: 0x0600ABE2 RID: 44002 RVA: 0x004AA638 File Offset: 0x004A8838
	public WaiteUiActive()
	{
		__XLua_Gen_Delegate0 c__Hotfix0_ctor = WaiteUiActive._c__Hotfix0_ctor;
		if (c__Hotfix0_ctor != null)
		{
			c__Hotfix0_ctor(this);
		}
	}
	// Token: 0x0600ABE3 RID: 44003 RVA: 0x004AA668 File Offset: 0x004A8868
	private void <>xLuaBaseProxy_OnDisable()
	{
		base.OnDisable();
	}
	// Token: 0x0400B853 RID: 47187
	private static __XLua_Gen_Delegate0 __Hotfix0_Start;
	// Token: 0x0400B854 RID: 47188
	private static __XLua_Gen_Delegate0 __Hotfix0_OnDisable;
	// Token: 0x0400B855 RID: 47189
	private static __XLua_Gen_Delegate0 _c__Hotfix0_ctor;
}

虽然远离似乎差不多,不过相比 InjectFix 直接注入方法进行判断的方式,XLua 的实现方式要迂回一点,例如除了注入 DLL 之外,还要生成一堆绑定代码,通过回调判断。

# 问题

热更时,可能出现执行热更后代码函数卡死,例如:

s
[IFix.Patch]
    protected override void Initialization()
    {
        base.Initialization();
        Main.Instance.GlobalEvent.AddEvent(eName.InfinityChallengeDataRefresh, OnUpdate);
        Main.Instance.GlobalEvent.AddEvent(eNameUI.InfinityChallengeFlyIcon, FlyIcon);
        Log.Debug("Initialization");
        LOG();
    }

在上述代码中,若热更时代码中也包含 base.Initialization(); ,会导致游戏卡死。

其中,该类继承的父类为夸程序集类。

后经测试,同一个程序集的类继承后,可以调用父类。

出现该问题的原因个人并未找到,猜测可能是编译的指令造成的,在 InjectFix Github Issues 中,似乎有类似问题,但是并未有什么有价值的回复。

# 结语

如果简单来说,InjectFix 和 XLua 热更已有代码,理论上应该都是通过在原有 DLL 中插入相关判断,判断有热更,则在自己的虚拟机中执行热更代码实现。