XLua热修复机制详解
深入理解 XLua 热修复机制:从反射到 IL 注入
本文从一个 Unity 客户端开发者的视角,系统梳理 XLua 热修复(Hotfix)的核心原理,涵盖反射、Wrap 代码生成、IL 注入、IL2CPP 兼容性等关键知识点。适合有一定 Unity + Lua 经验、想深入理解底层机制的开发者。
目录
1. XLua 热修复的整体流程
XLua 的热修复能力,允许你在不发版的情况下,通过下发 Lua 脚本来替换或修补 C# 方法的实现。
整体流程分为两条独立的管线:
管线 A:代码生成(Generate Code) 管线 B:热修复注入(Hotfix Inject)
读取 [LuaCallCSharp] 标注 读取 [Hotfix] 标注
│ │
生成 Wrap 桥接代码 修改 DLL 的 IL 字节码
(让 Lua 能高效调用 C#) (让方法支持被 Lua 替换)
│ │
影响:Lua 调 C# 的性能 影响:能不能 hotfix 这个方法
典型操作顺序:
添加 Hotfix 宏 → 编译 → Generate Code → 编译 → Hotfix Inject → 运行
2. 四个关键操作详解
2.1 添加 Hotfix 宏
在项目的编译宏(Scripting Define Symbols)中添加 HOTFIX_ENABLE。
作用: 这是一个编译开关。只有定义了这个宏,XLua 框架中与 Hotfix 相关的条件编译代码(#if HOTFIX_ENABLE)才会参与编译。
// 以下代码只在 HOTFIX_ENABLE 宏存在时才编译
#if HOTFIX_ENABLE
// Hotfix 相关的委托桥接逻辑
#endif
2.2 Generate Code(生成 Wrap 代码)
根据代码中标注的 [LuaCallCSharp]、[CSharpCallLua] 等特性,自动生成 Lua 与 C# 之间的桥接代码(Wrap),输出到 Assets/XLua/Gen/ 目录。
生成内容包括:
- 类 Wrap 代码 — 每个标注类生成一个
xxxWrap.cs - 委托桥接 — Lua 调用 C# 委托的适配层
- 枚举包装 — Lua 访问 C# 枚举
- 自动注册代码 —
XLuaGenAutoRegister.cs
2.3 Clear Generated Code(清除生成代码)
递归删除 Assets/XLua/Gen/ 整个目录。一般在重新生成之前或遇到编译报错时使用。
2.4 Hotfix Inject In Editor(IL 注入)
对已编译的 C# DLL(Assembly-CSharp.dll)进行 IL 级代码注入。这是热修复的核心步骤,下文详述。
3. Wrap 是什么?为什么需要它?
3.1 问题背景
Lua 和 C# 是两个完全不同的语言运行时,它们不能直接互相调用。需要一个"翻译官":
Lua 代码 Wrap(翻译官) C# 代码
transform:Find("xxx") → TransformWrap._m_Find() → Transform.Find("xxx")
把 Lua 参数转成 C# 参数
把 C# 返回值转回 Lua 值
3.2 Wrap 的核心逻辑
一个典型的 Wrap 文件(简化版):
public class UnityEngine_TransformWrap
{
public static void __Register(RealStatePtr L)
{
// 告诉 Lua:"Transform 有 Find 方法和 position 属性"
Utils.RegisterFunc(L, "Find", _m_Find);
Utils.RegisterFunc(L, "position", _g_get_position, _s_set_position);
}
// Lua 调 transform:Find("xxx") 时执行的 C# 代码
static int _m_Find(RealStatePtr L)
{
Transform self = 从Lua栈取出Transform;
string name = 从Lua栈取出string;
Transform result = self.Find(name); // 调用真正的 C# 方法
把result压入Lua栈;
return 1;
}
}
3.3 为什么不直接用反射?
| 对比项 | 反射 | Wrap |
|---|---|---|
| 性能 | 慢(运行时查方法、参数装箱拆箱) | 快(编译时生成,直接调用) |
| GC 压力 | 大(装箱产生垃圾对象) | 小(直接类型转换) |
| IL2CPP 兼容 | 大部分场景可用 | 100% 可用 |
3.4 没有 Wrap 能不能调?
能! XLua 有回退机制:
Lua 调用 C# 方法
├─ 有 Wrap → 走 Wrap(快)
└─ 没有 Wrap → 走反射(慢,但能用)
Editor 下开发调试不 Generate 也能跑,但发布前建议 Generate 以确保性能和兼容性。
3.5 Wrap 和 Hotfix 的关系
没有直接关系! 它们是两个独立的机制:
| 标注 | 作用 | 生成 Wrap 吗 |
|---|---|---|
[LuaCallCSharp] |
Lua 主动调用 C# 类 | ? |
[CSharpCallLua] |
C# 回调 Lua | ? |
[Hotfix] |
允许 Lua 热修复替换 C# 方法 | ? |
4. IL 注入(打桩)的原理
4.1 什么是打桩
打桩(Stub / Instrumentation)= 在已有代码里插入额外的代码片段,而不改变原来的源码。
类比:在水管上加一个阀门。
打桩前的方法:
入口 ──────────── 原始逻辑 ──────────── 出口
打桩后的方法:
入口 ── [检查桩] ── 原始逻辑 ──────────── 出口
│
└─ 如果有 Lua 替换 → 走 Lua,跳过原始逻辑
4.2 Hotfix Inject 具体做了什么
使用 Mono.Cecil 库(一个 .NET 程序集读写工具),对编译后的 DLL 进行 IL 级别的修改:
// 注入前的 C# 方法(反编译 IL 后的伪代码)
void SetJade()
{
m_Icon.SetActive(true);
m_Label.text = "Hello";
}
// 注入后
void SetJade()
{
// ↓ 这段是 Hotfix Inject 插入的
if (__hotfix_delegate != null)
{
__hotfix_delegate(this);
return;
}
// ↑ 插入结束
m_Icon.SetActive(true);
m_Label.text = "Hello";
}
同时生成一个 hotfix_id_map.lua.txt 映射文件,记录每个方法对应的 delegate ID,Lua 端通过这个 ID 来注册替换函数。
4.3 打桩 ≈ 注入
| 术语 | 英文 | 侧重 |
|---|---|---|
| 打桩 | Stub / Hook / Instrumentation | 插一个"检查点" |
| 注入 | Inject / Patch | 塞入一段逻辑 |
日常交流中基本等价,不用纠结。
5. hotfix 与 hotfix_ex 的区别
5.1 xlua.hotfix — 完全替换
xlua.hotfix(CS.MyClass, 'MyMethod', function(self)
self:MyMethod() -- ? 无限递归!调用的是 Lua 自己
end)
xlua.hotfix 把方法完全替换。此时 self:MyMethod() 调用的就是 Lua 函数自身 → 死循环。
5.2 hotfix_ex — 替换但保留原方法
hotfix_ex(CS.MyClass, 'MyMethod', function(self)
self:MyMethod() -- ? 调用 C# 原方法,不会递归
-- 在原方法基础上追加你的修复逻辑
end)
5.3 hotfix_ex 的实现原理
local function hotfix_ex(cs, field, func)
local function func_after(...)
xlua.hotfix(cs, field, nil) -- ① 临时摘掉 Lua 替换
local ret = {func(...)} -- ② 执行你的 Lua 函数
xlua.hotfix(cs, field, func_after) -- ③ 重新挂上 Lua 替换
return unpack(ret)
end
xlua.hotfix(cs, field, func_after)
end
执行流程图:
外部调用 MyMethod()
→ 进入 func_after
→ ① 摘掉 Lua 替换
→ ② 执行你写的整个 Lua 函数
│ ├─ 前置逻辑(随便写)
│ ├─ self:MyMethod() ← 此时没有 Lua 替换,走 C# 原方法 ?
│ └─ 后置逻辑(随便写)
→ ③ 重新挂上 Lua 替换
→ return
关键理解: ① 摘掉到 ③ 挂回来之间,你的整个 Lua 函数都在"没有 Lua 替换"的状态下运行。所以不管 self:MyMethod() 放在函数的开头、中间还是结尾,都不会递归。
5.4 增强版 hotfix_ex
实际项目中通常会用增强版,额外包裹一层 xpcall 错误捕获:
local function hotfix_ex_enhanced(cs, field, func)
local function func_after(...)
xlua.hotfix(cs, field, nil)
local ok, ret = xpcall(function() return {func(...)} end, error_handler)
xlua.hotfix(cs, field, func_after)
if ok then return unpack(ret) end
end
xlua.hotfix(cs, field, func_after)
end
好处:如果 Lua 补丁代码出错,不会导致整个方法崩溃,还能输出详细的错误日志便于定位问题。
6. IL2CPP 下的反射真相
6.1 常见误解
"IL2CPP 不支持反射"
这句话不准确。 更准确的说法是:
"IL2CPP 不支持运行时动态生成代码(JIT / Reflection.Emit),但支持对已有代码的反射调用。"
6.2 反射的本质
编译器在生成 DLL 时,不光生成了可执行代码,还打包了元数据(类名、方法名、参数类型等信息)。
正常调用:编译时就确定了调谁
foo.Bar(); → 直接生成 call 指令
反射调用:运行时查元数据,再调用
typeof(Foo).GetMethod("Bar").Invoke(obj, args);
→ 查元数据找到方法 → 找到代码地址 → 执行
反射的前提:元数据和代码都得在。
6.3 IL2CPP 能做什么,不能做什么
? IL2CPP 能做(反射已有代码):
typeof(MyClass).GetMethod("MyMethod") → 查元数据,编译时就有
methodInfo.Invoke(obj, args) → 调用已编译的 C++ 函数
? IL2CPP 不能做(动态生成新代码):
Reflection.Emit 动态创建新类/方法 → 需要 JIT 编译,IL2CPP 没有
DynamicMethod / Expression.Compile → 同上
6.4 为什么 Mono 可以?
Mono (JIT 模式):
运行时动态生成 IL 代码
→ Mono 的 JIT 编译器将 IL 编译成机器码
→ 机器码在内存中,可以执行
→ 反射能找到、能调用 ?
IL2CPP (AOT 模式):
运行时动态生成 IL 代码
→ 需要编译成机器码...
→ 但 IL2CPP 没有运行时编译器
→ 无法编译 → ?
核心区别:Mono 自带运行时编译器(JIT),IL2CPP 不带。
6.5 代码裁剪(Code Stripping)的影响
IL2CPP 有代码裁剪优化:如果某个类/方法在 C# 中没有被直接引用,可能被裁掉。被裁掉后反射就找不到了。
解决方案:
link.xml— 声明"不要裁剪这些类"[Preserve]属性 — 标注在类上防止裁剪- 降低裁剪级别 — Unity 设置中调整 Managed Stripping Level
6.6 Lua Hotfix 在 IL2CPP 下为什么能工作?
hotfix_ex 里的 self:MyMethod()
→ 走 IL 注入的钩子(打桩代码)
→ 不走反射,不走 Wrap
→ IL2CPP 下完全没问题 ?
hotfix_ex 里跨类调用 CS.OtherClass.Method()
→ 有 Wrap → 走 Wrap ?
→ 没有 Wrap → 走反射
→ 该类在编译时存在且未被裁剪 → 能用 ?
→ 该类被裁剪了 → 找不到 ?
7. 常见问题 FAQ
Q: 我在 Editor 下不 Generate Code 也能跑 Lua,为什么?
Editor 下 XLua 会自动走反射调用 C# 方法。Wrap 只是性能优化,Editor 下不是必须的。
Q: [Hotfix] 标注的类会生成 Wrap 吗?
不会。[Hotfix] 和 [LuaCallCSharp] 是两条独立的管线。Hotfix 走 IL 注入,不需要 Wrap。
Q: hotfix_ex 中 self:方法() 放在函数中间会递归吗?
不会。从"摘掉 Lua 替换"到"重新挂上"之间,你的整个 Lua 函数都在无替换状态下运行,self:方法() 始终走 C# 原方法。
Q: 什么时候必须加 [LuaCallCSharp]?
当你需要在 Lua 中主动调用某个 C# 类(不是通过 hotfix 的 self),且目标环境是 IL2CPP 时,加上更稳妥。不过如果类未被裁剪,反射也大概率能用。
Q: IL2CPP 真的不能反射吗?
能反射已有代码,不能反射运行时动态生成的代码。日常业务开发中,反射调用自己的业务类完全没问题。
总结
XLua 热修复知识体系
┌─────────────────────────────────────────────┐
│ Lua 热修复 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ Wrap 生成 │ │ IL 注入 │ │ 反射回退 │ │
│ │[LuaCallC#]│ │ [Hotfix] │ │ (无Wrap时) │ │
│ │ 性能优化 │ │ 方法打桩 │ │ 查元数据调用 │ │
│ └──────────┘ └──────────┘ └─────────────┘ │
│ │
│ hotfix:完全替换(会递归) │
│ hotfix_ex:摘掉→执行→挂回来(不递归) │
│ │
│ IL2CPP:支持反射已有代码 ? │
│ 不支持运行时生成新代码 ? │
└─────────────────────────────────────────────┘
希望这篇文章能帮你建立起 XLua 热修复的完整知识框架。理解了这些底层原理,写 Hotfix 补丁时就会更加得心应手。
作者注:本文基于 XLua 框架的通用机制编写,不涉及具体项目实现细节。不同项目的 hotfix_ex 实现可能略有差异,但核心原理一致。

浙公网安备 33010602011771号