目录
一、简介
提到程序集的加载和卸载,不得不提到 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