操做系統和運行時環境一般會在應用程序間提供某種形式的隔離。 例如,Windows 使用進程來隔離應用程序。 爲確保在一個應用程序中運行的代碼不會對其餘不相關的應用程序產生不良影響,這種隔離是必需的。這種隔離能夠爲應用程序域提供安全性、可靠性, 而且爲卸載程序集提供了可能。測試
在 .NET中應用程序域AppDomain是CLR的運行單元,它能夠加載應用程序集Assembly、建立對象以及執行程序。ui
在 CLR 裏、AppDomain就是用來實現代碼隔離的,每個AppDomain能夠單首創建、運行、卸載。this
關於AppDomain中的未處理異常:spa
若是默認AppDomain監聽了 UnhandledException 事件,任何線程的任何未處理異常都會引起該事件,不管線程是從哪一個AppDomain中開始的。
若是一個線程開始於一個已經監聽了 UnhandledException事件的 app domain, 那麼該事件將在這個app domain 中引起。
若是這個app domian 不是默認的app domain, 而且 默認 app domain 中也監聽了 UnhandledException 事件, 那麼 該事件將會在兩個app domain 中引起。
CLR啓用時,會建立一個默認的AppDomain,程序的入口點(Main方法)就是在這個默認的AppDomain中執行。
AppDomain是能夠在運行時進行動態的建立和卸載的,正因如此,才爲插件技術提供了基礎(注:應用程序集和類型是不能卸載的,只能卸載整個AppDomain)。
AppDomain和其餘概念之間的關係
一、AppDomain vs 進程Process
AppDomain被建立在Process中,一個Process內能夠有多個AppDomain。一個AppDomain只能屬於一個Process。
二、AppDomain vs 線程Thread
應該說二者之間沒有關係,AppDomain出現的目的是隔離,隔離對象,而 Thread 是 Process中的一個實體、是程序執行流中的最小單元,保存有當前指令指針 和 寄存器集合,爲線程(上下文)切換提供可能。若是說有關係的話,能夠牽強的認爲一個Thread可使用多個AppDomain中的對象,一個AppDomain中可使用多個Thread.
三、AppDomain vs 應用程序集Assembly
Assembly是.Net程序的基本部署單元,它能夠爲CLR提供元數據等。
Assembly不能單獨執行,它必須被加載到AppDomain中,而後由AppDomain建立程序集中的類型 及 對象。
一個Assembly能夠被多個AppDomain加載,一個AppDomain能夠加載多個Assembly。
每一個AppDomain引用到某個類型的時候須要把相應的assembly在各自的AppDomain中初始化。所以,每一個AppDomain會單獨保持一個類的靜態變量。
四、AppDomain vs 對象object
任何對象只能屬於一個AppDomain,AppDomain用來隔離對象。 同一應用程序域中的對象直接通訊、不一樣應用程序域中的對象的通訊方式有兩種:一種是跨應用程序域邊界傳輸對象副本(經過序列化對對象進行隱式值封送完成),一種是使用代理交換消息。
2、建立 和 卸載AppDomain
前文已經說明了,咱們能夠在運行時動態的建立和卸載AppDomain, 有這樣的理論基礎在、咱們就能夠熱升級應用程序了 。
那就讓咱們來看一下如何建立和卸載AppDomain吧
建立:
AppDomainSetup objSetup = new AppDomainSetup(); objSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; this.domain = AppDomain.CreateDomain("RemoteAppDomain", null, objSetup);
建立AppDomain的邏輯很是簡單:使用 AppDomain.CreateDomain 靜態方法、傳遞了一個任意字符串 和 AppDomainSetup 對象。
卸載:
AppDomain.Unload(this.domain);
卸載就更簡單了一行代碼搞定:AppDomain.Unload 靜態方法,參數就一個 以前建立的AppDomain對象。
3、在新AppDomain中建立對象
上文已經說了建立AppDomain了,可是建立的新AppDomain倒是不包含任何對象的,只是一個空殼子。那麼如何在新的AppDomain中建立對象呢?
this.remoteIPlugin = this.domain.CreateInstance("PluginDemo.NewDomain", "PluginDemo.NewDomain.Plugin").Unwrap() as IPlugin;
使用剛建立的AppDomain對象的實例化方法: this.domain.CreateInstance,傳遞了兩個字符串,分別爲 assemblyName 和 typeName.
而且該方法的重載方法 和 類似功能的重載方法多達十幾個。
4、影像複製程序集
建立、卸載AppDomain都有、建立新對象也能夠了,可是若是想完成熱升級,還有一點小麻煩,那就是一個程序集被加載後會被鎖定,這時候是沒法對其進行修改的。
因此就須要打開 影像複製程序集 功能,這樣在卸載AppDomain後,把須要升級的應用程序集進行升級替換,而後再建立新的AppDomain便可了。
打開 影像複製程序集 功能,須要在建立新的AppDomain時作兩步簡單的設定便可:
AppDomainSetup objSetup = new AppDomainSetup(); objSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
// 打開 影像複製程序集 功能 objSetup.ShadowCopyFiles = "true"; // 雖然此方法已經被標記爲過期方法, msdn備註也提倡不使用該方法, // 可是 以.net 4.0 + win10環境測試,還必須調用該方法 不然,即使卸載了應用程序域 dll 仍是未被解除鎖定 AppDomain.CurrentDomain.SetShadowCopyFiles(); this.domain = AppDomain.CreateDomain("RemoteAppDomain", null, objSetup);
5、簡單的Demo
現有一接口IPlugin:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Input; namespace PluginDemo { public interface IPlugin { int GetInt(); string GetString(); object GetNonMarshalByRefObject(); Action GetAction(); List<string> GetList(); } }
在另外的一個程序集中有其一個實現類 Plugin:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using PluginDemo; namespace PluginDemo.NewDomain { /// <summary> /// 支持跨應用程序域訪問 /// </summary> public class Plugin : MarshalByRefObject, IPlugin { // AppDomain被卸載後,靜態成員的內存會被釋放掉 private static int length; /// <summary> /// int 做爲基礎數據類型, 是持續序列化的. /// <para>在與其餘AppDomain通信時,傳遞的是對象副本(經過序列化進行的值封送)</para> /// </summary> /// <returns></returns> public int GetInt() { length += new Random().Next(10000); return length; } /// <summary> /// string 做爲特殊的class, 也是持續序列化的. /// <para>在與其餘AppDomain通信時,傳遞的是對象副本(經過序列化進行的值封送)</para> /// </summary> /// <returns></returns> public string GetString() { return "iqingyu"; } /// <summary> /// 未繼承 MarshalByRefObject 而且 不支持序列化 的 class, 是不能夠跨AppDomain通訊的,也就是說其餘AppDomain是獲取不到其對象的 /// </summary> /// <returns></returns> public object GetNonMarshalByRefObject() { return new NonMarshalByRefObject(); } private NonMarshalByRefObjectAction obj = new NonMarshalByRefObjectAction(); /// <summary> /// 委託,和 委託所指向的類型相關 /// <para>也就是說,若是其指向的類型支持跨AppDomain通訊,那個其餘AppDomain就能夠獲取都該委託, 反之,則不能獲取到</para> /// </summary> /// <returns></returns> public Action GetAction() { obj.Add(); obj.Add(); //obj.Add(); return obj.TestAction; } private List<string> list = new List<string>() { "A", "B" }; /// <summary> /// List<T> 也是持續序列化的, 固然前提是T也必須支持跨AppDomain通訊 /// <para>在與其餘AppDomain通信時,傳遞的是對象副本(經過序列化進行的值封送)</para> /// </summary> /// <returns></returns> public List<string> GetList() { return this.list; // return new List<Action>() { this.GetAction() }; } } }
在另外的一個程序集中還有一個
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace PluginDemo.NewDomain { /// <summary> /// 未繼承 MarshalByRefObject, 不能夠跨AppDomain交換消息 /// </summary> public class NonMarshalByRefObject { } }
測試程序以下:
using System; using System.Windows; using System.Diagnostics; using System.Runtime.Serialization.Formatters.Binary; namespace PluginDemo { /// <summary> /// MainWindow.xaml 的交互邏輯 /// </summary> public partial class MainWindow : Window { private AppDomain domain; private IPlugin remoteIPlugin; public MainWindow() { InitializeComponent(); } private void loadBtn_Click(object sender, RoutedEventArgs e) { try { unLoadBtn_Click(sender, e); this.txtBlock.Text = string.Empty; // 在新的AppDomain中加載 RemoteCamera 類型 AppDomainSetup objSetup = new AppDomainSetup(); objSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; objSetup.ShadowCopyFiles = "true"; // 雖然此方法已經被標記爲過期方法, msdn備註也提倡不使用該方法, // 可是 以.net 4.0 + win10環境測試,還必須調用該方法 不然,即使卸載了應用程序域 dll 仍是未被解除鎖定 AppDomain.CurrentDomain.SetShadowCopyFiles(); this.domain = AppDomain.CreateDomain("RemoteAppDomain", null, objSetup); this.remoteIPlugin = this.domain.CreateInstance("PluginDemo.NewDomain", "PluginDemo.NewDomain.Plugin").Unwrap() as IPlugin; this.txtBlock.AppendText("建立AppDomain成功\r\n\r\n"); } catch (Exception ex) { this.txtBlock.AppendText(ex.Message); this.txtBlock.AppendText("\r\n\r\n"); } } private void unLoadBtn_Click(object sender, RoutedEventArgs e) { if (this.remoteIPlugin != null) { this.remoteIPlugin = null; } if (this.domain != null) { AppDomain.Unload(this.domain); this.domain = null; this.txtBlock.AppendText("卸載AppDomain成功\r\n\r\n"); } } private void invokeBtn_Click(object sender, RoutedEventArgs e) { if (this.remoteIPlugin == null) return; this.txtBlock.AppendText($"GetInt():{ this.remoteIPlugin.GetInt().ToString()}\r\n"); this.txtBlock.AppendText($"GetString():{ this.remoteIPlugin.GetString().ToString()}\r\n"); try { this.remoteIPlugin.GetNonMarshalByRefObject(); } catch (Exception ex) { this.txtBlock.AppendText($"GetNonMarshalByRefObject():{ ex.Message}\r\n"); if (Debugger.IsAttached) { Debugger.Break(); } } } } }
按測試程序代碼執行,先Load AppDomain, 而後 Access Other Member, 此時會發現出現了異常,大體內容以下:
建立AppDomain成功
GetInt():1020
GetString():iqingyu
GetNonMarshalByRefObject():程序集「PluginDemo.NewDomain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null」中的類型「PluginDemo.NewDomain.NonMarshalByRefObject」未標記爲可序列化。
是因爲 PluginDemo.NewDomain.NonMarshalByRefObject 這個類型未標記可序列化 而引起的。 那麼這種狀況下和序列化又有什麼關係呢?
請繼續往下看。
6、AppDomain間的對象通訊
前文說過了,AppDomain 是用來隔離對象的,AppDomain 之間的對象是不能夠隨意通訊的,這一點在 MSND的備註 中有一段描述:
應用程序域是一個操做系統進程中一個或多個應用程序所駐留的分區。 同一應用程序域中的對象直接通訊。 不一樣應用程序域中的對象的通訊方式有兩種:一種是跨應用程序域邊界傳輸對象副本,一種是使用代理交換消息。
MarshalByRefObject 是經過使用代理交換消息來跨應用程序域邊界進行通訊的對象的基類。 不是從 MarshalByRefObject 繼承的對象根據值隱式封送。 當遠程應用程序引用根據值封送的對象時,將跨應用程序域邊界傳遞該對象的副本。
MarshalByRefObject 對象在本地應用程序域的邊界內可直接訪問。 遠程應用程序域中的應用程序首次訪問 MarshalByRefObject 時,會向該遠程應用程序傳遞代理。 對該代理後面的調用將封送回駐留在本地應用程序域中的對象。
當跨應用程序域邊界使用類型時,類型必須是從 MarshalByRefObject 繼承的,並且因爲對象的成員在建立它們的應用程序域以外沒法使用,因此不得複製對象的狀態。
也就是說AppDomain間的對象通訊有兩種方式:一種是繼承 MarshalByRefObject ,擁有使用代理交換消息的能力,另一種是利用序列化、傳遞對象副本。
第一種:表現形式上來講,傳遞的是對象引用。 第二種 傳遞的是對象副本,也就是說不是同一個對象。
也正所以,因爲 PluginDemo.NewDomain.NonMarshalByRefObject 即不是 MarshalByRefObject 的子類,也不能夠進行序列化,故 不可在兩個不一樣的AppDomain間通訊。
而上面的異常,則是由序列化 PluginDemo.NewDomain.NonMarshalByRefObject 對象失敗致使的異常。
若是一個類型 【不是】 MarshalByRefObject的子類 而且 【沒有標記】 SerializableAttribute,
則該類型的對象不能被其餘AppDomain中的對象所訪問, 固然這種狀況下的該類型對象中的成員也不可能被訪問到了
反之,則能夠被其餘AppDomain中的對象所訪問
若是一個類型 【是】 MarshalByRefObject的子類, 則跨AppDomain所獲得的是 【對象的引用】(爲了好理解說成對象引用,實質爲代理)
若是一個類型 【標記】 SerializableAttribute, 則跨AppDomain所獲得的是 【對象的副本】,該副本是經過序列化進行值封送的
此時傳遞到其餘AppDomain 中的對象 和 當前對象已經不是同一個對象了(只傳遞了副本)
若是一個類型 【是】 MarshalByRefObject的子類 而且 【標記了】 SerializableAttribute,
則 MarshalByRefObject 的優先級更高
另外:.net 基本類型 、string 類型、 List<T> 等類型,雖然沒有標記 SerializableAttribute, 可是他們依然能夠序列化。也就是說這些類型均可以在不一樣的AppDomain之間通訊,只是傳遞的都是對象副本。
7、完整的Demo
完整的Demo筆者已上傳至Github, https://github.com/iqingyu/BlogsDemo :
兩個項目爲完整的Demo