文章摘要
Unity等C#项目编译生成的DLL在运行时无法直接替换,主要原因有三点:首先,DLL一旦加载到AppDomain就无法卸载(除非卸载整个AppDomain,但Unity不支持);其次,运行时类型系统与原DLL强绑定,替换会导致引用丢失;最后,IL2CPP将代码静态编译为本地代码,完全缺乏动态加载机制。因此,Unity主工程DLL必须通过脚本语言或特殊解释器(如ILRuntime)实现热更新,无法直接替换。
1. 为什么Unity等C#项目,代码编译后生成DLL,无法直接在运行时替换?
1.1 DLL的加载机制
- 在C#(.NET/Mono)中,代码编译后生成DLL(程序集)。
- Unity启动时会把主工程的DLL(如Assembly-CSharp.dll)加载到AppDomain(应用程序域)中。
- 一旦程序集被加载到AppDomain,就无法卸载或替换(除非卸载整个AppDomain)。
1.2 Unity的运行时限制
- Unity的主工程代码(Assembly-CSharp.dll等)在启动时就被加载,且没有提供卸载/替换的API。
- 你不能在运行时用新DLL覆盖旧DLL,因为:
- 旧DLL已被加载到内存,文件被锁定(在某些平台)。
- .NET/Mono运行时不允许重新加载同名程序集。
- 所有对象、类型、静态变量都绑定在原DLL的类型上,替换会导致类型不一致、引用丢失。
1.3 IL2CPP的特殊性
- IL2CPP是Unity的AOT(Ahead-Of-Time)编译方案,会把C#代码编译成C++再编译成机器码。
- IL2CPP根本没有.NET/Mono的动态加载机制,所有代码都在编译时固化,运行时无法加载新DLL。
2. 传统C#运行时(Mono/IL2CPP)不支持动态卸载和替换已加载的程序集
2.1 AppDomain的局限
- .NET/Mono理论上可以通过创建新的AppDomain来加载/卸载程序集。
- 但Unity只用一个主AppDomain,不支持自定义AppDomain,也不支持卸载主AppDomain。
- 这意味着一旦DLL被加载,除非重启进程,否则无法卸载或替换。
2.2 运行时类型系统的限制
- 所有对象、类型、静态变量都和原DLL强绑定。
- 替换DLL会导致类型不一致,现有对象失效,GC无法正确回收。
2.3 IL2CPP的彻底静态化
- IL2CPP没有JIT(即时编译),所有代码都在编译时生成本地代码。
- 没有任何机制可以在运行时加载、卸载、替换代码。
3. 总结
- Unity/Mono/IL2CPP加载的DLL一旦进入内存,无法卸载或替换,因为类型系统、对象引用、静态变量等都和原DLL强绑定。
- IL2CPP更彻底,根本没有动态加载机制。
- 这就是为什么Unity等C#项目无法直接热更主工程DLL,必须用脚本语言、解释器(如ILRuntime、HybridCLR)、反射等特殊机制实现热更新。
4. 参考
下面我将从**.NET/Mono/IL2CPP的底层机制**、Unity的程序集加载流程、AppDomain的实现细节等角度,深入分析为什么Unity等C#项目无法在运行时直接替换已加载的DLL,并给出部分源码/伪代码参考。
1. .NET/Mono程序集加载与卸载机制
1.1 程序集加载流程
- .NET/Mono通过
Assembly.Load等API将DLL加载到AppDomain中。 - 加载后,CLR会为每个类型分配Type对象,所有对象实例、静态变量、方法表都与该Type绑定。
伪代码示例:
AppDomain domain = AppDomain.CurrentDomain;
Assembly asm = domain.Load("MyLogic.dll");
Type t = asm.GetType("MyLogic.MyClass");
object obj = Activator.CreateInstance(t);
1.2 卸载机制
- .NET Framework允许通过
AppDomain.Unload卸载整个AppDomain,间接卸载程序集。 - 但单个程序集无法卸载,只能卸载整个AppDomain。
- Mono/Unity只用一个主AppDomain,且不支持自定义/卸载AppDomain。
源码片段(Mono)
Mono的AppDomain实现(mono/metadata/appdomain.c):
MonoDomain* mono_domain_create_appdomain (const char *friendly_name, const char *config_file);
void mono_domain_unload (MonoDomain *domain);
Unity未暴露这些API,且主域不可卸载。
2. Unity的程序集加载流程
- Unity启动时,自动加载主工程DLL(如Assembly-CSharp.dll、Assembly-UnityScript.dll等)。
- 这些DLL被加载到主AppDomain,生命周期与进程一致。
- Unity未提供任何API允许你在运行时卸载或替换这些DLL。
Unity相关源码(C++层)
Unity的MonoManager负责DLL加载(见MonoManager::LoadAssemblies):
void MonoManager::LoadAssemblies()
{
// 加载主工程DLL
mono_domain_assembly_open(domain, "Assembly-CSharp.dll");
// ...
}
加载后,所有C#对象、类型都与此DLL绑定。
3. 类型系统与对象引用
- 加载DLL时,CLR/Mono为每个类型分配唯一的Type对象。
- 所有对象实例、静态变量、方法表都与Type绑定。
- 如果你尝试用新DLL替换旧DLL,类型信息不一致,现有对象会失效,GC无法正确管理。
伪代码说明:
// 假设已加载MyClass
object obj = new MyClass(); // 绑定到旧DLL的Type
// 替换DLL后
// obj依然指向旧Type,新DLL的MyClass是另一个Type
4. IL2CPP的特殊机制
- IL2CPP将C#代码AOT编译为C++,再编译为本地机器码。
- 所有类型、方法、元数据在编译时固化,运行时没有JIT、没有Assembly.Load、没有反射加载新类型的能力。
- 运行时根本没有“加载DLL”这个概念。
IL2CPP源码片段(il2cpp/il2cpp.cpp)
void il2cpp_codegen_initialize_method (const MethodInfo* method)
{
// 所有方法在编译时已生成,运行时只做初始化
}
5. 相关源码/文档
6. 结论
- 主AppDomain不可卸载,已加载的程序集不可替换,这是CLR/Mono/Unity的底层设计决定的。
- IL2CPP彻底没有动态加载机制,所有代码在编译时固化。
- 这就是为什么Unity等C#项目无法直接热更主工程DLL,必须用脚本解释器、IL解释器(如ILRuntime/HybridCLR)、反射等特殊机制实现热更新。
=

3369

被折叠的 条评论
为什么被折叠?



