.NET(Unity)模组依赖库多版本共存方案技术咨询
解决方案:实现Unity模组间工具库版本隔离的确定性方案
针对你遇到的Unity模组依赖版本冲突问题,以下是三个无需模组作者复杂配置的确定性实现方案,按适用场景优先级排序:
方案1:Assembly Load Context(ALC)隔离加载(Unity 2021+ 推荐)
利用.NET Core引入的Assembly Load Context实现程序集的独立加载,每个模组拥有专属的加载上下文,彻底隔离不同版本的工具库。
实现步骤(完全封装在NuGet包中,模组作者无感知)
- 强名称签署工具库:给每个版本的工具库生成唯一强名称(包含版本号、公钥令牌),确保程序集标识唯一。
- 开发引导程序集:创建
UtilityBootstrapper程序集,作为NuGet包的强制依赖,提供静态加载方法:public static class UtilityLoader { private static readonly Dictionary<string, AssemblyLoadContext> _modAlcs = new Dictionary<string, AssemblyLoadContext>(); public static T GetUtilityInstance<T>() where T : class { // 获取调用模组的程序集,确定模组目录 var callingAssembly = Assembly.GetCallingAssembly(); var modId = callingAssembly.GetName().Name; var modDir = Path.GetDirectoryName(callingAssembly.Location); // 创建或复用模组专属ALC if (!_modAlcs.TryGetValue(modId, out var alc)) { alc = new AssemblyLoadContext(modId, isCollectible: true); // 添加模组目录作为加载源 alc.Resolving += (ctx, name) => { if (name.Name != "UtilityLib") return null; var dllPath = Path.Combine(modDir, $"{name.Name}.dll"); return File.Exists(dllPath) ? ctx.LoadFromAssemblyPath(dllPath) : null; }; _modAlcs[modId] = alc; } // 加载对应版本的工具库并返回实例 var utilityAssembly = alc.LoadFromAssemblyPath(Path.Combine(modDir, "UtilityLib.dll")); var targetType = utilityAssembly.GetType(typeof(T).FullName); return (T)Activator.CreateInstance(targetType); } } - NuGet包自动化配置:在NuGet包的
.targets文件中,自动将引导程序集和工具库DLL复制到模组输出目录,无需模组作者手动复制。 - 模组作者使用方式:仅需一行代码获取工具库实例,无需额外配置:
var myUtility = UtilityLoader.GetUtilityInstance<IMyUtility>();
优势
- 完全隔离不同版本的程序集,无类型冲突风险
- 支持程序集卸载(Collectible ALC),减少内存占用
- Unity 2021+ 原生支持,兼容性好
方案2:版本化命名空间+Facade封装(兼容所有Unity版本)
通过给每个版本的工具库分配独立命名空间,让.NET/Unity将不同版本的类型视为完全独立的实体,彻底避免冲突。
实现步骤(封装在NuGet包中,模组作者无感知)
- 自动版本化命名空间:使用MSBuild任务在NuGet打包时,自动给工具库的所有类型添加版本前缀命名空间:
- 1.0.0版本:
UtilityLib.v1_0_0.Feature - 2.0.0版本:
UtilityLib.v2_0_0.Feature
示例MSBuild任务片段(添加到工具库csproj):
<Target Name="VersionizeNamespaces" BeforeTargets="Compile"> <ItemGroup> <Compile Include="**/*.cs" /> </ItemGroup> <Exec Command="dotnet tool run roslynator rewrite $(Compile) --namespace-prefix "UtilityLib.v$(Version.Replace('.', '_'))"" /> </Target> - 1.0.0版本:
- 提供无版本Facade程序集:创建
UtilityLib.Facade程序集,对外暴露统一的无版本命名空间(如UtilityLib.Feature),内部通过编译时绑定转发到对应版本的类型:// Facade中的类型,编译时绑定到当前版本的工具库类型 public class Feature : UtilityLib.v2_0_0.Feature { } - NuGet包配置:将Facade程序集设为默认引用,工具库本体设为私有依赖,自动复制到模组目录。
优势
- 兼容所有Unity版本(包括Mono和IL2CPP)
- 无运行时反射开销,性能稳定
- 模组作者完全感知不到版本化命名空间的存在,使用方式与普通库一致
IL2CPP注意事项
- 给工具库的所有类型添加
[UnityEngine.Scripting.Preserve]属性,避免被IL2CPP裁剪 - Facade程序集需与工具库版本严格对应,确保编译时绑定正确
方案3:强名称+AssemblyResolve事件拦截(旧版Unity折中方案)
针对无法使用ALC的旧版Unity,通过拦截程序集加载事件,强制从模组目录加载对应版本的工具库。
实现步骤
- 强名称签署工具库:确保每个版本的工具库拥有唯一的版本号和公钥令牌。
- 开发绑定重定向引导程序:创建
UtilityBindingRedirect程序集,提供初始化方法:public static class UtilityBindingRedirect { public static void Initialize() { AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => { var requestedAssembly = new AssemblyName(args.Name); if (requestedAssembly.Name != "UtilityLib") return null; // 获取调用模组的目录 var callingAssembly = Assembly.GetCallingAssembly(); var modDir = Path.GetDirectoryName(callingAssembly.Location); var targetDllPath = Path.Combine(modDir, $"{requestedAssembly.Name}.dll"); if (!File.Exists(targetDllPath)) return null; // 验证加载的程序集是否匹配请求的版本和公钥 var loadedAssembly = Assembly.LoadFrom(targetDllPath); var loadedName = loadedAssembly.GetName(); if (loadedName.Version == requestedAssembly.Version && loadedName.GetPublicKeyToken().SequenceEqual(requestedAssembly.GetPublicKeyToken())) { return loadedAssembly; } return null; }; } } - NuGet包配置:自动将引导程序集和工具库DLL复制到模组目录,并在模组的初始化代码中自动注入
UtilityBindingRedirect.Initialize()调用(通过MSBuild任务修改模组的csproj)。
优势
- 无需修改工具库的命名空间
- 兼容Unity 2019及以前版本
注意事项
- 无法实现程序集卸载,长期运行可能导致内存占用增加
- 需确保模组的初始化代码优先调用引导程序的初始化方法
内容的提问来源于stack exchange,提问作者Morgan Touverey Quilling
相关产品推荐
相关产品推荐

