# 前言
之前我们项目,首先是出的台服版本,后来国内版本是专门有组建一个程序团队进行开发维护的。
因此国内版包括打包、热更等都与这边有不少差异。
例如代码热更,台服使用的 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 之外,还要生成一堆绑定代码,通过回调判断。
# 问题
热更时,可能出现执行热更后代码函数卡死,例如:
[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 中插入相关判断,判断有热更,则在自己的虚拟机中执行热更代码实现。