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的一些总结的详细内容,更多关于C# AppDomain的资料请关注我们其它相关文章!

(0)

相关推荐

  • C# 忽略大小写进行字符串比较

    使用场景 字符串比较 在EF或者其他地方使用的时候,字符串的比较非常常见. 使用全部转化为大写或者小写进行比较,有时候并不能满足使用需求. 所以使用另外的字符串比较非常有意义. 代码示例 class Program { static void Main(string[] args) { CompareInfo Compare = CultureInfo.InvariantCulture.CompareInfo; string a = "AaasasaAAaasaa"; string b

  • c#的dllimport使用方法详解

    DllImport是System.Runtime.InteropServices命名空间下的一个属性类,其功能是提供从非托管DLL导出的函数的必要调用信息 DllImport属性应用于方法,要求最少要提供包含入口点的dll的名称.DllImport的定义如下: 复制代码 代码如下: [AttributeUsage(AttributeTargets.Method)]public class DllImportAttribute: System.Attribute{public DllImportA

  • C# 一个WCF简单实例

    WCF实例(带步骤) 复制代码 代码如下: <xmlnamespace prefix ="o" ns ="urn:schemas-microsoft-com:office:office" /> 本篇转自百度文档,自己试过,确实可以用. 以订票为例简单应用wcf 新建一个wcf服务应用程序 在IService1.cs定义服务契约 复制代码 代码如下: namespace WcfDemo { // 注意: 如果更改此处的接口名称 "IService

  • C#调用usb摄像头的实现方法

    1.下载AForge类库,下载地址:https://code.google.com/archive/p/aforge/downloads,我下载的版本是:AForge.NET Framework-2.2.5.exe: 2.下载安装好后,将下载类库中的Release文件夹复制到C#项目的可执行文件文件夹,即Debug文件夹下: 3.在C#项目中添加引用,右击解决方案资源管理器下的引用上,点击添加引用,通过浏览找到Debug文件夹下的Release文件夹选择要添加的引用文件:AForge.AForg

  • C#连接MySql数据库的方法

    1.要连接MySql数据库必须首先下载MySql官方的连接.net的文件,文件下载地址为http://dev.mysql.com/downloads/connector/net/6.6.html#downloads ,下载平台选择.Net&Mono,下载ZIP免安装版.2.解压缩刚才下载的mysql-connector-net-6.6.6-noinstall.zip文件,里面有几个版本选择,在这里我选V4, 选中这几个文件,然后添加到C#项目的引用中,然后就可以编写程序进行数据库的操作了. 3.

  • c#使用多线程的几种方式示例详解

    (1)不需要传递参数,也不需要返回参数 ThreadStart是一个委托,这个委托的定义为void ThreadStart(),没有参数与返回值. 复制代码 代码如下: class Program { static void Main(string[] args) { for (int i = 0; i < 30; i++) { ThreadStart threadStart = new ThreadStart(Calculate); Thread thread = new Thread(thr

  • asp.net(c#)网页跳转七种方法小结

    ①response.redirect 这个跳转页面的方法跳转的速度不快,因为它要走2个来回(2次postback),但他可以跳 转到任何页面,没有站点页面限制(即可以由雅虎跳到新浪),同时不能跳过登录保护.但速度慢是其最大缺陷!redirect跳转机制:首先是发送一个http请求到客户端,通知需要跳转到新页面,然后客户端在发送跳转请求到服务器端.需要注意的是跳转后内部空间保存的所有数据信息将会丢失,所以需要用到session. 实例 Example that uses Redirect [C#;

  • C# Stream 和 byte[] 之间的转换

    /* - - - - - - - - - - - - - - - - - - - - - - - -   * Stream 和 byte[] 之间的转换  * - - - - - - - - - - - - - - - - - - - - - - - */ /// <summary> /// 将 Stream 转成 byte[] /// </summary> public byte[] StreamToBytes(Stream stream) {     byte[] bytes 

  • c#处理3种json数据的实例

    网络中数据传输经常是xml或者json,现在做的一个项目之前调其他系统接口都是返回的xml格式,刚刚遇到一个返回json格式数据的接口,通过例子由易到难总结一下处理过程,希望能帮到和我一样开始不会的朋友. 一.C#处理简单json数据 json数据: 复制代码 代码如下: {"result":"0","res_info":"ok","queryorder_info":"info"} 我这

  • C#中HttpWebRequest的用法详解

    本文实例讲述了C#中HttpWebRequest的用法.分享给大家供大家参考.具体如下: HttpWebRequest类主要利用HTTP 协议和服务器交互,通常是通过 GET 和 POST 两种方式来对数据进行获取和提交.下面对这两种方式进行一下说明: GET 方式: GET 方式通过在网络地址附加参数来完成数据的提交,比如在地址 http://www.jb51.net/?hl=zh-CN 中,前面部分 http://www.jb51.net表示数据提交的网址,后面部分 hl=zh-CN 表示附

随机推荐