唯一安全可卸载dll插件的方式是使用iscollectible: true的assemblyloadcontext;assembly.loadfrom加载的程序集无法卸载,易致内存泄漏,主因是依赖解析失败或跨上下文类型不兼容。

动态加载 DLL 插件在 .NET 5+ 中唯一安全可卸载的方式是使用 AssemblyLoadContext,传 isCollectible: true;用 Assembly.LoadFrom 或 Assembly.LoadFile 加载的程序集永远无法卸载,哪怕你清空所有引用、强制 GC,它仍驻留在内存里——这是生产环境最常被忽略的内存泄漏根源。
为什么 Assembly.LoadFrom 加载后调用方法总报 FileNotFoundException 或 InvalidCastException
根本原因不是 DLL 本身缺失,而是它的依赖项未被正确解析,或类型跨上下文不兼容。
-
Assembly.LoadFrom只会自动查找并加载“同目录下”的直接依赖(如Newtonsoft.Json.dll),但不会递归处理依赖的依赖;若插件依赖MyUtils.dll,而MyUtils.dll又依赖System.Buffers, Version=6.0.0.0,且当前运行时没装对应版本,就会静默失败 - 调用返回值为
object的方法时,若该object实际是 collectible ALC 中的类型,却在默认 ALC 里做as IPlugin转换,会直接返回null,而非抛异常——容易误判为逻辑错误 - 不要用
typeof(T).Assembly == loadedAssembly判断归属,LoadFrom和LoadFromAssemblyPath加载的同名程序集会被视为不同实例,恒为false - 调试建议:启用 Fusion Log(
fuslogvw.exe),看实际绑定路径和失败环节;或临时改用AssemblyLoadContext.Default.LoadFromAssemblyPath(.NET 5+)验证是否为上下文隔离问题
AssemblyLoadContext 卸载失败的三个典型表现
调用 context.Unload() 后程序集仍在内存中,常见于以下情况:
- 仍有线程正在执行该上下文中的代码(比如插件启动了后台
Task.Run或注册了未注销的事件),此时Unload()会抛InvalidOperationException,但若没捕获就静默失败 - 插件类型对象(如
ILabPlugin实例)被缓存在静态集合(如static List<ilabplugin></ilabplugin>)中,即使变量设为null,GC 也无法回收其所属上下文 - 插件内部持有对默认 ALC 中类型的引用(例如把
ILogger传入插件构造函数并长期持有),形成跨上下文强引用链,阻止卸载 - 未显式调用
context.Unload()—— 仅设isCollectible: true不等于自动卸载,必须主动触发
如何安全跨 ALC 调用插件方法而不爆类型转换异常
不能直接在默认上下文中 Activator.CreateInstance(type) 然后调用,必须让实例创建和方法执行都发生在同一 ALC 内部。
- 推荐方式:用
context.ExecuteInLoadContext<t></t>执行闭包,返回委托(如Func<iplugin></iplugin>),委托本身可跨上下文传递 - 示例:
var pluginFactory = context.ExecuteInLoadContext<func>>(() => { var t = assembly.GetType("MyPlugin.PluginImpl"); return (Func<iplugin>) (() => (IPlugin) Activator.CreateInstance(t)); });</iplugin></func>再调用pluginFactory()得到插件实例 - 避免用
MarshalByRefObject:.NET Core+ 已移除 Remoting 支持,继承它无效 - 接口定义必须放在共享程序集(如主程序的
Contracts.dll)中,且插件项目需引用该程序集(非复制副本),否则IPlugin在两个上下文中被视为不同类型
插件热重载时路径与依赖的硬约束
路径不是“能读到就行”,而是直接影响依赖解析行为和卸载可行性。
- 必须用绝对路径传给
LoadFromAssemblyPath;相对路径(如"./Plugins/xxx.dll")会因GetCurrentDirectory()变动导致后续Load失败 - 所有插件依赖项(包括
System.*以外的三方库)必须与插件 DLL 放在同一目录,AssemblyLoadContext默认只从该目录探测依赖 - 若多个插件共用同一依赖但版本不同(如 A 插件要
Newtonsoft.Json 13.0,B 要14.0),必须为每个插件创建独立AssemblyLoadContext实例,且不能共享依赖路径 - 插件 DLL 文件在加载期间被其他进程占用(如被文件管理器选中、被杀毒软件扫描),
LoadFromAssemblyPath会直接抛IOException,需提前检查文件访问权限
真正难的不是加载,而是确保卸载时没有残留引用、没有后台线程、没有跨上下文的隐式绑定——这些点一旦漏掉,插件系统会在运行数小时后突然卡死或 OOM,而日志里几乎不报错。











