C# 中 AppDomain 的一些理解

前言

一直想写一个这样的程序:与其它的程序完全解耦,但可以动态的加载其它程序,并执行其中的特定方法,执行完后可以卸载,完全不影响该程序本身。最近无意间发现了 C# 中 AppDomain,再加上反射,感觉就是我所需要的。

基本概念

应用程序域为安全性、可靠性、版本控制以及卸载程序集提供了隔离边界。 应用程序域通常由运行时宿主创建,运行时宿主负责在运行应用程序之前引导公共语言运行时。

应用程序域所提供的隔离具有以下优点:

(1)在一个应用程序中出现的错误不会影响其他应用程序。 因为类型安全的代码不会导致内存错误,所以使用应用程序域可以确保在一个域中运行的代码不会影响进程中的其他应用程序。

(2)能够在不停止整个进程的情况下停止单个应用程序。 使用应用程序域使您可以卸载在单个应用程序中运行的

注意:不能卸载单个程序集或类型。只能卸载整个域。

一切的根源,都是因为只有 Assembly.Load 方法,而没有 Assembly.Unload 方法,只能卸载其所在的 AppDomain。

实践

1. 首先准备一个控制台小程序

操作为读取配置文件(为测试 AppDomain 中配置文件的读取情况),并使用 Newtonsoft.Json 将其序列化为 json(为测试 AppDomain 中加载程序中的第三方引用情况),在控制台输出。项目名为 ReadPrint, 将其编译为 exe 文件,并存放在 D:\AppDomainModules 中。

using Newtonsoft.Json;

using System;
using System.Configuration; namespace ReadPrint
{
class Program
{
static void Main(string[] args)
{
DoSomething();
} public static void DoSomething()
{
Person person = new Person
{
Account = ConfigurationManager.AppSettings["Account"],
Name = ConfigurationManager.AppSettings["Name"],
Age = int.Parse(ConfigurationManager.AppSettings["Age"])
}; Console.WriteLine(JsonConvert.SerializeObject(person));
Console.ReadLine();
} class Person
{
public string Account { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
}
}

为了查看方便定义了 DoSomething 来执行相关方法。也可以直接写在 Main 方法中,调用时需要传入参数 args。因为最终测试 AppDomain 的程序也打算使用控制台应用,也使用控制台应用来写这个小程序。

2. 编写使用 AppDomain 的程序

主要包含 AssemblyLoader.cs 文件用于封装使用细节,和 Program.cs 主程序文件。

AssemblyLoader.cs
using System;
using System.IO;
using System.Reflection; namespace AppDomainTest
{
public class AssemblyDynamicLoader
{
private AppDomain appDomain;
public readonly RemoteLoader remoteLoader;
public AssemblyDynamicLoader()
{
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationName = "ApplicationLoader";
setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
setup.PrivateBinPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules");
setup.CachePath = setup.ApplicationBase;
setup.ShadowCopyFiles = "true"; # 重点
setup.ShadowCopyDirectories = setup.ApplicationBase;
setup.ConfigurationFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules", "ReadPrint.exe.config");
//AppDomain.CurrentDomain.SetShadowCopyFiles();
this.appDomain = AppDomain.CreateDomain("ApplicationLoaderDomain", null, setup);
String name = Assembly.GetExecutingAssembly().GetName().FullName;
this.remoteLoader = (RemoteLoader)this.appDomain.CreateInstanceAndUnwrap(name, typeof(RemoteLoader).FullName); # 重点
} public void Unload()
{
try
{
if (appDomain == null) return;
AppDomain.Unload(this.appDomain);
this.appDomain = null;
}
catch (CannotUnloadAppDomainException ex)
{
throw ex;
}
}
} public class RemoteLoader : MarshalByRefObject
{
private Assembly _assembly; public void LoadAssembly(string assemblyFile)
{
try
{
_assembly = Assembly.LoadFrom(assemblyFile);
}
catch (Exception ex)
{
throw ex;
}
} public void ExecuteMothod(string typeName, string methodName)
{
if (_assembly == null)
{
return;
}
var type = _assembly.GetType(typeName);
type.GetMethod(methodName).Invoke(Activator.CreateInstance(type), new object[] { });
}
}
}

其中类 RemoteLoader 为加载程序集的类,AssemblyDynamicLoader 类在此基础上封装了新建 AppDomain 的细节

在 AssemblyDynamicLoader 的构造函数中,为了测试方便,硬编码了一些内容,如 程序集文件查找路径 PrivateBinPath 为当前程序执行目录下面的 Modules 目录,配置文件 ConfigurationFile 为 Modules 目录中的 ReadPrint.exe.config, 以及创建新 AppDomain 时的程序集名称。

AppDomainSetup 的属性 ShadowCopyFiles(似乎可以译为“卷影复制”) 代表是否锁定读取的程序集。如果设置为 true,则将程序集读取至内存,不锁定其文件,这也是热更新的前提;否则在程序执行期间这些程序集文件会被锁定,不能变化。

AppDomain 的方法 CreateInstanceAndUnwrap 意为在 AppDomain 的实例中创建指定类型的新实例,并返回。

在 RemoteLoader 的 ExecuteMethod 中,传入的参数硬编码为空。在实际使用时应当根据实际传入参数。

Program.cs
using System;
using System.IO; namespace AppDomainTest
{
class Program
{
static void Main(string[] args)
{
string modulesPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules");
DirectoryInfo di = new DirectoryInfo(modulesPath);
if (!di.Exists)
{
di.Create();
} string remotePath = @"D:\AppDomainModules\"; string[] fileNames = new string[] { "ReadPrint.exe", "Newtonsoft.Json.dll", "ReadPrint.exe.config" };
foreach(var fileName in fileNames)
{
FileInfo fi = new FileInfo(Path.Combine(remotePath, fileName));
fi.CopyTo(Path.Combine(modulesPath, fileName), true);
} AssemblyDynamicLoader adl = new AssemblyDynamicLoader();
adl.remoteLoader.LoadAssembly(Path.Combine(modulesPath, "ReadPrint.exe"));
adl.remoteLoader.ExecuteMethod("ReadPrint.Program", "DoSomething");
adl.Unload();
}
}
}

在主程序文件中,创建 Modules 文件夹,拷贝程序文件、库文件和配置文件。程序运行结果:

可以看到成功调用了我们定义的 DoSomething 方法。

一些思考

1. 为什么不使用 AppDomain 实例的 Load 方法加载程序集

使用此方法,会首先在主程序的 AppDomain 中加载一遍程序集(和依赖),再移至我们创建的 AppDomain 中(特别注意,此时不会从我们新建的 AppDomain 的 PrivateBinPath 中搜索和加载)。

缺点有二,一是随着程序的运行,可能会加载大量的程序集,因此主程序的 AppDomain 也要加载大量程序集,而程序集无法单独卸载,只有在主程序停止后才会卸载,其间必然越积越多,极不优雅二是无法自定目录,主程序加载程序集和依赖时只会在其指定的 PrivateBinPath 中搜索,因此其它模块所有需要的程序集文件都堆积在同一个目录中,条理不清。

验证

修改 AssemblyDynamicLoader.cs 中的代码,改为直接在构造函数里面执行程序加载,其它不变,并查看我们新建的 AppDomain 中已加载的程序集:

	    //String name = Assembly.GetExecutingAssembly().GetName().FullName;
//this.remoteLoader = (RemoteLoader)this.appDomain.CreateInstanceAndUnwrap(name, typeof(RemoteLoader).FullName); Assembly assembly = this.appDomain.Load("ReadPrint");
Type t = assembly.GetType("ReadPrint.Program");
MethodInfo mi = t.GetMethod("DoSomething");
//mi.Invoke(Activator.CreateInstance(t), new object[] { }); var tmp = this.appDomain.GetAssemblies();

此处最为奇怪的是,尽管我们在上面指定了自己 AppDomain 的 PrivateBinPath 和 配置文件,执行时依然找的是主程序的 PrivateBinPath 和 配置文件,因此将执行的那一行代码注释。

修改 Program.cs 中的代码,改为仅调用 AssemblyDynamicLoader 的构造函数,其它不变,并查看主程序 AppDomain 中已加载的程序集:

	    AssemblyDynamicLoader adl = new AssemblyDynamicLoader();
//adl.remoteLoader.LoadAssembly(Path.Combine(modulesPath, "ReadPrint.exe"));
//adl.remoteLoader.ExecuteMethod("ReadPrint.Program", "DoSomething");
//adl.Unload(); var tmp = AppDomain.CurrentDomain.GetAssemblies(); Console.ReadLine();

结果如图所示:

2. 为什么要使用类似于代理的类 RemoteLoader, 而不直接使用 CreateInstanceAndUnwrap 创建加载进来程序集的实例

直接使用会提示如下错误:

需要注意的是,RemoteLoader 类继承了 MarshalByRefObject,而继承此类的应用可以跨 AppDomain 使用。此处猜测虽然可以在主程序中创建新的 AppDomain,但新的 AppDomain 依然无法完全摆脱主程序。

我们不可能要求所有被调用的模块都继承此类,因此使用代理类 RemoteLoader。执行的过程为:创建新的 AppDomain;在其中新建代理类 RemoteLoader,代理类帮助我们加载不同的模块和依赖,并代替我们调用模块。CreateInstanceAndUnwrap 实际上就是在新建的 AppDomain 中创建并实例化代理类,此后所有的工作均在新的 AppDomain 中进行。

后记

代码中使用了很多硬编码。实际中,应向主程序指出要调用的模块路径、依赖文件路径和配置文件路径,由主程序拷贝至临时目录,再使用 AssemblyDynamicLoader 创建新的 AppDomain 和执行。

感觉大部分时候查看文章都是为了解决一些问题,因此本文把使用方法放在了前面,把详细说明放在了后面,也算是一些优化了XD。

参考

C#学习笔记-----基于AppDomain的"插件式"开发

关于MarshalByRefObject的解释

MSDN文档

C# 中 AppDomain 的一些理解的更多相关文章

  1. SQL SERVER 2005/2008 中关于架构的理解(二)

    本文上接SQL SERVER 2005/2008 中关于架构的理解(一)      架构的作用与示例 用户与架构(schema)分开,让数据库内各对象不再绑在某个用户账号上,可以解决SQL SERVE ...

  2. SQL SERVER 2005/2008 中关于架构的理解(一)

    SQL SERVER 2005/2008 中关于架构的理解(一) 在一次的实际工作中碰到以下情况,在 SQL SERVER 2008中,新建了一个新用户去访问几张由其他用户创建的表,但是无法进行查询, ...

  3. C++中 类的构造函数理解(一)

    C++中 类的构造函数理解(一) 写在前面 这段时间完成三个方面的事情: 1.继续巩固基础知识(主要是C++ 方面的知识) 2.尝试实现一个iOS的app,通过完成app,学习iOS开发中要用到的知识 ...

  4. ECshop中的session机制理解

    ECshop中的session机制理解     在网上找了发现都是来之一人之手,也没有用自己的话去解释,这里我就抛砖引玉,发表一下自己的意见,还希望能得到各界人士的指导批评! 此session机制不需 ...

  5. [开发技巧]·Numpy中对axis的理解与应用

    [开发技巧]·Numpy中对axis的理解与应用 1.问题描述 在使用Numpy时我们经常要对Array进行操作,如果需要针对Array的某一个纬度进行操作时,就会用到axis参数. 一般的教程都是针 ...

  6. 【C++】类中this指针的理解

    转自 苦涩的茶https://www.cnblogs.com/liushui-sky/p/5802981.html C++类中this指针的理解 先要理解class的意思.class应该理解为一种类型 ...

  7. C#中AppDomain.CurrentDomain.BaseDirectory与Application.StartupPath的区别

    // 获取程序的基目录. System.AppDomain.CurrentDomain.BaseDirectory // 获取模块的完整路径. System.Diagnostics.Process.G ...

  8. Java中线程同步的理解 - 其实应该叫做Java线程排队

    Java中线程同步的理解 我们可以在计算机上运行各种计算机软件程序.每一个运行的程序可能包括多个独立运行的线程(Thread). 线程(Thread)是一份独立运行的程序,有自己专用的运行栈.线程有可 ...

  9. OpenGL中的像素包装理解

    OpenGL中的像素包装理解 像素包装 位图和像素图很少会被紧密包装到内存中.在许多硬件平台上,考虑到性能的原因位图和像素图的每一行的数据会从特殊的字节对齐地址开始.绝大多数编译 器会自动把变量和缓冲 ...

随机推荐

  1. 解决Win10用户VS Code的C/C++更新到1.6.0后无法调试的问题

    今天突然遇到一个问题 Win10上 vscode C++突然无法正常调试 在运行调试后 编译成功后没有任何提示 直接就停止了 没有错误 不运行程序 尝试重新写一遍launch.json 自动生成lau ...

  2. 【笔记】求数据前n个主成分以及对高维数据映射为低维数据

    求数据前n个主成分并进行高维数据映射为低维数据的操作 求数据前n个主成分 先前的将多个样本映射到一个轴上以求使其降维的操作,其中的样本点本身是二维的样本点,将其映射到新的轴上以后,还不是一维的数据,对 ...

  3. 利用Nginx实现反向代理web服务器

    一.Nginx简介 Nginx是一个很强大的高性能Web服务器和反向代理服务器,它具有很多非常优越的特性: 可以高并发连接 内存消耗少 成本低廉 配置文件非常简单 支持Rewrite重写 内置的健康检 ...

  4. VLAN-1 基础配置及access接口

    一.实验拓扑图 二.实验编制 三.实验步骤 1.给对应的PC设置对应的IP和掩码还有接口,以及根据需要划分不同的vlan区域,再用文本标记出不同部门. 2.启动设备(全选) 3.首先用ping命令检查 ...

  5. Jetpack Compose学习(2)——文本(Text)的使用

    原文: Jetpack Compose学习(2)--文本(Text)的使用 | Stars-One的杂货小窝 对于开发来说,文字最为基础的组件,我们先从这两个使用开始吧 本篇涉及到Kotlin和DSL ...

  6. Linux下的段错误(Segmentation fault)

    Linux下的段错误(Segmentation fault) 段错误是指:访问了系统分配给程序的内存空间之外起的内存空间,比如: 访问不存在的地址 访问受系统保护的地址 访问了只读内存地址 内存访问越 ...

  7. 题解 Game

    传送门 一有「字典序最大」什么的的就懵了--这题我颓的std 首先可以发现全局最大得分很好统计,我们令它为 \(k\) 然后我们尝试构造方案,但发现无论怎么放都可能会有后效性 发现对于一个位置,可以放 ...

  8. Java:学习什么是多线程

    线程是什么 进程是对CPU的抽象,而线程更细化了进程的运行流程 先看一下这个图 线程和进程的关系有 进程中就是线程在执行,所有(主)线程执行完了进程也就结束了 多个线程从1秒钟是同时运行完成,从1纳秒 ...

  9. C# 正则表达式的重点知识 1

  10. vsftpd - FTP 服务器安装

    由于要将本地程序上传至云服务器中,所以需要给云服务器端安装ftp服务器.记录一下ftp的安装过程,以便以后使用.服务器端所用系统为Ubuntu16.04. 1. 安装ftp服务器, apt-get i ...