时效性提示 :本文为 2026-08-28 HybridCLR 官方文档(hybridclr.cn,版本 7.6.0 / 7.8.1 / 8.5.0)、DeepWiki 架构解析、UWA 技术博客及社区实战教程的公开信息快照。HybridCLR 当前社区版免费、商业版(Ultimate)含 DHE 差分混合执行与程序集热重载能力,具体功能边界与性能数据以官方最新公告为准。⚠️ 注意:文中"性能数倍优于 XLua/ILRuntime"等量化数据经 deep-research 对抗验证(3 票制)已被证伪,仅属官方自述基准、不可作为选型硬依据,详见下文 1.7 节。
数据来源 :hybridclr.cn 官方文档(
/docs/basic/methodbridge、/docs/basic/performance、/docs/basic/memory、/docs/business/ultimate/build、/docs/business/reload/manual)、deepwiki.com/focus-creative-games/hybridclr_unity、UWA(blog.uwa4d.com)等一手页面。实战篇:基于某 Unity 项目已落地的 HybridCLR + Addressables 集成,源码见第五节实战篇。
一、HybridCLR 原理:把 IL2CPP 从"纯 AOT"改成"混合运行时"
1.1 一句话定位
HybridCLR(原名 huatuo / wolong,focus-creative-games 出品)是一个全平台原生 C# 热更新方案。它做的事情一句话概括:
在 Unity 的 IL2CPP VM 里内置一个 .NET 字节码解释器,并把 .NET 数据对象映射到 native 数据对象,从而把 IL2CPP 由"纯 AOT 运行时"改造成"AOT + Interpreter 混合运行时",原生支持
System.Reflection.Assembly.Load动态加载 DLL。
这句话里藏着它的全部野心:别的方案(Lua/ILRuntime/puerts)都是独立 VM,热更代码跑在 VM 里、访问 Unity 对象要过一层桥;HybridCLR 直接改 IL2CPP 本体,让热更代码和 AOT 代码共享同一套类型系统和内存布局。所以它才能做到"任何 C# 代码都能热更"、且性能接近原生。
1.2 为什么 IL2CPP 本来做不了热更
Unity 的 IL2CPP 是静态 AOT :编译期把 C# 全部转成 C++ 再编成机器码,且剥离了元数据。两个直接后果:
- 不能动态生成机器码------iOS / 主机 / WebGL 禁止 JIT;
- 元数据没了 ------运行时无法
Assembly.Load加载新类型。
HybridCLR 的破局点就是把"解释执行"塞进 AOT 运行时:热更代码不编译成机器码,而是以 IL 字节码形式打包成 DLL,运行时由内置解释器逐条解释执行,从而绕开"禁止动态生成机器码"的限制。这正是它能在 iOS、Console、WebGL 上做 C# 代码热更的根因。
1.3 三大核心组件
HybridCLR 的核心运行时由四个 C++ 实现的功能块组成(DeepWiki / 官方源码解析):
| 组件 | 作用 | 关键位置 |
|---|---|---|
| DLL 解析库 | 解析热更 DLL 的 PE 头、CLR 头、元数据 | --- |
| 元数据注册 | IL2CPP 是静态 AOT、不支持动态注册元数据,HybridCLR 对它做了少量(约几百行)源码修改,大多只是插入 hook | MetadataCache 扩展 + DynamicModule(内存重建 PE 结构)+ token 映射表 |
| 指令集转换 | 把栈式 IL 指令转成更高效的寄存器式指令 | HiTransform::Transform(transform/Transform.cpp) |
| 寄存器解释器 | 一个大 switch 解释执行寄存器指令 | Interpreter::Execute(interpreter/Interpreter_Execute.cpp) |
最小侵入 是它的设计原则之一:官方称仅需修改 6 个关键函数(如 il2cpp_runtime_class_init()、il2cpp_resolve_icall()、il2cpp_method_get_virtual_method()),采用 DetourHook 函数劫持,核心解释器体积 < 50KB。这也是它能跟得上 Unity 版本迭代、社区接受度高的原因。
1.4 桥接函数(Method Bridge)------AOT 与解释器的边界
这是 HybridCLR 原理里最容易踩坑、也最该理解透的部分。
问题 :解释器部分的参数存在解释器栈 上,AOT 部分的参数存在native 栈上,两者存储/传递方式不同。解释器调用 AOT 函数时,参数没法直接传过去;AOT 回调解释器时,解释器也拿不到参数。
解法 :为每一种签名的函数预生成对应的桥接函数,实现解释器 ↔ AOT 的双向参数传递。桥接主通道三条:
- Managed → Native(解释器调 AOT 方法)
- Native → Managed(AOT 调解释器方法)
- ReversePInvoke(跨域委托)
(注:真实系统实际生成五种桥接------除上述三种外还有接口方法桥 Adjust Thunk 与 calli 函数指针桥;DeepWiki 只列了三种,属少计。此处主通道命名无误。)
官方文档明确把它类比为"Lua 的 wrapper 函数生成"(XLua/toLua 的绑定代码),原理相似。
三个关键结论(来自官方 methodbridge 文档):
- 签名共享 :参数类型和返回值类型完全等效 的函数可以共享同一个桥接函数,极大减少桥接函数数量(例如
object Fun1(object a, long b)和string Fun2(string a, long b)共享object(object, long)签名)。 - 集合是确定的 :对固定的 AOT 部分,桥接函数集是确定的、完备的 ,后续无论怎么热更都不会需要新的额外桥接函数------不用担心热更上线后突然出现"桥接函数缺失"。
- 必须预生成 :桥接函数必须提前在 AOT 构建期生成(这就是接入时要跑
HybridCLR/Generate的原因之一)。同一 DLL 生成的桥接函数文件跨平台完全相同,但不能复用------不同平台的编译宏和基础库(mscorlib 等)不同,所以每个平台要单独生成。
1.5 AOT 元数据补充(LoadMetadataForAOTAssembly)
热更代码要调用 AOT 里的泛型/类型时,AOT 编译期可能因为"没被直接引用"而裁剪掉了这些类型的元数据。所以运行时加载热更 DLL 之前,要先把 AOT 程序集的补充元数据喂回去:
RuntimeApi.LoadMetadataForAOTAssembly(bytes, HomologousImageMode.SuperSet)
HomologousImageMode 有两种:
- ConsistentAOTHomologousImage:使用相同版本程序集,从单个目标加载并维护缓存,保证一致性;
- SuperSetAOTHomologousImage:把多个版本合并为"超集程序集",运行时更灵活地管理组件模块。
这也是接入流程里"生成 AOT 泛型引用"(AOTGenericReferences.cs)和 link.xml 的作用------告诉 IL2CPP 编译器"这些泛型别裁剪,热更代码要用"。
1.6 DHE 差分混合执行(商业版特性)
社区版的热更代码整体解释执行 ,比 AOT 慢若干倍。商业版(Ultimate)引入 DHE(Differential Hybrid Execution,差分混合执行):
- 未改动的方法:继续以 AOT 原生速度执行;
- 改动/新增的方法:才走解释器。
于是"改一个函数,只有这个函数变慢,其余还是原生速度"。官方用一句话戳破"零损耗"的营销话术:
"零损耗"并非指所有热更代码与 AOT 完全一样快:纯新增函数仍是解释执行、比 AOT 慢若干倍;只有未改动的已有方法通过 DHE 保持 AOT 原生速度。
DHE 的产物是 dhe dll + 对应的 dhao 文件,二者是热更补丁的核心产物,需要配合原始 AOT DLL 快照(首包构建后备份 HybridCLRData/AssembliesPostIl2CppStrip/{platform} 并纳入版本管理)才能生成。
1.7 性能与内存(官方口径 vs 已验证事实)
⚠️ 本节的性能数据来自官方 performance 文档(作者 walon,对"自身原理"是一手权威、但对"相对他方案的优势"存在厂商立场),其中量化性能宣称经 deep-research 对抗验证已被证伪 ,仅作"官方自述口径"陈列,不作为选型硬依据。
官方自述的性能口径(⚠️ 已被证伪,仅列不采信):
- "社区版解释执行,比 AOT 慢若干倍(OnePlus 9R ArmV8 实测 AOT 是社区版的 4.1~90 倍)";
- "商业版数值计算指令性能是 XLua 的 651%~720%、是 Mono mint 解释器的 140%~330%、比社区版快 2.87~7.35 倍";
- "相对 Lua/ILRuntime 有数倍到数十倍性能优势"。
→ 上述三条在对抗验证中分别以 0-3 / 1-2 票 被驳回(理由:缺少可信第三方/设备实测支撑,系厂商自发布基准)。性能上"碾压 XLua/ILRuntime"不能被当作已证实优势。
内存(部分存活,但同样来自官方自述):
- 热更类型与 AOT 类型内存布局完全一致,等价类型对象大小相同(单字节 struct 在 HybridCLR/native 是 1 字节)------此点 3-0 存活,是 HybridCLR 相对独立 VM 方案最可靠的结构性优势;
- 官方给出的对比:ILRuntime 引用类型用
IlTypeInstance表示,空类型占 72 字节、每加字段至少 16 字节;XLua 单字节 struct 88+ 字节。⚠️ 注意这是厂商自发布数据,验证中因"ILRuntime V1 精确值应为 88、'88+' 实指 XLua"而被降级为 medium; - "GC 行为与 il2cpp 完全一致、无额外 GC"这条已被 1-2 票驳回,同样不宜作为硬结论采信。
小结 :HybridCLR 相对 XLua/ILRuntime 的可靠比较优势是"内存布局与原生 il2cpp 一致、无跨域对象装箱"(结构性、原理可推),而不是"性能碾压"(量化宣称存疑)。选型时应以内存/开发体验/可热更范围为准,性能需自行做基准。
二、接入流程(从零到能跑)
2.1 环境前置条件
- Unity 2019.4+(官方最低版本要求);
- Scripting Backend 必须切 IL2CPP(用 Mono 后端会导致后续所有热更操作无效);
- Api Compatibility Level 切 .NET Framework(Unity 2021+)/ .NET 4.x(2019-2020);
- 因为 IL2CPP 会把代码转 C++,需要 C++ 工具链(对应平台的 IL2CPP 模块 + 编译组件)。
2.2 安装与初始化
-
安装包 :Package Manager 用 Git URL 安装
https://gitee.com/focus-creative-games/hybridclr_unity.git -
HybridCLR/Installer :这一步会下载并安装与当前 Unity 匹配的修改版 IL2CPP ,并生成当前项目的桥接函数------官方称这是"HybridCLR 能运行热更代码的基础"。
2.3 程序集规划(接入最关键的一步)
原则(社区广泛共识的"程序集规划铁律"):
- 热更层(如
HotUpdate.dll)可以引用主包层(如Main.dll/ AOT 层); - 主包层绝对不能反向引用热更层;
- 主包要调热更层,走 接口解耦(推荐)/ 委托回调 / 反射(仅限一次性初始化) 三种方式。
落地动作:
- 为热更代码新建一个 Assembly Definition(asmdef) ,例如
HotUpdate------该文件夹内的 C# 脚本会编译进HotUpdate.dll,而不会进Assembly-CSharp.dll; - 在
HybridCLR/Settings里把这个 asmdef 加进 hotUpdateAssemblyDefinitions (本项目的ProjectSettings/HybridCLRSettings.asset里就指向了Game.HotUpdate.asmdef); - 关闭热更程序集的 Auto References,避免被主包无意引用。
2.4 生成(Generate)------把 AOT 侧准备好
构建/热更前必须执行 HybridCLR/Generate/...,核心子命令:
| 子命令 | 产物 | 作用 |
|---|---|---|
| Generate/LinkXml | HybridCLRGenerate/link.xml |
分析项目所有可能用到的泛型,告诉 IL2CPP"别裁剪,热更要用" |
| Generate/AOTGenericReference | HybridCLRGenerate/AOTGenericReferences.cs |
预生成泛型类型,漏了这一步,热更代码命中被裁剪泛型会运行时崩溃 |
| Generate/MethodBridge | 桥接函数 | 生成 AOT ↔ 解释器桥接函数(见 1.4) |
| Generate/All | 上述全部 | 一键执行 |
2.5 编译热更 DLL
HybridCLR/CompileDll/{ActiveBuildTarget} 编译热更程序集,输出到 HybridCLRData/HotUpdateDlls/{platform}。
坑(官方明确提示) :如果首包用了 Development Build ,热更时必须用 CompileDll/ActivedBuildTarget_Development 编译 Development 模式的 DLL------否则 development 与非 development 编译产物差异过大,会导致几乎所有函数被判定为"已变化"。
2.6 拷贝为 .bytes 入库
把编译出的 DLL 拷贝进 Assets 目录并改后缀为 .bytes(避免被 Unity 当脚本重新编译),再交给资源管理(Addressables Remote / CDN / 加密后下发)。注意:DLL 不能放 Resources,应从 persistentDataPath 或 CDN 加载。
2.7 打包
资源(含 .bytes 的 DLL + 补充元数据)打进 Addressables Remote 组或 StreamingAssets,随版本发布。关键点 :发布主包后,只重打热更 DLL 时无需重新 Generate(除非新增了泛型引用)。
三、热更时如何操作(补丁发布全流程)
一次典型的热更发布,链路是:
改热更代码 → 重新 CompileDll → (必要时)重新 Generate → 拷贝 .bytes → 打包资源 → 客户端下载 → 补元数据 → 加载 DLL → 反射调用入口
3.1 客户端运行时加载(固定四步)
无论用哪种资源下发方式,运行时加载热更 DLL 的固定顺序是:
- 下载 :从 Addressables / CDN 拿到
HotUpdate.dll.bytes; - 补 AOT 元数据 :先遍历补丁 AOT 程序集列表,逐个
RuntimeApi.LoadMetadataForAOTAssembly(bytes, HomologousImageMode.SuperSet); - 加载热更 DLL :
Assembly.Load(byte[]); - 调用入口 :反射拿到入口类型/方法并
Invoke(本项目是HotUpdate.Entry.Start())。
3.2 函数替换的语义(重要认知)
HybridCLR 的热更不是"打单个函数补丁" ,而是整 DLL 替换:
- 社区版:整个热更 DLL 被重新加载、整体解释执行;
- 商业版 DHE:加载时对比原始 DLL 快照,只有改动的函数走解释器,未改函数保持 AOT 原生------这才是"函数级"的差分替换。
这跟 XLua 的 [Hotfix] 标记 + 逐函数打补丁是两套哲学:HybridCLR 不需要预判"哪些代码可能被改",因为全部 C# 代码理论上都可热更。
3.3 DHE 补丁发布细节(商业版)
- 首包构建不需要任何 DHE dll/dhao 文件(此时所有 DHE 程序集代码未变);
- 首包构建后,备份
HybridCLRData/AssembliesPostIl2CppStrip/{platform}目录下的原始程序集并纳入版本管理 ------后续每次生成dhao文件都要依赖这些原始 DLL; - 每次热更:
CompileDll生成热更 DLL →HybridCLR.Editor.DHE.BuildUtils.GenerateDHAODatas生成dhao→ 把dll + dhao一起加入热更资源下发(运行时不需要原始 dll)。
3.4 程序集热重载/卸载(商业版)
商业版还提供"程序集热重载":支持卸载程序集并释放其近 100% 内存(除 MonoBehaviour/ScriptableObject、[Serializable]、不涉本程序集类型的泛型如 List<int> 等少量元数据外,约 99.9% 的元数据内存可释放)。约束:
- 卸载须按反向依赖顺序(A.dll 依赖 B.dll → 先卸 A 再卸 B);
- 热重载对 MonoBehaviour 有限制:
Awake/OnEnable等事件消息函数不能增删(函数体可改),同名脚本类的序列化字段名不得改变; - 与 Unity DOTS 不兼容。
四、Unity 手游热门热更方案全方位对比
4.1 先厘清一个概念:资源热更 vs 代码热更
| 类别 | 代表方案 | 能做什么 | 不能做什么 |
|---|---|---|---|
| 资源热更 | AssetBundle / Addressables | 更新 prefab、贴图、配置、音频等资源 | 不能改 C# 逻辑(除非重打包) |
| 代码热更(脚本语言) | XLua / toLua / uLua / slua | 热更 Lua 脚本 | C# 逻辑不能热更(需 [Hotfix] 打补丁,麻烦且不可预判) |
| 代码热更(C# IL 解释) | ILRuntime | 热更"热更工程"里的 C# | 主框架 C# 无法热更 |
| 代码热更(原生 C#) | HybridCLR | 理论上全部 C# 代码 | 不能热更 AOT 部分(需重新打包) |
所以 AssetBundle 与代码热更方案是组合关系 而非替代关系------大多数项目是"Addressables/AssetBundle 管资源 + 某个代码热更方案管逻辑"。本项目的落地正是 HybridCLR + Addressables。
4.2 各方案执行模型对比
| 维度 | Lua(XLua/toLua) | ILRuntime | HybridCLR |
|---|---|---|---|
| 实现方式 | 嵌入 Lua VM,脚本解释执行 | 纯 C# 实现的 IL 解释器 | 改造 IL2CPP,内置 IL 解释器 |
| 热更代码跑在哪 | 独立 Lua VM | 独立 C# VM(跨域) | 直接跑在 IL2CPP VM 内 |
| 数据/类型系统 | 与 C# 类型系统不通,需绑定/wrapper | 与 IL2CPP 类型系统不互通,需适配器 | 共享 IL2CPP 类型系统 |
| 跨语言/跨域开销 | 有(PInvoke/ReversePInvoke + 参数转换) | 有(IlTypeInstance 包装) | 无(直调 IL 转出的 C++ 函数) |
| 语言 | Lua(需额外学) | C#(热更工程内) | 纯 C#,零额外语言 |
4.3 能力对比(重点)
| 维度 | Lua | ILRuntime | HybridCLR |
|---|---|---|---|
| 可热更范围 | 仅 Lua 脚本 | 仅热更工程 C# | 任何 C# 代码 |
| 直接挂 MonoBehaviour | 否 | 否(需 AddComponent 动态加载 + 适配器) | 是 |
| ref/out 参数 | 受限 | 不支持 | 支持 |
| LINQ / async/await | 否 | 部分 | 完整支持 |
| 泛型 / 反射 / 委托 | 受限 | 部分 | 完整支持 |
| 多线程(volatile/ThreadStatic/Task) | 否 | 否 | 是(唯一完整支持) |
| 函数替换 / 新增类 / 静态变量改 | Lua 侧可,C# 侧难 | 部分 | 支持 |
4.4 性能 / 内存 / 包体对比
| 维度 | Lua | ILRuntime | HybridCLR |
|---|---|---|---|
| 性能 | 较低(解释 + 跨语言) | 中等(优于 Lua,低于原生) | 社区版解释执行、比 AOT 慢;商业版 DHE 未改函数走原生。⚠️ 无可信第三方基准,"接近原生/碾压"存疑 |
| 内存 | 对象头 + 对齐开销 | 引用类型每实例开销大(空类型 72B) | 与 AOT 布局一致、无跨域装箱(结构性优势,官方自述) |
| GC | 有额外 GC | 有额外 GC | 官方称与 il2cpp 一致,⚠️ 已被验证驳回,不宜采信 |
| 包体增量 | Lua 解释器 | 约 100~500KB | 约 100~500KB(核心解释器 <50KB) |
| 版本迭代性能 | 每版都解释执行 | 每版都解释执行 | 新版可把热更 DLL 直接 AOT 化,越迭代越快(原理成立,量化待证) |
4.5 HybridCLR 的局限(客观审视)
- 不能热更 AOT 部分:主包(AOT)代码改动仍需重新打包------这是所有"混合运行时"方案的共同边界;
- 依赖 Unity 版本:官方维护的修改版 IL2CPP 需匹配 Unity 版本(如 2020.3+ LTS),升级 Unity 要等官方跟进;
- 必须 IL2CPP 后端:Mono 后端直接无效;
- 桥接函数/泛型需预生成:接入流程比 Lua 方案多一步 Generate,新增泛型引用需重新生成元数据;
- 社区版解释执行有性能损耗:纯新增函数比 AOT 慢若干倍,追求极致性能需商业版 DHE。
4.6 选型建议
| 项目类型 | 推荐 |
|---|---|
| 中大型 / 复杂业务逻辑、希望纯 C# 开发 | HybridCLR + Addressables |
| 团队已有 Lua 技术栈 / 重度 Lua 脚本 | XLua |
| 仅需资源热更、无代码热更诉求 | 官方 Addressables + AssetBundle |
| 需要"逐函数 hotfix"的存量 C# 项目、不引第三方 C++ | ILRuntime |
五、实战篇------Unity手游项目接入
当前,手游项目已完成 HybridCLR + Addressables 的完整接入。
5.1 启动流程编排:先补元数据,再加载 DLL
Assets/AOT/Common/xxx/xxxHybridCLRHotUpdate.cs------启动 Flow 链里的"加载 HybridCLR"节点,严格遵循第三节的加载顺序:
csharp
public class xxxHybridCLRHotUpdate : StartFlowBase
{
private const string HotUpdateDllName = "HotUpdate.dll";
public override void Start()
{
Load().Forget();
}
private async UniTaskVoid Load()
{
var succeeded = false;
try
{
// ① 先补 AOT 元数据(对应 1.5 / 3.1 第 2 步)
var aotResult = await HybridCLRAddressablesLoader.LoadAotMetadataAsync(
AOTGenericReferences.PatchedAOTAssemblyList);
// ② 再加载热更 DLL(对应 3.1 第 3 步)
var asm = await HybridCLRAddressablesLoader.LoadHotUpdateAssemblyAsync(HotUpdateDllName);
m_gameFlowManager.m_flowData.HotUpdateAssembly = asm;
succeeded = true;
}
catch (Exception e)
{
}
if (succeeded)
{
await UniTask.Yield();
m_gameFlowManager.DoNextFlow(m_taskName);
}
}
}
启动 Flow 链在 xxxGameFlowManager.Init() 里注册,顺序是:xxxGetVersion → xxxGetServerInfo → xxxInitComponent → xxxAddressablesHotUpdate(资源热更)→ xxxLoadHybridCLRHotUpdate(代码热更)→ xxxPreLoad。注意 xxxLoadHybridCLRHotUpdate 只在 !UNITY_EDITOR 下注册------资源热更先跑、代码 DLL 加载在后,正好对应"Addressables 管资源 + HybridCLR 管代码"的组合关系。
5.2 核心加载器
csharp
public static async UniTask<Assembly> LoadHotUAssemblyAsync(
string hotUpdateDllName, CancellationToken ct = default)
{
var address = xxHybridCLRAssetPath.GetHotUDllBytesAddress(hotUpdateDllName);
var bytes = await LoadBResKitAsync(address, ct);
// 加载热更DLL:原生反射加载(对应 1.1 / 3.1 第 3 步)
var asm = Assembly.Load(bytes);
bytes = null;
return asm;
}
5.3 入口反射调用:从 AOT 跨入热更世界
HotUInvoker.cs------加载完 DLL 后,用反射定位并调用热更入口
csharp
public static class HotUInvoker
{
private const string DefaultEntryTypeName = "HotUpdate.Entry";
private const string DefaultEntryMethodName = "Start";
public static bool TryInvoke(Assembly hotUpdateAssembly)
{
method.Invoke(null, null); // 热更
return true;
}
}
调用点在 HCLauchGame():
csharp
public void HCLaunchGame()
{
#if UNITY_EDITOR || (UNITY_ANDROID && !UNITY_EDITOR)
MessageBroker.Default.Publish(new StartHotUpdateEvent());
#else
var invoked = HotUInvoker.TryInvoke(
m_gameFlowManager.m_flowData.HotUpdateAssembly); // 真机走反射调用
#endif
}
这段代码也印证了 2.3 的"程序集规划铁律":AOT 层不直接引用 热更层,而是通过字符串约定的入口类型/方法名 + 反射解耦调用------这正是"主包调热更层走反射(仅限一次性初始化)"的标准做法。
参考资料
- 构建和热更新 | HybridCLR 官方文档
- Quick Start | HybridCLR 官方快速上手
- 桥接函数 | HybridCLR 官方文档
- Execution Performance | HybridCLR 官方文档
- Memory and GC | HybridCLR 官方文档
- 程序集热重载/卸载 | HybridCLR 官方文档
- Architecture Overview | hybridclr_unity (DeepWiki)
- HybridCLR------划时代的Unity原生C#热更新技术 (UWA)
- Lua, ILRuntime, HybridCLR(wolong)/huatuo 热更对比分析 (cnblogs)
- HybridCLR 源码解析 (知乎)