一、HybridCLR
HybridCLR是一个特性完整、零成本、高性能、低内存的近乎完美的Unity全平台原生c#热更新解决方案。

对比
| ToLua/Xlua | ILRuntime | HybridCLR | |
|---|---|---|---|
| 语言 | Lua | C# | C# |
| 虚拟机 | Lua虚拟机 | Mono虚拟机 | 没有虚拟机 |
| 学习曲线 | 弱类型,无精确代码提示,要学习并精通第二门语言 | 要适配ILRuntime | 无感透明 |
| 性能 | 低 | 中 | 高 |
| 成熟度 | 端游时代热更方案 | Unity官方支持 | 正式介入一些商业项目 |
工作原理
HybridCLR从mono的 mixed mode execution 技术中得到启发,为unity的il2cpp之类的AOT runtime额外提供了interpreter模块,将它们由纯AOT运行时改造为"AOT + Interpreter"混合运行方式。

更具体地说,HybridCLR做了以下几点工作:
- 实现了一个高效的元数据(dll)解析库
- 改造了元数据管理模块,实现了元数据的动态注册
- 实现了一个IL指令集到自定义的寄存器指令集的compiler
- 实现了一个高效的寄存器解释器
- 额外提供大量的instinct函数,提升解释器性能
二、YooAsset
YooAsset是一套用于Unity3D的资源管理系统,用于帮助研发团队快速部署和交付游戏。
系统特点
-
可扩展文件系统
通过统一的文件系统参数配置,支持微信小游戏、抖音小游戏等特殊平台。支持接入虚拟文件系统,支持自定义各类存储、缓存和下载需求。
-
构建管线无缝切换
支持传统的内置构建管线,也支持可编程构建管线(SBP)。
-
支持分布式构建
支持分工程构建,支持工程里分内容构建,很方便支持游戏模组(MOD)。
-
支持可寻址资源定位
默认支持完整路径的资源定位,也支持可寻址资源定位,不需要繁琐的过程即可高效的配置寻址路径。
-
安全高效的分包方案
基于资源标签的分包方案,自动对依赖资源包进行分类,避免人工维护成本。可以非常方便的实现零资源安装包,或者全量资源安装包。
-
强大灵活的打包系统
可以自定义打包策略,自动分析依赖实现资源零冗余,基于资源对象的资源包依赖管理方案,天然的避免了资源包之间循环依赖的问题。
-
基于引用计数方案
基于引用计数的管理方案,可以帮助我们实现安全的资源卸载策略,更好的对内存管理,避免资源对象冗余。还有强大的分析器可帮助发现潜在的资源泄漏问题。
-
多种模式自由切换
编辑器模拟模式,单机运行模式,联机运行模式,WebGL运行模式。在编辑器模拟模式下,可以不构建资源包来模拟真实环境,在不修改任何代码的情况下,可以自由切换到其它模式。
-
强大安全的加载系统
异步加载 支持协程,Task,委托等多种异步加载方式。 同步加载 支持同步加载和异步加载混合使用。 边玩边下载
在加载资源对象的时候,如果资源对象依赖的资源包在本地不存在,会自动从服务器下载到本地,然后再加载资源对象。 多线程下载
支持断点续传,自动验证下载文件,自动修复损坏文件。 多功能下载器
可以按照资源分类标签创建下载器,也可以按照资源对象创建下载器。可以设置同时下载文件数的限制,设置下载失败重试次数,设置下载超时判定时间,也支持自定义下载重试策略和URL选择策略。多个下载器同时下载不用担心文件重复下载问题,下载器还提供了下载进度以及下载失败等常用接口。
-
原生格式文件管理
无缝衔接资源打包系统,可以很方便地实现原生文件的版本管理和下载。
-
灵活多变的版本管理
支持线上版本快速回退,支持区分审核版本,测试版本,线上版本,支持灰度更新及测试。
-
多平台的完美适配
支持安卓,苹果,PC,WebGL以及各类小游戏平台。通过可扩展文件系统适配不同平台的文件访问、缓存和下载行为。
三、安装配置
3.1 HybridCLR
安装 com.code-philosophy.hybridclr 包 : 主菜单中点击Windows/Package Manager打开包管理器。如下图所示点击Add package from git URL...,填入https://gitee.com/focus-creative-games/hybridclr_unity.git或https://github.com/focus-creative-games/hybridclr_unity.git。

初始化 com.code-philosophy.hybridclr: 打开菜单HybridCLR/Installer..., 点击安装按钮进行安装。 耐心等待30s左右,安装完成后会在最后打印 安装成功日志。
创建 HotUpdate 热更新模块:在目录下 右键 Create/Assembly Definition,创建一个名为HotUpdate的程序集模块
配置HybridCLR :
打开菜单 HybridCLR/Settings, 在Hot Update Assemblies配置项中添加HotUpdate程序集,如下图:

配置PlayerSettings :
如果你用的hybridclr包低于v4.0.0版本,需要关闭增量式GC(Use Incremental GC) 选项
Scripting Backend 切换为 IL2CPP
Api Compatability Level 切换为 .Net 4.x(Unity 2019-2020) 或 .Net Framework(Unity 2021+)

3.2 YooAsset
打开管理界面 Edit/Project Settings/Package Manager
// 输入以下内容(国际版)
Name: package.openupm.com
URL: https://package.openupm.com
Scope(s): com.tuyoogame.yooasset

打开管理界面 Edit/Windows/Package Manager

通过右键创建配置文件(Project窗体内右键 -> Create -> YooAsset -> Create YooAsset Settings)
注意:请将配置文件放在Resources文件夹下

四、详细流程
4.1 创建AOT文件
这个文件是要打进主包的文件,主要负责游戏的启动流程。

- 创建AOT程序集,添加对应需要的因为程序集。注意这里不能添加HotUpdate的引用,以防产生循环引用导致报错。
- 动态加载被裁切的dll文件。主要用于解析热更代码中的泛型
csharp
private static List<string> AOTMetaAssemblyFiles {get; } = new List<string>(){
"UniFramework.Event.dll",
"UnityEngine.CoreModule.dll",
"YooAsset.dll",
"mscorlib.dll",
};
private IEnumerator LoadDLLs(){
var gamePackage = YooAssets.GetPackage("DefaultPackage");
var assets = new List<string>() { "HotUpdate.dll" }.Concat(AOTMetaAssemblyFiles);
foreach (var dllName in assets)
{
Debug.Log($"加载程序集: {dllName}");
var handle = gamePackage.LoadAssetAsync<TextAsset>(dllName);
yield return handle;
if(handle.Status != EOperationStatus.Succeeded){
Debug.LogError($"加载程序集: {dllName} 失败");
yield break;
}
s_assetDatas.Add(dllName, handle.AssetObject as TextAsset);
handle.Release();
}
LoadMetadataForAOTAssembly();
LoadHotUpdateDlls();
SceneChangeToHomeEvent.SendEventMessage();
}
private void LoadMetadataForAOTAssembly(){
var mode = HomologousImageMode.SuperSet;
foreach (var asset in AOTMetaAssemblyFiles){
var bytes = s_assetDatas[asset].bytes;
var error = RuntimeApi.LoadMetadataForAOTAssembly(bytes, mode);
if (error != LoadImageErrorCode.OK)
{
Debug.LogError($"Load metadata for AOT assembly {asset} failed: {error}");
}
}
}
private void LoadHotUpdateDlls(){
if(s_assetDatas.Count == 0){
Debug.Log("程序集字节数据为空,跳过加载热更dll");
return;
}
#if !UNITY_EDITOR
_hotUpdateAss = Assembly.Load(s_assetDatas["HotUpdate.dll"].bytes);
#else
_hotUpdateAss = AppDomain.CurrentDomain.GetAssemblies().First(assembly => assembly.GetName().Name == "HotUpdate");
#endif
}
AOTMetaAssemblyFiles这个文件中的数据可以根据HybridCLR生成的文件中获得

4.2 创建HotUpdate
在项目文件中创建HotUpdate程序集,添加对应引用:

同时将这个配置到HybridCLR的Setting中:

生成热更的代码文件HotUpdate.dll:

初始化的时候先用Generate -> all 。 后面已经打包后的就可以用CompileDll -> ActiviteBuildTarget
4.3 创建DOLLS文件
- 将这个文件添加到YooAsset作为资源热更新中

- 将之前生成的HotUpdate.dll改名成HotUpdate.dll.bytes后放到这个文件中去。
- 同时将之前被剪切的程序集需要动态加载的文件也在后面加上.bytes后放到这个文件中。
五、打包测试

如果Scripting Backed无法选中IL2CPP说明你没有安装unity对应Module

然后打包到PC平台:

六、本地测试
通过YooAsset的BundleBuild打包热更资源和代码
本地启动服务器

通过python启动本地服务器:python -m http.server 80
将YooAsset打包的资源对应拷贝到服务器对应的路径下:

接下来你就可以修改代码或者预制体后直接替换资源而不用重新再打包了。
七、课外扩展
7.1 泛型实例化
什么是泛型实例化?
csharp
// 这是一个泛型类
public class List<T>
{
public void Add(T item) { ... } // 泛型函数
}
// 当你写下这些代码时,编译器会为每个 T 生成一个独立的版本
List<int> list1 = new List<int>(); // 需要 List<int>.Add 的代码
List<string> list2 = new List<string>(); // 需要 List<string>.Add 的代码
List<long> list3 = new List<long>(); // 需要 List<long>.Add 的代码
正常 JIT 环境(PC、Android 等)
JIT 看到 List 时,运行时根据泛型定义 + long 类型,即时生成 List.Add 的机器码
只要有原始 IL 代码,就能随时生成任意 T 的版本
IL2CPP AOT 环境(iOS)
IL2CPP 的流程是:
C# 源码 → IL 代码 → C++ 代码 → 编译成静态机器码(AOT)
关键问题: 在生成 C++ 代码的阶段,编译器需要知道将来会用到哪些 T,才能预先生成对应版本的 C++ 代码。
cpp
// IL2CPP 生成的 C++ 代码(伪代码)
// 它只能生成编译时已知的版本
void List_Add_int(List<int>* this, int item) { ... } // 只有 int 版本
void List_Add_object(List<object>* this, object* item) { ... } // 只有 object 版本
// 没有 List<long>.Add,因为编译时没人用 List<long>
后果: 如果热更新代码中突然用了 List,运行时找不到 List.Add 的机器码,程序就崩溃了。
你不能把 List.Add 的二进制代码直接改成 List.Add,因为指令不同、栈大小不同、GC 跟踪方式也不同。必须要有原始的泛型函数体 IL 元数据,才能重新生成正确的新版本。
HybridCLR 的解决方案
核心思路 : 补充上丢失的原始泛型函数体元数据。
IL2CPP 在翻译时丢弃了原始 IL(只保留了翻译后的 C++ 代码)。HybridCLR 做的事情就是:在运行时把这些被丢弃的原始 IL重新加载回来。

┌─────────────────────────────────────────────────────────────────┐
│ 正常 IL2CPP AOT(崩溃) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 编译时:C# → IL → C++ → 机器码 │
│ ↓ │
│ 原始 IL 被丢弃 ❌ │
│ │
│ 运行时:热更新代码调用 List.Add │
│ ↓ │
│ 找不到对应的机器码 → 崩溃 ❌ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ HybridCLR 解决方案(成功) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 编译时:保留一份 AOT 程序集的原始 DLL(包含完整 IL) │
│ ↓ │
│ 随 App 发布或热更新下载 │
│ │
│ 运行时:1. 加载热更新 DLL │
│ 2. 调用 LoadMetadataForAOTAssembly() │
│ ↓ │
│ 把原始 IL 元数据加载到内存 ✅ │
│ ↓ │
│ 3. 热更新代码调用 List.Add │
│ ↓ │
│ HybridCLR 解释器/增强运行时: │
│ 根据原始 IL 元数据 + long 类型, │
│ 动态生成/解释执行 List.Add ✅ │
└─────────────────────────────────────────────────────────────────┘
一句话总结 : IL2CPP 把原始泛型函数的 IL 代码弄丢了,所以无法在运行时生成新的泛型实例化版本。HybridCLR 的
LoadMetadataForAOTAssembly 就是把这份"被丢弃的原始 IL 元数据"重新加载回来,让泛型函数能够像 JIT
环境一样被正确实例化。调用时机要在使用任何 AOT 泛型之前,越早越安全。
但是补充元数据加载后,大约会占用6倍dll大小的内存,而且这些内存无法回收。对内存有较高的要求。因此有了商业版本的"完全泛型共享"技术,其核心思路非常巧妙------它没有像社区版那样去"修补"问题,而是直接利用了IL2CPP底层的一个原生特性,将计就计,从根本上避免了"补充元数据"的需求。

商业版"完全泛型共享"通过以下方式解决了上述难题,实现了对值类型的共享:
- 统一值类型的内存布局:IL2CPP在编译时,会为所有值类型生成一种统一的、可供共享的内存布局和函数调用约定。这相当于为所有值类型穿上了一件统一的"外套",确保它们在内存中能被同样地处理。
- 运行时类型"伪装":在这个机制下,运行时遇到 List 时,它会认为 long 在内存布局上和 int
是"兼容"的。因此,它不需要去生成新的代码,而是直接复用已经编译好的 List.Add 的机器码,通过调整偏移量来正常工作。
这样一来,因为不再需要原始IL来生成新代码,所以也完全不再需要"补充元数据",那部分内存占用自然就降为了0。
7.2 为什么IOS可以允许Hybrid热更新
IOS的审核条款(Guideline 2.5.2和2.3.1)严格禁止的是下载可执行代码并动态改变应用功能。HybridCLR的纯热更方案之所以可行,是因为它完全遵守了另一条安全路径。

🔍 技术原理:为什么纯热更是"合规"的?
HybridCLR方案的精妙之处,在于它精准地卡在了"代码"与"数据"的边界线上。
- AOT地基(留在包内):你的App安装包里,包含了HybridCLR的解释器(Interpreter)和桥接函数。这个解释器是苹果审核通过的"官方部件",它本身并不执行任何逻辑,只是一个工具。
- 热更新逻辑(动态下载):你的业务逻辑代码,被编译成了IL中间语言,作为一个普通的数据文件从服务器下载。在苹果看来,这和下载一张图片、一首歌没什么本质区别。
- 解释执行(关键一步):当下载完成后,App内部预置的解释器会读取这份"数据"(IL代码),然后在内存里逐条翻译并执行它。执行流程被完全限制在解释器这个"沙盒"里,无法做出恶意行为。
⚠️ 需要特别注意的风险点
虽然原理上合规,但在实际操作中,仍有几个需要留意的地方:
- 避免"下载后又改变核心功能":这是苹果的底线。如果你的App过审时是个计算器,上线后通过热更新变成了游戏,这会被视为严重欺诈。
- 小心开发框架本身:早期像JSPatch这样的框架,因为权限过大且被滥用,已被苹果明确封杀。HybridCLR等基于IL/解释器的方案,是目前被认为安全的替代技术。