基于.NET示例利用AssemblyLoadContext卸载托管DLL并实现版本替换
基于AssemblyLoadContext实现程序集卸载与DLL替换
问题场景
想要实现完整的插件生命周期:加载DLL(v1.0)→执行代码→卸载DLL→替换为新版本DLL(v1.1)→重新加载并执行更新代码。但当前基于微软插件示例实现的代码无法真正释放DLL,无法在不关闭主应用的情况下删除或覆盖目标文件,程序集被存储在全局List<Assembly>中。
现有代码问题
当前代码无法卸载的核心原因:
- 全局
List<Assembly>和IEnumerable<ICommand>持有了程序集及插件实例的强引用,导致GC无法回收对应的AssemblyLoadContext - 创建
PluginLoadContext后未保存引用,无法主动调用Unload()方法触发卸载
以下是现有实现代码:
全局变量与加载流程
static IEnumerable<ICommand> Plugs = Enumerable.Empty<ICommand>(); static readonly List<Assembly> PluginAssemblies = new(); static readonly List<string> PluginPath = new(); PluginPath.Add("C:\\...\\Plugin1.dll"); PluginPath.Add("C:\\...\\Plugin2.dll"); PluginPath.Add("C:\\...\\Plugin3.dll"); PluginPath.Add("C:\\...\\Plugin4.dll"); PluginPath.ForEach(P => { PluginAssemblies.Add(LoadPlugin(P)); }); Plugs = Plugs.Concat(PluginAssemblies.SelectMany(A => CreateCommands(A)));
LoadPlugin方法
static Assembly LoadPlugin(string relativePath) { string root = Path.GetFullPath(Path.Combine( Path.GetDirectoryName( Path.GetDirectoryName( Path.GetDirectoryName( Path.GetDirectoryName( Path.GetDirectoryName(typeof(Program).Assembly.Location))))))); string pluginLocation = Path.GetFullPath(Path.Combine(root, relativePath.Replace('\\', Path.DirectorySeparatorChar))); Console.WriteLine($"Loading commands from: {pluginLocation}"); PluginLoadContext loadContext = new PluginLoadContext(pluginLocation); return loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(pluginLocation))); }
CreateCommands方法
static IEnumerable<ICommand> CreateCommands(Assembly assembly) { int count = 0; foreach (Type type in assembly.GetTypes()) { if (typeof(ICommand).IsAssignableFrom(type)) { ICommand result = Activator.CreateInstance(type) as ICommand; if (result != null) { count++; yield return result; } } } if (count == 0) { string availableTypes = string.Join(",", assembly.GetTypes().Select(t => t.FullName)); throw new ApplicationException( $"Can't find any type which implements ICommand in {assembly} from {assembly.Location}.\n" + $"Available types: {availableTypes}"); } }
PluginLoadContext类
using System; using System.Reflection; using System.Runtime.Loader; namespace AppWithPlugin { class PluginLoadContext : AssemblyLoadContext { private AssemblyDependencyResolver _resolver; public PluginLoadContext(string pluginPath) { _resolver = new AssemblyDependencyResolver(pluginPath); } protected override Assembly Load(AssemblyName assemblyName) { string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); if (assemblyPath != null) { return LoadFromAssemblyPath(assemblyPath); } return null; } protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) { string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); if (libraryPath != null) { return LoadUnmanagedDllFromPath(libraryPath); } return IntPtr.Zero; } } }
解决方案
1. 重构存储结构,保存上下文引用
创建容器类统一管理插件的上下文、程序集和实例,避免分散的强引用:
public class PluginContainer { public PluginLoadContext LoadContext { get; set; } public Assembly Assembly { get; set; } public List<ICommand> Commands { get; set; } } // 替换原全局变量 static IEnumerable<ICommand> Plugs = Enumerable.Empty<ICommand>(); static readonly List<PluginContainer> PluginContainers = new(); static readonly List<string> PluginPath = new();
2. 修改加载逻辑,保存完整插件信息
调整LoadPlugin方法,返回包含上下文的容器,并更新加载流程:
static PluginContainer LoadPlugin(string pluginPath) { string pluginLocation = Path.GetFullPath(pluginPath); Console.WriteLine($"Loading commands from: {pluginLocation}"); var loadContext = new PluginLoadContext(pluginLocation); var assembly = loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(pluginLocation))); var commands = CreateCommands(assembly).ToList(); return new PluginContainer { LoadContext = loadContext, Assembly = assembly, Commands = commands }; } // 更新加载流程 PluginPath.ForEach(p => { var container = LoadPlugin(p); PluginContainers.Add(container); }); Plugs = PluginContainers.SelectMany(c => c.Commands);
3. 实现完整卸载逻辑
卸载时必须释放所有相关强引用,并触发Unload和GC回收:
static void UnloadAllPlugins() { // 1. 清空全局命令引用,释放插件实例 Plugs = Enumerable.Empty<ICommand>(); // 2. 遍历容器,调用Unload并清空内部引用 foreach (var container in PluginContainers) { container.LoadContext.Unload(); container.Commands.Clear(); container.Assembly = null; container.LoadContext = null; } // 3. 清空容器列表 PluginContainers.Clear(); // 4. 强制触发GC,加速上下文回收(按需使用) GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); }
4. 验证与排查
卸载后若仍无法操作DLL,需检查:
- 是否有事件注册未取消(插件实例订阅了主应用的事件)
- 是否存在静态变量持有插件类型或实例的引用
- 是否有未处理的异常导致实例被保留在内存中
完整生命周期示例
// 加载v1.0插件 LoadPlugins(); // 执行插件逻辑 ExecutePluginCommands(); // 卸载插件 UnloadAllPlugins(); // 替换DLL文件(确保文件操作权限) ReplacePluginDlls(); // 加载v1.1插件 LoadPlugins(); // 执行更新后的逻辑 ExecutePluginCommands();
内容的提问来源于stack exchange,提问作者Tyler
相关产品推荐
相关产品推荐

