應用程序域(AppDomain)已經不是一個新名詞了,只要熟悉.net的都知道它的存在,不過我們還是先一起來重新認識下應用程序域吧,究竟它是何方神聖。
應用程序域
眾所周知,進程是代碼執行和資源分配的最小單元,每個進程都擁有獨立的內存單元,而進程之間又是相互隔離的,自然而然,進程成為了代碼執行的安全邊界。
一個進程對應一個應用程序是一個普遍的認知,而.net卻打破了這一慣例,因為它帶來了應用程序域這一全新的概念,CLR可使用應用程序域來提供應用程序之間的隔離,而一個進程中可以運行多個應用程序域,也就是說只要使用應用程序域,我們可以在一個進程中運行多個應用程序,而不會造成進程間調用或進程間切換等方面的額外開銷。
是不是覺得應用程序域是個很神奇的東東了,別急,我們再來看看它的隔離特性又為我們帶來了什麼。
優勢
首先,應用程序域之間是不相互影響的,它是天生的異常隔離機制。也就是說,在一個應用程序域中出現的錯誤不會影響到其他應用程序域,因為類型安全的代碼不會導致內存錯誤。
其次,它能夠在運行時動態的加載和卸載程序集。我們都知道,在.net世界中,加載器一旦加載了程序集,那麼它將一直存在於應用程序的整個生命周期中,而應用程序域則改變了這一切,它為我們提供了卸載程序集的能力。
最後,應用程序域可以單獨實施安全策略和配置策略。說白了就是可以為每個應用程序域配置相應的權限,以更好的管理應用程序。
另外值得注意的是,應用程序域和線程之間不具有一對一的相關性。在任意給定時間,在單個應用程序域中可以執行多個線程,而特定線程並不局限在單個應用程序域內。也就是說,線程可以自由跨越應用程序域邊界,如果沒有主動新啟線程,那麼多個應用程序域依然運行在同一個線程中。
總的來說,應用程序域形成了托管代碼的隔離、卸載和安全邊界。而這些特性帶給一個插件式框架的將是異常隔離、動態加載卸載插件和更安全的插件運行環境。
由於這篇文章的定位是針對框架設計結合應用程序域的特性,因此假設你已經對應用程序域有了一定的了解了,下面通過示例,讓我們一步一步來認識應用程序域的這些特性。
創建和卸載AppDomain
使用C#我們可以用如下的方式創建一個應用程序域,並在新域中執行一段代碼:
AppDomain domain = AppDomain.CreateDomain("Hello AppDomain!");
domain.DoCallBack(new CrossAppDomainDelegate(() =>
{
Window win = new Window
{
Width = 300,
Height = 100,
Content = AppDomain.CurrentDomain.FriendlyName
};
win.Show();
}));
運行後可以看到在新域中創建的Window展示如下:
卸載應用程序域則可以通過AppDomain靜態方法AppDomain.Unload(domain)實現,就是這麼簡單。
配置域加載方式
如果你運行了上面這段代碼,是不是發現新域創建的Window過了好久才呈現出來,這是怎麼回事呢,簡單來說,這是因為.net加載器默認的行為是在每個域裡都會重新加載引用的程序集(包括Framework本身除了mscorlib外的程序集),當然我們可以更改這種行為,不過在這之前我們先來了解下一個新概念”domain neutrality”, 詳細資料可以看這篇文章Domain Neutral Assemblies,簡單來說它擁有跨域共享程序集的能力,這就避免了重復加載的損耗,我們可以通過為程序入口main函數添加LoaderOptimization標簽修改默認加載方式:
[System.STAThreadAttribute()]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[LoaderOptimization(
LoaderOptimization.MultiDomainHost)]
public static void Main()
{
AppDomainTest.App app = new AppDomainTest.App();
app.InitializeComponent();
app.Run();
}
重新編譯運行,速度有了明顯的提升吧。
LoaderOptimization有三種方式(SingleDomain, MultiDomain和MultiDomainHost),在Domain Neutral Assemblies中均有詳細的解譯,有興趣的朋友可以看下,此處就不再重述了。
異常隔離
對於插件式框架而言,異常隔離是非常重要的,這是保證一個框架穩定性的必要特性。下面我們來看看使用應用程序域如何實現異常隔離。
首先我們來模擬在新創建的域中拋出異常:
AppDomain domain = AppDomain.CreateDomain("Hello AppDomain!");
domain.DoCallBack(new CrossAppDomainDelegate(() =>
{
Window win = new Window
{
Width = 300,
Height = 100,
Content = AppDomain.CurrentDomain.FriendlyName
};
win.Loaded += (obj, arg) =>
{
throw new Exception("test exception.");
};
win.Show();
}));
這裡采用的是在Window loaded事件中直接拋出異常達到模擬效果,OK,編譯運行,很不幸,成功掛掉。
這是因為新域中未處理的異常,最終都會拋至默認域,進而導致崩潰。要驗證這一點,很容易,我們只要在默認域中添加 AppDomain.CurrentDomain.UnhandledException事件處理就可以截獲到新域中拋出的異常,可惜在此你只能截獲卻無法改變崩潰的結果。
那麼如何才能處理掉這個異常,在默認域或者新域中注冊System.Windows.Threading.Dispatcher.CurrentDispatcher.UnhandledException事件處理,示例如下:
AppDomain domain = AppDomain.CreateDomain("Hello AppDomain!");
System.Windows.Threading.Dispatcher.CurrentDispatcher.UnhandledException += (obj, arg) =>
{
arg.Handled = true;
MessageBox.Show(arg.Exception.Message);
AppDomain.Unload(domain);
};
domain.DoCallBack(new CrossAppDomainDelegate(() =>
{
Window win = new Window
{
Width = 300,
Height = 100,
Content = AppDomain.CurrentDomain.FriendlyName
};
win.Loaded += (obj, arg) =>
{
throw new Exception("test exception.");
};
win.Show();
}));
注意到最關鍵的arg.Handled = true這一句,它的意義在於告訴系統這個事件已經被處理過了,不要再往下傳遞了,最後主動把新域卸載掉,而默認域則仍然正常運行著,如此便達到了異常隔離的效果。
組合不同域中的插件
假設所有的插件都處於不同的域中,那麼如何組合它們呢,即如何將不同域中的插件同時呈現到一個容器中。
眾所周知,要實現對象在域之間傳遞,對象必須是可序列化的或者是繼承自MarshalByRefObject的類型,然而UI控件對此卻是無能為力了, 在此就需要微軟的Addin框架幫助了,雖然大家都覺得Addin框架復雜、難用,但是裡面有好些東西還是很有用處的,比如這裡將要用到的 FrameworkElementAdapters類,它提供了兩個靜態方法ContractToViewAdapter和ViewToContractAdapter用於實現FrameworkElement和INativeHandleContract之間的相互轉換,傳說中這種轉換是通過句柄實現的。還是用例子來說明如何讓插件跨域呈現吧,首先添加System.Addin.Contract.dll和System.Windows.Presentation.dll兩個引用,然後編寫如下代碼
AppDomain domain = AppDomain.CreateDomain("test");
domain.DoCallBack(new CrossAppDomainDelegate(() =>
{
// 在新域中創建Button控件
Button btn = new Button { Content = "test" };
// 將Button控件轉換為INativeHandleContract
INativeHandleContract ict = FrameworkElementAdapters.ViewToContractAdapter(btn);
AppDomain.CurrentDomain.SetData("testbtn", ict);
}));
// 在主域中獲取新域中的INativeHandleContract對象
INativeHandleContract iContract = domain.GetData("testbtn") as INativeHandleContract;
// 將INativeHandleContract對象轉換回FrameworkElement
FrameworkElement ctrl = FrameworkElementAdapters.ContractToViewAdapter(iContract);
Application.Current.MainWindow.Content = ctrl;
運行結果如下,新域中創建的控件成功的呈現在了主域中
至於域中的權限配置部分,將在下篇中講述。