Unity引擎中InjectFix接入使用与边界全梳理

0 阅读11分钟

腾讯开源的 Unity C# 代码热修复框架,官方仓库:github.com/Tencent/Inj… 本文结合官方 README / user_manual / FAQ、以及本项目实际接入代码整理。

一、是什么 / 原理

  • 定位:Unity 业务 C# 层热修复,不用 Xlua/Lua,直接在 C# 工程上改代码即可出补丁。与 XLua 生成器导出 IFix 注入方法导致 Gen 编译报错(XLua 与 IFix 注入在 IL 层的冲突案例)一起构成某游戏项目的热更体系(Lua 走 XLua,纯 C# 逻辑走 InjectFix)。
  • 原理编译期注入 + 运行时解释执行
    1. 注入阶段(Inject):打包前用 IFix.exe(基于 Mono.Cecil)读 [Configure] 配置,对 [IFix] 属性里列出的所有类的所有方法,在 IL 里注入一段重定向桩(函数入口被改成跳转到虚拟机的 CallVirtualMachine)。这个"未打补丁 → 走原逻辑"的表叫 IDMap,会打进主包。
    2. 补丁阶段(Fix/Patch):改好 bug 后再次跑 IFix.exe,对比 修改后的程序集 vs 原始注入后程序集,把发生变化的函数反编译成自定义 IL(IFix.Core.Instruction 指令集),打包成 .patch
    3. 运行时加载PatchManager.Load(stream) 读补丁,把补丁指令挂到虚拟机上;被修复函数执行时注入桩发现 IDMap 里有新版本 → 走解释执行,不命中 → 走原逻辑。
  • 优点:老项目无需改代码、支持 Unity 全系列全平台(Mono + IL2CPP 均可)、补丁格式私有(INSTRUCTION_FORMAT_MAGIC 校验)。
  • 与 Xlua / ILRuntime 区别:不是把代码搬到 Lua/虚拟语言,而是原生 C# 方法原地被解释执行;补丁里是字节码指令而非 Lua 代码,热更代码和主工程代码完全同源(同一个 .cs 文件用 [IFix.Patch]/[IFix.Interpret] 标注)。

二、接入(安装)步骤

官方 Source/UnityProj/ 对应一个 Unity 工程目录:

  1. 编译 IFix 工具(仅 Windows,mac 需自行用 mcs/mono 编译或直接复用现成 exe):Source/VSProj/build_for_unity.bat,把 UNITY_HOME 改成 Unity 安装目录,运行。
  2. 拷贝到 Unity 工程
    • IFixToolKit/(内含 IFix.exeMono.Cecil*.dll)→ Unity 工程的 Assets 同级目录(本项目在仓库根 IFix/IFixToolKit/)。
    • Assets/IFixAssets/Plugins(内含 IFix.Core.dll 运行时)→ 工程 Assets/ 下。本项目运行时库在 Assets/Scripts/Hotfix/Plugins/IFix.Core.dll(Mono 用)+ Assets/Plugins/(Android/iOS 平台化版本)。
  3. [Configure] 注入配置(必须放 Editor 目录)。
  4. 注入 & 出包:正常出包前跑一次注入(IFix.Editor.IFixEditor.InjectAll),把注入桩编进主程序集。
  5. 打补丁:改代码 → 给要修的已有方法[IFix.Patch] / 给要新增的东西加 [IFix.Interpret] → 菜单生成 .patch.bytes → 走发布管线。
  6. 运行时加载xxxPatch.Load(...)

Unity 版本要求:官方支持 Unity 全系列。Unity 2018.3+ 直接用菜单 IFix/InjectIFix/Fix 即可(Unity 开放了 C# 编译接口,patch 可带平台条件宏直接生成);2018.3 以下需要手动用 mcs 按平台编译出 Assembly-CSharp.dll 再调 IFixEditor.GenPatch(见官方 FAQ)。

三、注入配置与标签使用(核心)

3.1 标签总览(官方 user_manual 总结表)

标签阶段用途用法
[Configure]注入配置类只能放单独一个类,必须放 Editor 目录
[IFix]注入列出将来可能修的类集合只能放 [Configure] 类的静态属性
[Filter]注入过滤掉不想注入的函数只能放 [Configure] 类的静态方法
[IFix.Patch]补丁修复已有方法只能放方法
[IFix.Interpret]补丁新增字段/属性/方法/类可放字段、属性、方法、类型上
[IFix.CustomBridge]注入把 VM 类适配到原生 interface / VM 函数适配到原生 delegate只能放单独静态类不能放 Editor 目录,不能内嵌别的类

3.2 [Configure] 注入配置(本项目实例)

项目里生效的配置在 xxxHotfix/Editor/

// ScriptsCfg.cs —— Scripts 程序集全量注入(凡是 namespace != null 的都注入桩)
[Configure]
public class ScriptsCfg
{
    [IFix]
    static IEnumerable<Type> hotfix
    {
        get
        {
            return (from type in Assembly.Load("Scripts").GetTypes()
                    where type.Namespace != null select type).ToList();
        }
    }
}
  • 用法建议:被 [IFix] 覆盖的所有方法都会注入跳转桩,是有包体和性能开销的,官方建议只列"可能出问题"的类;本项目是全量注入(游戏逻辑代码量大、需要热更的几乎都能热更),代价是主程序集膨胀 + 注入耗时。

3.3 [IFix.Patch] —— 修复已有方法

  • 前提:该方法所在的类必须已被 [IFix] 注入。
  • 加在方法上,改完方法体,生成的补丁会修正该函数:
// 修复前
public int Add(int a, int b) { return a * b; }   // 有 bug

// 修复后:打开 [Patch] 注释
[IFix.Patch]
public int Add(int a, int b) { return a + b; }    // 正确

3.4 [IFix.Interpret] —— 新增代码

补丁阶段新增字段/属性/方法/类,直接打标即可:

[IFix.Interpret]
public class NewClass { ... }

[IFix.Interpret]
public int intValue = 0;              // 新增字段

[IFix.Interpret]
public IEnumerator TestInterface()    // 新增协程(有限制,见 4.2)
{
    yield return new WaitForSeconds(1);
}

3.5 [IFix.CustomBridge] —— interface / delegate 桥接(关键边界)

什么时候必须加(官方列出的场景):

  • 修复代码给一个 delegate 变量赋值闭包;
  • 修复代码(或新增代码)的协程用了 yield return
  • 新增类赋值到原生 interface 变量;
  • 新增函数用到 yield return

要求:写成一个独立静态类,静态字段 bridge 放 interface 和 delegate 的 Type 集合;不能放 Editor 目录、不能内嵌其他类。

本项目实例 xxx/IFix/InjectFixCustomBridge.cs

[CustomBridge]
public static class InjectFixCustomBridge
{
    static List<Type> bridge = new List<Type>();

    static readonly List<Type> DefaultTypes = new List<Type>
    {
        typeof(IEnumerator),
        typeof(ISubsystem)
    };

    static InjectFixCustomBridge()
    {
        UpdateAllReferences();
    }
}
  • 注意:官方属性名是 bridge(小写,框架按名字反射)。注释掉的 KongWebViewCharacterTool.Runtime 反射段已随对应程序集停用而删除。
  • 好处:自动收集 interface + delegate,避免手工逐个维护 bridge 列表;代价:程序集里任何新 interface/delegate 都会被兜进 bridge,注入产物略大。
  • 菜单 InjectFix/✡ Print Custom Bridges ✡ 可打印当前收集到的全部桥接类型,排查"新增类实现原生接口报错"时先看这个。

四、边界与限制

4.1 [IFix.Patch](修已有方法)边界

能力是否支持备注
普通方法基础用法
getter / setterTestProperty { [IFix.Patch] get/set } 有效(成员变量不支持,但访问器可 patch)
普通协程yield return 正常
方法内调用泛型方法在 Patch 方法体内 InnerGenericMethod<string>(...) 可用
泛型方法本身[IFix.Patch] public void GenericMethod<T>(T t) → 编辑器报错;带 out 泛型参数同样不生效
构造函数[IFix.Patch] public Calculator() 不行(private 构造函数同样无法 patch)
字段不能 Patch 字段;原生类中新增字段也不行(需用 [Interpret],但见 4.2)

4.2 [IFix.Interpret](新增代码)边界

能力是否支持备注
新增普通方法 / 属性 / 类
新增类继承新增类PatchChildClass : PatchBaseClass(两者都 [Interpret])
新增类实现原生接口必须配合 [IFix.CustomBridge] 把接口加进 bridge
新增类继承原生类[Interpret] class PatchClassInheritClass : TestClass 不支持
新增泛型类
泛型方法[Interpret] public void PatchGenericMethod<T>(T t) 不行
字段官方 user_manual 里写明 [Interpret] 可放字段,但 CSDN 实测 新增字段不可用(issue:InjectFix 如何新增字段)。⚠️ 本项目也遵循此边界:能改方法体、不能靠补丁加字段。
构造函数
Struct(结构体)新增 struct 类型不支持
协程⚠️ 有限制见下

协程的坑

// 非热更代码里已用过 IEnumerator → 热更代码里才可以用
private IEnumerator IE_Main() { yield return new WaitForEndOfFrame(); }

[IFix.Interpret]
private IEnumerator IE_Patch()   // ✅ 编译 OK
{
    yield return new WaitForSeconds(1);
    yield return new WaitForEndOfFrame();
    yield return 0;
    yield return null;
    yield break;
}

[IFix.Interpret]
public IEnumerator IE_Patch2()   // ❌ 非热更代码没用过 IEnumerator 时,报错
{
    yield return ...
}

原因:yield 会生成状态机类 IE_Patch(CompilerGenerated),本质是一个新增类;而新增类不能用泛型,状态机又是 IEnumerator<T>/泛型结构,所以只有"非热更代码里已经存在对应的 IEnumerator 类型,VM 能复用"时才可行。

4.3 泛型 / IL 相关深层边界

  • 泛型是最大禁区:Patch 不支持泛型方法、Interpret 不支持泛型方法/泛型类;Stelem_Any 等依赖泛型解析的 IL 指令在 VM 里基本不会走到(官方注释 //case Code.Stelem_Any: //泛型不支持解析)。多个 issue 报过"子类重写时先执行带泛型的方法会出错"。
  • new int[]{1,2,3} 数组字面量:生成 ldtoken <PrivateImplementationDetails> 指令,官方 warning:not support il[IL_0008: ldtoken ...]
  • 不支持 async / awaitasync Task.NET 4.x async 注入阶段报异常(VM 不支持 AsyncStateMachine)。→ 别把异步方法塞进 IFix 热更。
  • 不支持的 IL 指令集合CalliInstruction.cs 里被注释掉)、JmpCpblk/Initblk(块拷贝)、LocallocTail(尾调用)等。遇上报 not support il 的,改写成简单写法规避。
  • 结构体初始化必须 newstruct 不使用 new 初始化(直接 default/逐字段)→ 报错。按下标取 struct 数组解释执行会报错。
  • in 修饰 struct 参数的虚函数,inject 后执行报错。
  • Enum 相关:补丁中 Enum.Parse 报错;foreach 遍历含枚举的容器报错。
  • IEnumerator<T> 的 Current 在解释执行下报错(见 4.2 协程边界)。
  • 方法体里不能用 typeof()(新增类中)IFix.Interpret 新增类内不能用到 typeof
  • 不能 Patch 返回"协程衍生类"(非 IEnumerator 本身)的函数InvokeMethodInfo.Invoke)在解释执行下 Non-static method requires a target 报错(TargetException),新方法调用新方法同理注意。
  • DateTime.Now.AddSeconds 打补丁失败Debug.unityLogger.logEnabled 无法访问(纯 Unity API 访问边界)。
  • BurstCompile 类/结构体不能注入:加载 patch 后报 burst 错误(ECS/Jobs 代码不要走 IFix)。
  • [IFix] 里 ifix 无 using System.Linq 的 linq 配置会在编辑器初始化报错(项目里 ScriptsCfg 等都 using System.Linq 规避)。

4.4 与 IL2CPP / 打包相关

  • 支持 IL2CPP(Android/iOS 都可热更),但要注意时机:IL2CPP 会把注入桩编译进 native 代码,所以:
    • 主包必须带注入:打包时执行 inject,IFix.CoreCallVirtualMachine 入口才能编进去;打包后再注入无效。
    • iOS armv7 注入方法过多链接报错(新机基本 arm64,影响小)。
    • 手动编译 IFix.Core.dll 后导出 xcode 工程报 IFix.Core.EvaluationStackOperation::ToObject 错 → 用 build_for_unity.bat 重新构建,别手动编。
  • 补丁平台模板:2018.3 以下平台 patch 需要 IFixToolKit 里的 android.win.tpl / android.osx.tpl / ios.osx.tpl / ios.win.tpl(从一次正常平台构建的 Temp/UnityTempFile* 拷贝改名)。报错 please put template file for android/ios in IFixToolKit directory 即缺模板。本项目 2022.3 不受影响

4.5 版本 / 兼容

  • 官方 issue 统计里出现过 2019.2.15 打包未注入2019.3.x 找不到 gmcsEditor\Data\Mono\bin\gmcs 路径差异)、2019.3 安卓注入失败等老 Unity 版本的坑,本项目 Unity 2022.3 已避开。
  • IFix.Core.dllIFix.exe配套(同一版本);找不到 IFix.Core.dllInstruction.cs 多版本不一致(Code 枚举顺序变了 IDMap 就对不上)都是升级时的坑。
  • 升级 Unity 或改条件编译宏后,平台模板、注入 IDMap 都要重新生成。

五、注意事项 / 运维红线

  1. 补丁只进不出PatchManagerUnload,但跨版本/多次加载同一方法会叠加;每次发版必须带新版本的 IDMapinstruction magic not match 就是新旧不匹配)。不能靠"先卸载再重载"做版本回退,回退要整个包回退。
  2. 注入桩有开销[IFix] 覆盖的每个方法入口都跳虚拟机的 CallVirtualMachine(即使没打补丁),全量注入会明显增大主程序集 + 首次调用慢。取舍:只注入"可能热更"的类,或用 [Filter] 排除热点函数(如 DamageCalculator 这种战斗高频计算,其实应该 Filter 掉,注入它反而拖累性能)。
  3. .meta 与二进制:patch 和 IFix.Core.dll 是二进制产物,git add-A,按路径 stage。
  4. 热更代码别写:泛型方法、泛型类、构造/析构函数、新增字段、struct、async/await、typeof(新增类内)、Burst/ECS、Enum.Parse、LINQ 容器含枚举的 foreach、数组字面量 {...} 等(见第四章)。新增字段是最常踩的坑 —— 需求说"加个字段"时,改成改方法体/加静态配置,别指望补丁加字段。
  5. 编辑器 vs 真机一致性:编辑器模拟(SimulateInjectFix)和真机加载路径不同,验证热更先在编辑器模拟测,再出 patch 上真机;编辑器不拦截 patch 加载错误,真机出错会卡在 ShowNotice 重试循环。
  6. 修改 IFix.Core 源码需用 build_for_unity.bat 重建,且 DLL/exe 配套升级,同时重生成 IDMap;本项目直接复用官方二进制。

七、故障速查

现象原因/处理
instruction magic not match补丁与主包 IDMap 不匹配 → 换同版本补丁/重新出包
Error: the new assembly must not be inject, please reimport the project!拿注入后的 dll 生成 patch → 工程根目录右键 Reimport
please put template file for android/ios in IFixToolKit directory缺平台模板 tpl(2018.3 以下才需要)
not support il[IL_xxxx: ldtoken ...]数组字面量/不支持的指令 → 改写(如换成 new List<int>{...} 或逐个赋值)
patch 泛型方法/类、构造、字段报错越界了,改设计
编辑器能热更、apk 不行包没带注入桩 / 补丁路径版本不对
Non-static method requires a target解释执行下 MethodInfo.Invoke 等反射调用不支持
补丁里调 Enum.Parse / 含枚举 foreach 报错换实现
IFix.Core.EvaluationStackOperation::ToObject IL2CPP 报错IFix.Core 手动编译导致 → 用 build_for_unity.bat 重编
找不到 IFix.Core.dllIFix.Core.dll/exe 配套丢失或版本不匹配
新增类实现原生接口报错接口没进 [IFix.CustomBridge] bridge 列表

八、参考