C# 程序集的加载和卸载

目录

一、简介

二、实现程序集的加载和卸载

三、动态程序集

四、效果的实现

结束


一、简介

提到程序集的加载和卸载,不得不提到 AppDomain 对应的概念,AppDomain 在使用上,.NetFramework 和 .Net 、 .Net Core 用法也不太一样,下面是 AppDomain 的一些简介

AppDomain(应用程序域)在C#和.NET框架中是一个非常重要的概念,它提供了一种隔离应用程序中运行的不同部分的方式。每个AppDomain都是一个应用程序的轻量级容器,它为运行其中的代码提供了一个隔离的环境。这种隔离有助于减少应用程序不同部分之间的干扰,并使得应用程序的管理更加灵活和安全。

AppDomain 的主要功能和用途:
1.隔离性:不同的AppDomain可以加载不同的程序集(Assemblies),这些程序集之间是相互隔离的。这意味着,在一个AppDomain中发生的异常或崩溃不会影响到其他AppDomain中的代码执行。
2.安全性:AppDomain提供了一种机制来限制代码的执行权限。通过配置安全策略,可以限制特定AppDomain中的代码只能执行某些操作,从而增强应用程序的安全性。
3.资源监控和管理:开发者可以监控和管理每个AppDomain中的资源使用情况,包括内存使用、线程创建等。这对于识别和解决性能问题或资源泄露问题非常有帮助。
4.卸载和重新加载程序集:AppDomain允许在不停止整个应用程序的情况下,卸载并重新加载程序集。这对于需要动态更新应用程序功能的场景非常有用。
5.并行执行:虽然AppDomain本身不直接提供并行执行的能力,但通过使用多个AppDomain,可以并行地运行应用程序的不同部分,从而利用多核处理器的优势。

在去年我写过一篇关于 ECSharp 框架的帖子,在那个时候发就看了他加载和卸载程序集,只是都是 .Net6 的技术,.NetFramework 是用不了的,在后面我就想如何在 .NetFramework 中来实现动态更新 DLL 的技术,于是就写了这篇文章。

经常看我帖子的粉丝可能会注意到,我一直纠结如何在 Winform 上如何实现热更新技术,但是做出来的效果对我来说都不是太满意,包括之前的 自动更新(基于FTP),后面的 ECSharp 框架,和 NLua 框架,包括这次的加载和卸载DLL,甚至我还想用 Unity3d 的热更新框架 ILRuntime,配合浏览器插件 CSharpCEF,CSharpCEF 我在公司用的几个月,还是比较稳定的,但是把这么多的插件融合在一起,总体来说还是非常的麻烦的。

二、实现程序集的加载和卸载

新建一个 .NetFramework 的 Winform 项目,项目名字随意,我这里就用 HotfixTest

界面如下:

按钮的名字我也懒的改了,就用默认的名字,具体名字是什么可以看后面的代码

新建一个类 AssemblyLoader

using System;
using System.Collections.Generic;
using System.Reflection;

public class AssemblyLoader : MarshalByRefObject
{
    private Assembly _assembly;
    private Dictionary<string, object> _classDic = new Dictionary<string, object>();

    public void LoadAssembly(string path)
    {
        _assembly = Assembly.LoadFrom(path);
        _classDic.Clear();
    }


    public object GetClassInstance(string typeName)
    {
        if (_assembly == null)
        {
            Console.WriteLine("[GetClassInstance]请先加载DLL");
            return null;
        }

        if (_classDic.ContainsKey(typeName))
            return _classDic[typeName];

        var type = _assembly.GetType(typeName);
        if (type == null)
        {
            Console.WriteLine("[GetClassInstance]未找到当前类名:{0}", typeName);
            return null;
        }

        object instance = Activator.CreateInstance(type);
        _classDic[typeName] = instance;
        return instance;
    }

    public object InvokeMethod(string typeName, string methodName, object[] objects)
    {
        if (_assembly == null)
        {
            Console.WriteLine("[InvokeMethod]请先加载DLL");
            return null;
        }
        var type = _assembly.GetType(typeName);
        if (type == null)
        {
            Console.WriteLine("[InvokeMethod]未找到当前类名:{0}", typeName);
            return null;
        }
        var method = type.GetMethod(methodName);
        if (method == null)
        {
            Console.WriteLine("[InvokeMethod]未找到当前方法:{0}", methodName);
            return null;
        }
        if (!method.IsStatic)
        {
            if (!_classDic.ContainsKey(typeName))
            {
                object instance = Activator.CreateInstance(type);
                _classDic[typeName] = instance;
                return method.Invoke(instance, objects);
            }
            else
            {
                object instance = _classDic[typeName];
                return method.Invoke(instance, objects);
            }
        }
        else
            //静态方法不用实例作为参数
            return method.Invoke(null, objects);
    }
}

这里使用了一个字典 _classDic 主要是用来存储反射生成的实例,目的是如果上次实例化了,下次再调用这个类中的方法,不用重复的实例化。

AssemblyLoader 继承了 MarshalByRefObject 是为了后面的程序域而做准备的。

新建一个类 AssemblyManager,从这里开始就要用到程序域了

using System;
using System.IO;

public class AssemblyManager
{
    private AppDomain _appDomain;
    private AssemblyLoader _loader;

    /// <summary>
    /// 加载指定路径的DLL
    /// </summary>
    /// <param name="path"></param>
    public void LoadAssembly(string path)
    {
        if (!File.Exists(path))
        {
            Console.WriteLine("当前DLL路径不存在");
            return;
        }
        string suffix = Path.GetExtension(path);
        if (suffix.ToLower() != ".dll")
        {
            Console.WriteLine("当前的文件不是一个DLL");
            return;
        }

        string fileName = Path.GetFileNameWithoutExtension(path);
        //Console.WriteLine("程序域名字:{0}", fileName);

        //创建一个新的程序域
        _appDomain = AppDomain.CreateDomain(fileName);
        //在新的 AppDomain 中创建一个 AssemblyLoader 实例
        string assemblyName = typeof(AssemblyLoader).Assembly.FullName;
        string fullName = typeof(AssemblyLoader).FullName;
        _loader = (AssemblyLoader)_appDomain.CreateInstanceAndUnwrap(assemblyName, fullName);
        _loader.LoadAssembly(path);
    }

    /// <summary>
    /// 执行方法
    /// </summary>
    /// <param name="typeName">命名空间.类名</param>
    /// <param name="methodName">方法名</param>
    /// <param name="objects">参数</param>
    public object InvokeMethod(string typeName, string methodName, object[] objects)
    {
        return _loader?.InvokeMethod(typeName, methodName, objects);
    }

    /// <summary>
    /// 获取类的实例
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="typeName"></param>
    /// <returns></returns>
    public object GetClassInstance(string typeName)
    {
        return _loader?.GetClassInstance(typeName);
    }

    /// <summary>
    /// 卸载DLL
    /// </summary>
    public void UnloadAssembly()
    {
        if (_appDomain != null)
        {
            string domainName = _appDomain.FriendlyName;
            AppDomain.Unload(_appDomain);
            _appDomain = null;
            _loader = null;
            Console.WriteLine("程序域 {0} 已卸载,程序集已卸载", domainName);
        }
    }
}

程序域相关的用法可以参考微软官方,由于这个实在是太冷门了,能查到的资料很少,估计很多公司也不会去用它,你只要知道是这么用的,不报错就行了,不用关注它内部是如何实现的。

新建一个类 Tool,这个主要是给后面 DLL 来调用的,这里也是作为一个演示,作为拓展的 DLL 调用主程序的方法也是没问题的

using System;

public class Tool
{
    public static int Add(int x, int y)
    {
        int res = x + y;
        Console.WriteLine("结果:{0}", res);
        return res;
    }
}

三、动态程序集

动态程序集主要是用来在 Winform 运行时动态加载的 Dll 。

新建一个类库,我就用默认的名字和类好了,这里只是演示,做完了上面的工作,项目结构如下:

在这个类库中,你可以把 HotfixTest 这个 Winform 项目添加到引用中,如果用不上那么就不用加了。

在 Class1 中的代码如下:

using System;
using System.Threading.Tasks;

namespace ClassLibrary1
{
    public class Class1
    {
        public string Name { get; set; }

        public string SetName(string name)
        {
            Name = name;
            Console.WriteLine("名字是:{0}", name);
            return "设置成功";
        }

        public void SayHi()
        {
            Console.WriteLine("{0} 说:hello", Name);
        }

        public void StartTimer()
        {
            Swtichs();
        }

        private bool isSw = false;
        public async void Swtichs()
        {
            if (isSw) return;
            isSw = true;
            while (isSw)
            {
                await Task.Delay(1000);
                Console.WriteLine("1");
            }
        }

        public void TestAdd()
        {
            int res = Tool.Add(5, 6);
            Console.WriteLine("[TestAdd] res:{0}", res);
        }
    }
}

这里我并没有用静态类,或者静态方法,AssemblyLoader 类中进行反射执行时,会自动实例化,如果是静态类也没有关系,AssemblyLoader 类中,我都有做相关的判断,如下:

public object InvokeMethod(string typeName, string methodName, object[] objects)
{
    if (_assembly == null)
    {
        Console.WriteLine("[InvokeMethod]请先加载DLL");
        return null;
    }
    var type = _assembly.GetType(typeName);
    if (type == null)
    {
        Console.WriteLine("[InvokeMethod]未找到当前类名:{0}", typeName);
        return null;
    }
    var method = type.GetMethod(methodName);
    if (method == null)
    {
        Console.WriteLine("[InvokeMethod]未找到当前方法:{0}", methodName);
        return null;
    }
    if (!method.IsStatic)
    {
        if (!_classDic.ContainsKey(typeName))
        {
            object instance = Activator.CreateInstance(type);
            _classDic[typeName] = instance;
            return method.Invoke(instance, objects);
        }
        else
        {
            object instance = _classDic[typeName];
            return method.Invoke(instance, objects);
        }
    }
    else
        //静态方法不用实例作为参数
        return method.Invoke(null, objects);
}

在上面的方法中 method.IsStatic 是用来判断是否是静态方法,如果不是静态方法,那么就把类进行实例化,如果是静态方法,在调用时,就不必传入类的实例,如:method.Invoke(null, objects);

四、效果的实现

将 ClassLibrary1 类库生成 DLL,并将 DLL 复制到 HotfixTest 项目的 Debug 目录下(目前没用到发布模式),Debug 文件夹内文件如下:

HotfixTest.exe 这三个文件是我运行项目后自动生成的,如果你的项目没有这三个文件可以不用管,后面运行项目后自然会有了。

Winform 界面如下:

下面对界面中的按钮实现对应的功能

using System;
using System.Windows.Forms;

namespace HotfixTest
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        AssemblyManager manager = new AssemblyManager();

        private void Form1_Load(object sender, EventArgs e)
        {

        }

        //加载DLL
        private void button1_Click(object sender, EventArgs e)
        { 
            string dllPath = $"{Application.StartupPath}\\ClassLibrary1.dll";

            manager.LoadAssembly(dllPath);
            manager.InvokeMethod("ClassLibrary1.Class1", "SetName", new object[] { "张三" });
            Console.WriteLine("读取成功");
        }

        //卸载DLL
        private void button2_Click(object sender, EventArgs e)
        {
            manager.UnloadAssembly();
        }

        //调用方法1
        private void button3_Click(object sender, EventArgs e)
        {
            manager.InvokeMethod("ClassLibrary1.Class1", "SayHi", null);
        }

        //调用方法2
        private void button4_Click(object sender, EventArgs e)
        {
            manager.InvokeMethod("ClassLibrary1.Class1", "StartTimer", null);
        }

        //调用方法3
        private void button5_Click(object sender, EventArgs e)
        {
            manager.InvokeMethod("ClassLibrary1.Class1", "TestAdd", null);
        }
    }
}

运行项目,点击加载DLL按钮

有了正确的打印,就说明调用 DLL 中的方法成功了,继续点击 调用方法1, 调用方法2, 调用方法3,卸载DLL,效果如下:

卸载 DLL 后,定时器也会自动停止,这时,你可以改变 Class1 内部的逻辑,重新生成后,替换 HotfixTest Debug 目录中原来的 DLL,再次加载 DLL,调用其中的方法时,你会发现,逻辑也是最新的了。

源码:点击下载

结束

如果这个帖子对你有用,欢迎 关注 + 点赞 + 留言,谢谢

end

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

熊思宇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值