腾讯开源的 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)。
- 原理:编译期注入 + 运行时解释执行。
- 注入阶段(Inject):打包前用
IFix.exe(基于 Mono.Cecil)读[Configure]配置,对[IFix]属性里列出的所有类的所有方法,在 IL 里注入一段重定向桩(函数入口被改成跳转到虚拟机的CallVirtualMachine)。这个"未打补丁 → 走原逻辑"的表叫 IDMap,会打进主包。 - 补丁阶段(Fix/Patch):改好 bug 后再次跑
IFix.exe,对比 修改后的程序集 vs 原始注入后程序集,把发生变化的函数反编译成自定义 IL(IFix.Core.Instruction指令集),打包成.patch。 - 运行时加载:
PatchManager.Load(stream)读补丁,把补丁指令挂到虚拟机上;被修复函数执行时注入桩发现 IDMap 里有新版本 → 走解释执行,不命中 → 走原逻辑。
- 注入阶段(Inject):打包前用
- 优点:老项目无需改代码、支持 Unity 全系列全平台(Mono + IL2CPP 均可)、补丁格式私有(
INSTRUCTION_FORMAT_MAGIC校验)。 - 与 Xlua / ILRuntime 区别:不是把代码搬到 Lua/虚拟语言,而是原生 C# 方法原地被解释执行;补丁里是字节码指令而非 Lua 代码,热更代码和主工程代码完全同源(同一个
.cs文件用[IFix.Patch]/[IFix.Interpret]标注)。
二、接入(安装)步骤
官方 Source/UnityProj/ 对应一个 Unity 工程目录:
- 编译 IFix 工具(仅 Windows,mac 需自行用 mcs/mono 编译或直接复用现成 exe):
Source/VSProj/build_for_unity.bat,把UNITY_HOME改成 Unity 安装目录,运行。 - 拷贝到 Unity 工程:
IFixToolKit/(内含IFix.exe、Mono.Cecil*.dll)→ Unity 工程的 Assets 同级目录(本项目在仓库根IFix/IFixToolKit/)。Assets/IFix、Assets/Plugins(内含IFix.Core.dll运行时)→ 工程Assets/下。本项目运行时库在Assets/Scripts/Hotfix/Plugins/IFix.Core.dll(Mono 用)+Assets/Plugins/(Android/iOS 平台化版本)。
- 写
[Configure]注入配置(必须放 Editor 目录)。 - 注入 & 出包:正常出包前跑一次注入(
IFix.Editor.IFixEditor.InjectAll),把注入桩编进主程序集。 - 打补丁:改代码 → 给要修的已有方法加
[IFix.Patch]/ 给要新增的东西加[IFix.Interpret]→ 菜单生成.patch.bytes→ 走发布管线。 - 运行时加载:
xxxPatch.Load(...)。
Unity 版本要求:官方支持 Unity 全系列。Unity 2018.3+ 直接用菜单
IFix/Inject、IFix/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(小写,框架按名字反射)。注释掉的KongWebView、CharacterTool.Runtime反射段已随对应程序集停用而删除。 - 好处:自动收集 interface + delegate,避免手工逐个维护
bridge列表;代价:程序集里任何新 interface/delegate 都会被兜进 bridge,注入产物略大。 - 菜单
InjectFix/✡ Print Custom Bridges ✡可打印当前收集到的全部桥接类型,排查"新增类实现原生接口报错"时先看这个。
四、边界与限制
4.1 [IFix.Patch](修已有方法)边界
| 能力 | 是否支持 | 备注 |
|---|---|---|
| 普通方法 | ✅ | 基础用法 |
| getter / setter | ✅ | 对 TestProperty { [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 / await:
async Task、.NET 4.x async注入阶段报异常(VM 不支持AsyncStateMachine)。→ 别把异步方法塞进 IFix 热更。 - 不支持的 IL 指令集合:
Calli(Instruction.cs里被注释掉)、Jmp、Cpblk/Initblk(块拷贝)、Localloc、Tail(尾调用)等。遇上报not support il的,改写成简单写法规避。 - 结构体初始化必须
new:struct不使用new初始化(直接default/逐字段)→ 报错。按下标取 struct 数组解释执行会报错。 in修饰 struct 参数的虚函数,inject 后执行报错。- Enum 相关:补丁中
Enum.Parse报错;foreach遍历含枚举的容器报错。 IEnumerator<T>的 Current 在解释执行下报错(见 4.2 协程边界)。- 方法体里不能用
typeof()(新增类中):IFix.Interpret新增类内不能用到typeof。 - 不能 Patch 返回"协程衍生类"(非
IEnumerator本身)的函数;Invoke(MethodInfo.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.Core的CallVirtualMachine入口才能编进去;打包后再注入无效。 - iOS armv7 注入方法过多链接报错(新机基本 arm64,影响小)。
- 手动编译
IFix.Core.dll后导出 xcode 工程报IFix.Core.EvaluationStackOperation::ToObject错 → 用build_for_unity.bat重新构建,别手动编。
- 主包必须带注入:打包时执行 inject,
- 补丁平台模板: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 找不到
gmcs(Editor\Data\Mono\bin\gmcs路径差异)、2019.3 安卓注入失败等老 Unity 版本的坑,本项目 Unity 2022.3 已避开。 IFix.Core.dll与IFix.exe要配套(同一版本);找不到IFix.Core.dll、Instruction.cs多版本不一致(Code枚举顺序变了 IDMap 就对不上)都是升级时的坑。- 升级 Unity 或改条件编译宏后,平台模板、注入 IDMap 都要重新生成。
五、注意事项 / 运维红线
- 补丁只进不出:
PatchManager有Unload,但跨版本/多次加载同一方法会叠加;每次发版必须带新版本的 IDMap(instruction magic not match就是新旧不匹配)。不能靠"先卸载再重载"做版本回退,回退要整个包回退。 - 注入桩有开销:
[IFix]覆盖的每个方法入口都跳虚拟机的CallVirtualMachine(即使没打补丁),全量注入会明显增大主程序集 + 首次调用慢。取舍:只注入"可能热更"的类,或用[Filter]排除热点函数(如DamageCalculator这种战斗高频计算,其实应该 Filter 掉,注入它反而拖累性能)。 .meta与二进制:patch 和IFix.Core.dll是二进制产物,git add别-A,按路径 stage。- 热更代码别写:泛型方法、泛型类、构造/析构函数、新增字段、struct、async/await、
typeof(新增类内)、Burst/ECS、Enum.Parse、LINQ 容器含枚举的foreach、数组字面量{...}等(见第四章)。新增字段是最常踩的坑 —— 需求说"加个字段"时,改成改方法体/加静态配置,别指望补丁加字段。 - 编辑器 vs 真机一致性:编辑器模拟(
SimulateInjectFix)和真机加载路径不同,验证热更先在编辑器模拟测,再出 patch 上真机;编辑器不拦截 patch 加载错误,真机出错会卡在ShowNotice重试循环。 - 修改
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.dll | IFix.Core.dll/exe 配套丢失或版本不匹配 |
| 新增类实现原生接口报错 | 接口没进 [IFix.CustomBridge] bridge 列表 |
八、参考
- 官方:github.com/Tencent/Inj… (README / Doc/user_manual.md / Doc/faq.md)