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的資料請關註WalkonNet其它相關文章!