代码热更原理
前言
游戏发行后总是伴随着有bug或者功能需要发布出去,常规的方法是通过强更版本让玩家去商店下载新版本或者游戏内下载新版本安装,如果是资源bug可以通过资源热更的方式进行游戏内热更后再进入游戏即可。但C#在IOS上禁止热更(核心原因后续会聊)。
C#代码热更原理
在深入学习HybridCLR之前,我们先了解下C#代码热更原理。
C#运行原理
在聊C#为什么被IOS禁止热更前,我们要了解C#是如何从代码到真机生效运行的。
C#->编译器->IL->.NET/Mono->机器码(运行时IL转机器码(JIT))
IOS不允许应用在运行时随意生成机器码,也就是上述流程在IL->.NET/Mono->机器码这一步禁止了。
IOS禁止JIT
JIT本质是运行时将IL转成对应机器码,也就等价于运行时生成代码。
为什么IOS禁止运行时生成代码了?
这和 iOS 的安全原则冲突。iOS 通常要求:
- 可执行代码必须经过签名;
- App 能执行的原生代码应当在打包、签名时确定;
- 普通应用不能随意创建“既可写又可执行”的内存;
- 不能下载一段新的原生机器码后直接执行。
这类限制通常可以从代码签名、W^X(Writable or Executable)和禁止动态生成可执行代码几个角度理解。
JIT 的本质恰恰是“运行时动态生成新机器码”,所以普通 iOS App 无法使用传统的 Mono JIT 模式。
Unity IL2CPP
最初Unity使用的Mono来实现跨平台,后来推出了IL2CPP。
为什么Unity会从Mono转向IL2CPP了?
因为不能依赖JIT,所以Unity选择把IL全部转换成C++代码(IL2CPP这一步)。
使用IL2CPP流程就会变成如下:
C#->编译器->IL->IL2CPP->C++->Xcode/Clang AOT 编译->ARM64/ARMV7原生机器码
但AOT带来的一个问题就是,如果新写的C#代码如果没经过IL2CPP->C++->ARM64->签名这条流程,那么在IOS上就没法直接热更运行。
ILRuntime
讲到这里,那顺便一提ILRuntime这个老牌C#热更框架,为什么ILRuntime能绕过IOS这个限制了?
ILRuntime 的思路不是在手机上把新 IL 编译成机器码,而是:
用安装包中已经 AOT 编译并签名过的解释器,逐条解释热更新 DLL 的 IL 指令。
使用ILRuntime流程就会变成如下:
热更新DLL->读取IL指令->ILRuntime解释器->逐条模拟IL的语义
真正被 CPU 执行的是提前打包、签名过的 ILRuntime 解释器。新下载的 DLL 在这个模型中更像是解释器读取的“指令数据”。
这也就是ILRuntime新的DLL为什么不走IL2CPP也能正常执行的原因。
但ILRuntime代价也很明显:
- 解释执行通常比原生代码慢;
- 热更代码和主工程之间需要跨域调用;
- 泛型、委托、继承、反射等场景需要适配;
- 经常需要生成绑定代码来降低反射和跨域调用开销;
- 调试体验与标准 CLR 有差异。
HybridCLR
然后进入本章节的主题,HybridCLR,让我们来看看HybridCLR是如何从原理上实现支持C#热更的。
HybridCLR解决问题的思路更贴近标准 CLR。它扩展了 Unity 的 IL2CPP 运行时,使其具备执行热更新程序集 IL 的能力。
1 | AOT 程序集 |

也就是说,HybridCLR仍然遵守 iOS 的限制:
- 不在运行时 JIT 生成新的 ARM64 代码;
- 新下载的 DLL 主要以 IL 解释方式执行;
- CPU 实际执行的解释器和运行时本身已经随 App AOT 编译并签名。
HybridCLR为什么还需要“补充元数据”?
IL2CPP在打包时会对代码和元数据进行裁剪、转换。某些泛型方法虽然可以复用已有的 AOT 原生代码,但运行时可能缺少足够的类型元数据。
例如热更新代码新增了一个调用:
1 | List<MyNewType> |
AOT 阶段未必知道未来会出现这个具体类型组合。相应泛型函数有时可以通过泛型共享机制复用,但运行时仍需要知道:
MyNewType的字段布局;- 泛型参数信息;
- 方法签名;
- 类型关系;
- 虚方法和反射信息。
HybridCLR可以加载裁剪前 AOT 程序集的补充元数据,让运行时重新获得这些信息。需要注意:
补充元数据不是把新代码变成原生机器码。
它主要用于恢复类型和方法描述,使热更新 IL 能正确调用已有 AOT 泛型代码、反射能力和运行时设施。没有可复用 AOT 实现的热更新方法,仍然由解释器执行。
ILRuntime、HybridCLR与传统 JIT 的本质区别
| 方案 | 新 DLL 如何执行 | 是否运行时生成原生机器码 | iOS 可行性 |
|---|---|---|---|
| Mono/.NET JIT | IL 即时编译为 ARM64 | 是 | 普通 App 不可行 |
| 下载原生动态库 | 直接执行新 ARM64 代码 | 代码在安装包外生成 | 通常不可行 |
| ILRuntime | 独立解释器解释 IL | 否 | 可行 |
| HybridCLR | IL2CPP 运行时扩展后解释 IL | 否 | 可行 |
| 原生 IL2CPP | 发布前把 IL 编译为 ARM64 | 仅构建阶段生成 | 可行,但不能代码热更 |
两种热更新技术的共同本质是:
把“运行时加载并执行新的机器码”,转换成“用已有、已签名的原生解释器读取并执行新的 IL 数据”。
区别主要在于:
- ILRuntime建立了一套相对独立的 IL 执行环境,和 IL2CPP 主工程之间存在明显的解释域边界。
- HybridCLR直接扩展 IL2CPP 运行时,类型系统、泛型、反射以及主工程与热更程序集的交互通常更接近正常 C# 开发体验。
Lua
说到这里提一下Lua为什么可以热更新,因为Lua是解释执行,代码内部已经包含Lua虚拟机,已经支持Lua代码的解析执行,Lua可以像代码一样热更后解释执行。
总结
C# 本身并不是不能热更新,真正的问题是:
- C# DLL通常包含的是 IL,不是CPU可以直接执行的ARM64机器码;
- 传统 CLR需要 JIT 把新 IL 转成机器码;
- iOS 的代码签名和内存执行策略禁止普通 App 随意 JIT 或加载未签名原生代码;
- Unity因此使用 IL2CPP/AOT,但 AOT 只能编译发布时已知的代码;
- 发布后下载的新 DLL 没有对应的 AOT 机器码,因而无法直接运行;
- ILRuntime和HybridCLR通过“解释执行 IL”解决问题,不在运行时生成新的原生代码。
HybridCLR学习
HybridCLR安装
不知道是不是因为国外Unity和国内Unity分区了,国外PackageManager里没法直接搜索到HybridCLR,这里采用从Git URL的安装方式。
注意地址要填:https://github.com/focus-creative-games/hybridclr_unity.git而非官方Github地址,上面这个地址才是专门给Unity制作的Github安装包。
安装完成后如下:
HybridCLR初始化
安装好后初始化HybridCLR,点击菜单HybridCLR/Installer…
安装完成后我们会看到项目工程目录下多了LocalIl2CppData-WindowsEditor目录(看得出是跟平台挂钩的,不同平台执行应该会复制到不一样的目录):
创建ConsoleToCreen.cs脚本(方便日志打印到屏幕验证流程),复制以下代码:
1 | using System; |
HybridCLR划分程序集
前面我们说过HybridCLR实现C#热更新原理是扩展IL2CPP运行时,将热更程序集IL通过扩展的IL2CPP利用已经打进包的类型数据信息进行解释执行。
所以第一步我们需要针对热更和非热更进行程序集划分(以下只是学习热更目的所以划分的比较粗暴,实际工程会划分更细):
Main.asmdef(主AOT程序集)
HotUpdate.asmdef(热更新程序集)
如果Main.asmdef作为AOT不热更的程序集,最好关闭HotUpdate.asmdef的Auto Referenced避免出现失误引用热更新程序集导致打包失败的情况。
将热更新程序集HotUpdate.asmdef设置成HybridCLR的热更新程序集:
HybridCLR/Settings/Hot Update Assembly Definitions
HybridCLR项目设置
前面已经提到HybridCLR是通过扩展IL2CPP运行时来实现热更DLL解析执行的,那么我们想使用HybridCLR那么肯定必须将打包编译后台设置成IL2CPP:
创建热更新代码
在热更新程序集(HotUpdate)目录下创建HotUpdateEntry.cs文件作为热更新入口代码:
HotUpdateEntry.cs
1 | using System.Collections; |
使用GameLauncher.cs脚本(挂载在启动场景上)进行热更程序集(HotUpdate.dll)的资源加载和代码使用:
GameLauncher.cs
1 | using System.Linq; |
生成热更程序集
在HybridCLR里热更新程序集DLL是当做资源下载和加载的,所以在加载热更新程序集之前我们还要生成热更新程序集DLL,点击HybridCLR->Generate->All
生成后会在HybridCLRData/HotUpdateDlls/StandaloneWindows64目录下看到生成热更新Hotupdate.dll:
Note:
- 热更新的DLL要当做二进制文件加载,要给热更新DLL加.bytes后缀。
加载并使用热更新程序集
在HybridCLR里,热更新DLL就相当于资源一样加载,所以热更新代码走的热更流程跟资源一样,当前我是测试方便就直接放在SteamingAsset目录下,同时热更新DLL直接走Assembly.Load加载程序集即可,所以首先将HyrbidCLR->Generate->All生产的HotUpdate.dll拷贝到StreamingAssets目录:
HyrbidCLR->Generate->All同时会生成HybridCLR相关放裁剪文件。
拷贝到StreamingAssets目录后,在Editor下可以直接通过System.AppDomain.CurrentDomain.GetAssemblies()加载因为已经加载过了,非Editor则把热更程序集当做二进制数据加载:
GameLauncher.cs
1 | using System.Linq; |
运行项目就会看到热更新程序集入口我们加载成功并打印除了”Hello, HybridCLR”:
然后Editor打包Windows进行测试:
File->Build Profiler->Build
运行Windows打包Exe结果:
可以看到即使是打包成Windows真机包,通过加载StreamingAssets目录下的热更新程序集也是能正确运行热更新代码的。
Note:
- 主工程不能直接引用热更新代码。有多种方式可以从主工程调用热更新程序集中的代码,比如反射来调用热更新代码。
修改热更代码生效
修改HotUpdateEntry.cs的方法,添加新的热更方法调用:
1 | using System.Collections; |
Editor因为是直接编译,所以热更新程序集能实时生效:
但要想打包热更新程序集修改生效,还得走一次HybridCLR->CompileDll->ActiveBuildTarget(如果有明确需要触发重新编译的平台可以选对应平台菜单)触发重新编译热更新代码+新热更新程序集拷贝到StreamingAssets目录:
新生成的热更新程序集放到StreamingAssets目录后,重新打包运行EXE:
可以看到直接放入最新热更新程序集到StreammingAssets目录重新打包时能成功生效最新代码的,这个结果也是预期之类的,核心我们需要验证的是热更新热更程序集到包外后并加载看是否生效,这一点等后续接入热更新流程后再验证。
HybridCLR热更注意事项
Monobehaviour热更注意事项
- 由于Unity资源管理系统的限制,热更新脚本所挂载的资源(prefab、scene、ScriptableObject资源)必须打成assetbundle,从ab包中实例化资源,才能正确还原脚本。
- 如果将热更新脚本挂载到Resources等随主包的资源上,会发生scripting missing的错误!但如果先打成assetbundle包,再放到Resources下,运行时加载该随包assetbundle则没有问题。
泛型热更注意事项
- 使用热更新中定义的泛型类或函数,直接使用即可。
- AOT中已经有代码实例化过某个泛型类或者函数,则热更新中可以直接使用
- AOT中没有实例化过某个AOT泛型类或者函数,解决办法有以下几种:
- 在AOT代码添加相应的实例化代码。(需要强更不合适)
- 补充元数据技术。 这是HybridCLR的专利技术,社区版本也能使用。
- full generic sharing完全泛型共享技术,相比补充元数据技术,工作流更简单,既不需要随包携带或者下载补充元数据dll,也不需要加载补充元数据dll,包体大小和内存都明显降低。该技术目前只在商业化版本提供。
因为没买商业版这里主要针对补充元数据技术进行学习验证:
补充元数据技术
这里以热更新程序集泛型访问热更新程序集的类为例:
首先我在HotUpdate程序集定义一个HotUpdateClass,然后在热更程序集尝试List
HotUpdate.cs
1 | using UnityEngine; |
然后在热更新工程泛型访问HotUpdateClass:
HotUpdateEntry.cs
1 | using System.Collections; |
然后按照正常流程HybridCLR->CompileDll->ActiveBuildTarget,然后拷贝HotUpdate.dll到StreamingAssets的HotUpdate.dll.bytes,然后打包运行:
可以看到即使打包到真机在热更程序集访问一个主程序集里没用到泛型Struct依然不行,这个时候就需要用到补充元数据去补充List
GameLauncher.cs
1 | /// <summary> |
因为使用到了HybridCLR相关的接口,所以主程序集还需要添加引用HybridCLR.Runtime的引用:
补充元数据的程序集可以通过HybridCLR/Generate/AotDlls之后在{project}/HybridCLRData/AssembliesPostIl2CppStrip/{target}目录找到相关用于补充元数据的程序集:
然后生成热更新程序集并拷贝到StreamingAssets目录(补充元数据的程序集也要拷贝)并打包测试:
可以看到通过补充元数据,我们正确访问到List
关于哪些Dll需要进行补充元数据,HybridCLR->Generate->AOTGenericReference时(包含在All里)会生成AOTGenericReferences.cs文件,里面有需要进行补充的元数据列表,如果不在意补充元数据过多的内存问题,可以直接使用这个列表作为依据:
HybridCLR的基础学习先到这,更多深入的学习以后实战用到深入学习。
Note:
直接在热更新程序集里访问Main程序集会提示报错,因为HotUpdate程序集没设置引用Main程序集,所以我们需要给HotUpdate程序集添加Main程序集作为Assembly Definition References。
因为主程序集有使用过List
相关,所以AOT可能已经有相关List泛型Class的元数据,在测试热更程序集的泛型访问问题时最好定义成Struct

