导读:本期聚焦于小伙伴创作的《C#如何在运行时动态加载和卸载程序集并调用其中的方法》,敬请观看详情。把插件逻辑塞进主程序重新发版太笨重,不少团队希望把业务模块做成独立DLL,在软件跑起来之后再按需装载。Net Core之后老的AppDomain卸载方式已失效,取而代之的是AssemblyLoadContext这套轻量隔离机制。它允许你新建一个上下文专门加载某个插件程序集,通过反射拿到类型与MethodInfo执行目标方法,最后释放上下文并切断文件占用实现真正卸载。需要注意依赖项解析、类型透传陷阱以及卸载事件回调,否则容易出现内存泄漏或文件被锁死。下面从原理到代码拆解完整实现路径。

在C#中,运行时动态加载和卸载程序集的核心在于使用AssemblyLoadContext。与旧版Framework依赖AppDomain不同,.NET Core及后续版本通过AssemblyLoadContext提供隔离且可卸载的程序集加载环境,使插件化架构和热更新成为可能。借助反射,我们可以在不知道具体类型的情况下调用方法。

C#如何在运行时动态加载和卸载程序集并调用其中的方法

一、为什么需要AssemblyLoadContext

在早期.NET Framework中,开发者习惯用AppDomain来加载和卸载DLL,但这种方式开销大且跨域调用复杂。从.NET Core开始,AppDomain的卸载能力被削弱,官方引入了AssemblyLoadContext(简称ALC)作为更轻量的替代方案。ALC不仅能够将程序集加载到独立上下文中避免污染默认上下文,还支持通过Unload方法释放资源,从而解决DLL文件被占用无法替换的问题。

使用ALC时,每个上下文拥有自己的程序集解析逻辑。当你从文件夹动态读取一个插件DLL时,可以新建一个ALC实例,让它优先从该文件夹解析依赖,而不是去全局缓存找。这种隔离对插件系统尤为关键,因为不同插件可能引用同一库的不同版本,ALC能避免冲突。同时,只有将ALC标记为可卸载,并在其中加载的程序集没有外部强引用时,卸载才会成功。

二、动态加载并调用方法的基础实现

下面示例展示如何创建一个可卸载的自定义AssemblyLoadContext,加载指定路径的DLL,并通过反射调用其中某个类的静态方法。我们定义一个继承自AssemblyLoadContext的类,并覆写Load方法以控制依赖解析。

using System;
using System.Reflection;
using System.IO;

public class PluginLoadContext : AssemblyLoadContext
{
    private AssemblyDependencyResolver _resolver;

    public PluginLoadContext(string pluginPath) : base(isCollectible: true)
    {
        // 根据插件主DLL路径解析其依赖
        _resolver = new AssemblyDependencyResolver(pluginPath);
    }

    protected override Assembly Load(AssemblyName name)
    {
        // 尝试从插件目录解析依赖
        string path = _resolver.ResolveAssemblyToPath(name);
        if (path != null)
        {
            return LoadFromAssemblyPath(path);
        }
        return null;
    }
}

class Program
{
    static void Main()
    {
        string dllPath = @"C:pluginsMyPlugin.dll";
        var alc = new PluginLoadContext(dllPath);
        Assembly asm = alc.LoadFromAssemblyPath(dllPath);

        // 假设插件中有类 Demo.PluginRunner,包含静态方法 Run
        Type type = asm.GetType("Demo.PluginRunner");
        MethodInfo method = type.GetMethod("Run");
        string result = (string)method.Invoke(null, new object[] { "hello" });
        Console.WriteLine(result);
    }
}

上述代码通过PluginLoadContext加载MyPlugin.dll,并利用AssemblyDependencyResolver自动处理同目录下的依赖项。调用Invoke执行方法时,如果方法是实例方法,则需要先创建对象实例再传入。注意,LoadFromAssemblyPath会将文件锁住,直到ALC卸载才会释放,因此不要频繁用同一路径重复加载。

在真实项目中,插件方法往往有返回值或抛出异常。建议将反射调用包在try-catch中,并将插件接口抽象为公共契约程序集,主程序引用该契约而插件实现它,这样能减少直接使用字符串类型名的脆弱性。不过即便如此,跨ALC传递类型时仍需小心,因为如果主程序直接引用了插件内部的具体类,会导致ALC无法卸载。

三、实现真正的卸载

仅仅丢弃ALC变量并不能卸载程序集,必须确保没有来自默认上下文的引用。.NET的垃圾回收器会在ALC无强引用且内部程序集类型无存活对象时,通过终结器触发卸载。我们通常使用WeakReference监听卸载状态。

using System;
using System.Reflection;
using System.Runtime.CompilerServices;

class UnloadDemo
{
    static WeakReference StartAndUnload()
    {
        string dllPath = @"C:pluginsMyPlugin.dll";
        var alc = new PluginLoadContext(dllPath);
        Assembly asm = alc.LoadFromAssemblyPath(dllPath);
        Type type = asm.GetType("Demo.PluginRunner");
        type.GetMethod("Run").Invoke(null, new object[] { "unload test" });

        // 保存弱引用用于后续检查
        WeakReference weakRef = new WeakReference(alc, trackResurrection: true);
        alc.Unload();
        return weakRef;
    }

    static void Main()
    {
        var weak = StartAndUnload();
        // 强制GC以帮助卸载
        for (int i = 0; i < 3; i++)
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }
        Console.WriteLine(weak.IsAlive ? "尚未卸载" : "已成功卸载");
    }
}

代码中调用alc.Unload()只是发出卸载请求,实际释放依赖GC回收。循环调用GC.Collect能加速这一过程,但生产环境不应频繁强制GC。若weak.IsAlive仍为true,通常是因为主程序仍持有插件类型的对象、事件订阅未取消,或静态字段引用了插件类型。

为避免泄漏,插件内部不应在静态构造函数中向主程序上下文的类型注册回调;若必须通信,可使用弱事件模式或依赖注入容器在ALC卸载时清理。另外,文件锁的释放也依赖卸载完成,因此在热更新场景中,只有当WeakReference确认死亡后才能安全覆盖DLL。

四、常见陷阱与优化建议

动态加载中最容易出错的是类型透传。如果主程序直接引用了插件DLL中的具体类(哪怕只是作为返回类型),CLR会将该类加载到默认上下文,导致ALC永远无法卸载。正确做法是定义单独的契约程序集(如IPlugin接口),主程序和插件都引用它,但主程序绝不引用插件实现程序集。

问题原因对策
DLL文件无法删除ALC未卸载或仍有引用使用WeakReference确认卸载后再操作文件
找不到依赖项Load方法未正确解析利用AssemblyDependencyResolver或手动LoadFromAssemblyPath
类型转换异常同名称类型分属不同上下文通过接口或序列化传递数据

此外,对于需要频繁加载卸载的高性能场景,应避免在插件中分配大对象或开启非托管资源而未释放。可结合DynamicDependency特性或提前裁剪无用代码,减小插件体积。若方法调用极频繁,反射带来的开销可通过委托缓存(如CreateDelegate)缓解,将MethodInfo转为强类型委托后调用几乎无额外成本。

综合来看,C#在运行时动态加载卸载程序集已不再是难题,关键在于理解AssemblyLoadContext的隔离边界与垃圾回收机制。只要契约清晰、引用干净,就能构建稳定的插件化系统。

AssemblyLoadContext反射DynamicDependency修改时间:2026-08-05 23:48:36

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。