基于AppDomain的"插件式"開發(fā)
2011-08-01 09:58 空逸云 閱讀(12021) 評論(47) 收藏 舉報(bào)很多時(shí)候,我們都想使用(開發(fā))USB式(熱插拔)的應(yīng)用,例如,開發(fā)一個(gè)WinForm應(yīng)用,并且這個(gè)WinForm應(yīng)用能允許開發(fā)人員定制擴(kuò)展插件,又例如,我們可能維護(hù)著一個(gè)WinService管理系統(tǒng),這個(gè)WinService系統(tǒng)管理的形形色色各種各樣的服務(wù),這些服務(wù)也是各個(gè)"插件式"的類庫,例如:
public interface IJob { void Run(DateTime time); } public class CollectUserInfo : IJob { public void Run(DateTime time) { //doing some thing... } }
我們提供了一個(gè)IJob接口,所有"服務(wù)"都繼承該接口,然后做相關(guān)的配置,在服務(wù)啟動(dòng)時(shí),就可以根據(jù)配置,反射加載程序集,執(zhí)行我們預(yù)期的任務(wù).
更新程序集(dll/exe)
服務(wù)/插件程序(后面只稱為服務(wù),雖然兩者應(yīng)用不同,但是在此處他們所運(yùn)用的原理和作用是相同的 :-) )很健穩(wěn)的運(yùn)行著.但在服務(wù)/插件程序運(yùn)行一段時(shí)間之后,某些"插件"的業(yè)務(wù)需求發(fā)生的變化,或者版本升級等種種外部原因,導(dǎo)致我們對原本的"插件"程序集進(jìn)行了升級(可能從v1.0升級至v2.0).當(dāng)我們想像Asp.net應(yīng)用一樣.把新的dll替換舊dll的時(shí)候,錯(cuò)誤發(fā)生了.
發(fā)生該錯(cuò)誤的原因很簡單,因?yàn)槲覀兊某绦蛑幸呀?jīng)調(diào)用了該dll,那么在CLR加載該dll到文件流中也給其加了鎖,所以,當(dāng)我們要進(jìn)行覆蓋,修改,刪除的時(shí)候自然就無法操作該文件了.那我們該怎么做?為什么Asp.net可以直接覆蓋?
AppDomain登場
我們知道,AppDomain是.Net平臺(tái)里一個(gè)很重要的特性,在.Net以前,每個(gè)程序是"封裝"在不同的進(jìn)程中的,這樣導(dǎo)致的結(jié)果就造就占用資源大,可復(fù)用性低等缺點(diǎn).而AppDomain在同一個(gè)進(jìn)程內(nèi)劃分出多個(gè)"域",一個(gè)進(jìn)程可以運(yùn)行多個(gè)應(yīng)用,提高了資源的復(fù)用性,數(shù)據(jù)通信等.詳見應(yīng)用程序域
CLR在啟動(dòng)的時(shí)候會(huì)創(chuàng)建系統(tǒng)域(System Domain),共享域(Shared Domain)和默認(rèn)域(Default Domain),系統(tǒng)域與共享域?qū)τ谟脩羰遣豢梢姷?默認(rèn)域也可以說是當(dāng)前域,它承載了當(dāng)前應(yīng)用程序的各類信息(堆棧),所以,我們的一切操作都是在這個(gè)默認(rèn)域上進(jìn)行."插件式"開發(fā)很大程度上就是依靠AppDomain來進(jìn)行.
"熱插拔"實(shí)現(xiàn)說明
當(dāng)加載了一個(gè)程序集之后,該程序集就會(huì)被加入到指定AppDomain中,按照原來的想法,要實(shí)現(xiàn)"熱插拔",只要在需要使用該"插件"的時(shí)候,加載該"插件"的程序集(dll),使用結(jié)束后,卸載掉該程序集便可達(dá)到我們預(yù)期的效果.加載程序集很簡單,.C#提供一個(gè)Assembly類,方便又快捷.
var _assembly = Assembly.LoadFrom(assemblyFile);
Assembly提供了數(shù)個(gè)加載方法詳見Assembly類.
然后,C#卻沒有提供卸載程序集的方法,唯一能卸載程序集的方法只有卸載該程序集所在的AppDomain,這樣,該AppDomain下的程序集都會(huì)被釋放.知道這一點(diǎn),我們便可以利用AppDomain來達(dá)到我們預(yù)期的效果.
AppDomain實(shí)現(xiàn)"熱插拔"
首先,我們需要先實(shí)例化一個(gè)新AppDomain作為"插件"的宿主.在實(shí)例化一個(gè)Domain之前,先聲明該Domain的一些基本配置信息
AppDomainSetup setup = new AppDomainSetup(); setup.ApplicationName = "ApplicationLoader"; setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; setup.PrivateBinPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "private"); setup.CachePath = setup.ApplicationBase; setup.ShadowCopyFiles = "true"; //啟用影像復(fù)制程序集 setup.ShadowCopyDirectories = setup.ApplicationBase; AppDomain.CurrentDomain.SetShadowCopyFiles();
setup.ShadowCopyFiles = "true";這句很重要,其作用就是啟用影像復(fù)制程序集,什么是影像復(fù)制程序集,復(fù)制程序集是保證"熱插拔"
實(shí)現(xiàn)的主要工作.AppDomain加載程序集的時(shí)候,如果沒有ShadowCopyFiles,那就直接加載程序集,結(jié)果就是程序集被鎖定,相反,如果啟用了ShadowCopyFiles,則CLR會(huì)將準(zhǔn)備加載的程序集拷貝一份至CachePath,再加載CachePath的這一份程序集,這樣原程序集也就不會(huì)被鎖定了. AppDomain.CurrentDomain.SetShadowCopyFiles();的作用就是當(dāng)前AppDomain也啟用ShadowCopyFiles,在此,當(dāng)前AppDomain也就是前面我們說過的那個(gè)默認(rèn)域(Default Domain),為什么當(dāng)前域也要啟用ShadowCopyFiles呢?
主AppDomian在調(diào)用子AppDomain提供過來的類型,方法,屬性的時(shí)候,也會(huì)將該程序集添加到自身程序集引用當(dāng)中去,所以,"插件"程序集就被主AppDomain鎖定,這也是為什么創(chuàng)建了單獨(dú)的AppDomain程序集也不能刪除,替換(釋放)的根本原因
利用SOS,可以很清楚的看到這一點(diǎn)
0:018> !dumpdomain -------------------------------------- System Domain: 5b912478 LowFrequencyHeap: 5b912784 HighFrequencyHeap: 5b9127d0 StubHeap: 5b91281c Stage: OPEN Name: None -------------------------------------- Shared Domain: 5b912140 LowFrequencyHeap: 5b912784 HighFrequencyHeap: 5b9127d0 StubHeap: 5b91281c Stage: OPEN Name: None Assembly: 00109de0 [C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll] ClassLoader: 00110f68 Module Name 58631000 C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll -------------------------------------- Domain 1: 000f4598 LowFrequencyHeap: 000f4914 HighFrequencyHeap: 000f4960 StubHeap: 000f49ac Stage: OPEN SecurityDescriptor: 000f5568 Name: AppDomainTest.exe Assembly: 00109de0 [C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll] ClassLoader: 00110f68 SecurityDescriptor: 001097b0 Module Name 58631000 C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Assembly: 0011d448 [E:\Test\AppDomainTest\AppDomainTest\bin\Debug\AppDomainTest.exe] ClassLoader: 00117fd0 SecurityDescriptor: 0011d3c0 Module Name 001c2e9c E:\Test\AppDomainTest\AppDomainTest\bin\Debug\AppDomainTest.exe Assembly: 00131370 [C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Windows.Forms\v4.0_4.0.0.0__b77a5c561934e089\System.Windows.Forms.dll] ClassLoader: 0011fa00 SecurityDescriptor: 001299a0 Module Name 579c1000 C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Windows.Forms\v4.0_4.0.0.0__b77a5c561934e089\System.Windows.Forms.dll Assembly: 00131400 [C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Drawing\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Drawing.dll] ClassLoader: 00131490 SecurityDescriptor: 0012e9c0 Module Name 62661000 C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Drawing\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Drawing.dll Assembly: 00131d20 [C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll] ClassLoader: 00133d08 SecurityDescriptor: 0012f078 Module Name 5aa81000 C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll Assembly: 00131ed0 [C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Configuration\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Configuration.dll] ClassLoader: 001415a8 SecurityDescriptor: 0012f430 Module Name 5a981000 C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Configuration\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Configuration.dll Assembly: 00132080 [C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Xml\v4.0_4.0.0.0__b77a5c561934e089\System.Xml.dll] ClassLoader: 00141620 SecurityDescriptor: 0012f5c8 Module Name 546e1000 C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Xml\v4.0_4.0.0.0__b77a5c561934e089\System.Xml.dll Assembly: 00132ce0 [E:\Test\AppDomainTest\AppDomainTest\bin\Debug\CrossDomainController.dll] ClassLoader: 001b3450 SecurityDescriptor: 06f94560 Module Name 001c7428 E:\Test\AppDomainTest\AppDomainTest\bin\Debug\CrossDomainController.dll Assembly: 00132350 [C:\Users\kong\AppData\Local\assembly\dl3\6ZYK3XE9.86Q\2AQ35O7C.VHE\1f704bbb\b7cca5cf_8c4fcc01\ShowHelloPlug.DLL] ClassLoader: 001b32e8 SecurityDescriptor: 070a8620 Module Name 001c7d78 C:\Users\kong\AppData\Local\assembly\dl3\6ZYK3XE9.86Q\2AQ35O7C.VHE\1f704bbb\b7cca5cf_8c4fcc01\ShowHelloPlug.DLL -------------------------------------- Domain 2: 06fd0238 LowFrequencyHeap: 06fd05b4 HighFrequencyHeap: 06fd0600 StubHeap: 06fd064c Stage: OPEN SecurityDescriptor: 06724510 Name: ApplicationLoaderDomain Assembly: 00109de0 [C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll] ClassLoader: 00110f68 SecurityDescriptor: 06f93bd0 Module Name 58631000 C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Assembly: 00132e90 [E:\Test\AppDomainTest\AppDomainTest\bin\Debug\ApplicationLoader\assembly\dl3\c91a2898\f6f7f865_9a4fcc01\CrossDomainController.DLL] ClassLoader: 001b3540 SecurityDescriptor: 06f92be0 Module Name 00a833c4 E:\Test\AppDomainTest\AppDomainTest\bin\Debug\ApplicationLoader\assembly\dl3\c91a2898\f6f7f865_9a4fcc01\CrossDomainController.DLL Assembly: 001330d0 [E:\Test\AppDomainTest\AppDomainTest\bin\Debug\ApplicationLoader\assembly\dl3\32519346\b7cca5cf_8c4fcc01\ShowHelloPlug.DLL] ClassLoader: 001b39f0 SecurityDescriptor: 06f92f98 Module Name 00a83adc E:\Test\AppDomainTest\AppDomainTest\bin\Debug\ApplicationLoader\assembly\dl3\32519346\b7cca5cf_8c4fcc01\ShowHelloPlug.DLL
除了新建的AppDomain(Domain2)中的Module引用了ShowHelloPlug.dll,默認(rèn)域(Domian1)也有ShowHelloPlug.dll的
程序集引用.
應(yīng)用程序域之間的通信
每個(gè)AppDomain都有自己的堆棧,內(nèi)存塊,也就是說它們之間的數(shù)據(jù)并非共享了.若想共享數(shù)據(jù),則涉及到應(yīng)用程序域之間的通信.C#提供了MarshalByRefObject類進(jìn)行跨域通信,那么,我們必須提供自己的跨域訪問器.
public class RemoteLoader : MarshalByRefObject { private Assembly _assembly; public void LoadAssembly(string assemblyFile) { try { _assembly = Assembly.LoadFrom(assemblyFile); //return _assembly; } catch (Exception ex) { throw ex; } } public T GetInstance<T>(string typeName) where T : class { if (_assembly == null) return null; var type = _assembly.GetType(typeName); if (type == null) return null; return Activator.CreateInstance(type) as T; } public void ExecuteMothod(string typeName, string methodName) { if (_assembly == null) return; var type = _assembly.GetType(typeName); var obj = Activator.CreateInstance(type); Expression<Action> lambda = Expression.Lambda<Action>(Expression.Call(Expression.Constant(obj), type.GetMethod(methodName)), null); lambda.Compile()(); } }
為了更好的操作這個(gè)跨域訪問器,接下來我構(gòu)建了一個(gè)名為AssemblyDynamicLoader的類,它內(nèi)部封裝了RemoteLoader類
的操作.
public class AssemblyDynamicLoader { private AppDomain appDomain; private RemoteLoader remoteLoader; public AssemblyDynamicLoader() { AppDomainSetup setup = new AppDomainSetup(); setup.ApplicationName = "ApplicationLoader"; setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; setup.PrivateBinPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "private"); setup.CachePath = setup.ApplicationBase; setup.ShadowCopyFiles = "true"; setup.ShadowCopyDirectories = setup.ApplicationBase; 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 LoadAssembly(string assemblyFile) { remoteLoader.LoadAssembly(assemblyFile); } public T GetInstance<T>(string typeName) where T : class { if (remoteLoader == null) return null; return remoteLoader.GetInstance<T>(typeName); } public void ExecuteMothod(string typeName, string methodName) { remoteLoader.ExecuteMothod(typeName, methodName); } public void Unload() { try { if (appDomain == null) return; AppDomain.Unload(this.appDomain); this.appDomain = null; } catch (CannotUnloadAppDomainException ex) { throw ex; } } }
這樣我們每次都是通過AssemblyDynamicLoader類進(jìn)行跨域的訪問.
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);
通過我們前面構(gòu)造的一個(gè)AppDomainSetup,構(gòu)建了一個(gè)我們所需的AppDomain,并且在這個(gè)appDomain中構(gòu)建了
一個(gè)RemoteLoader類的實(shí)例(此時(shí)該實(shí)例已具備跨域訪問能力,也就是說我們在主域能獲取子域內(nèi)部的數(shù)據(jù)信息).目前RemoteLoader只提供了少數(shù)的幾個(gè)方法.
跨域操作
下面,我們就模擬一次"插件式"的跨域操作.首先我們構(gòu)造了一個(gè)窗體,其有以下元素.
選擇程序集路徑之后,加載程序集,然后就觸發(fā)程序集指定類型(通過配置獲取)的特定操作.這里我們定義了一個(gè)公共接口,它是所有"插件"操作的主要入口了.
public interface IPlug { void Run(); }
隨后定義了一個(gè)實(shí)現(xiàn)該接口的類.
[Serializable] public class ShowHelloPlug : IPlug { public void Run() { MessageBox.Show("Hello World..."); } }
這個(gè)"插件"的工作很簡單.僅僅彈出一個(gè)對話框,說聲"Hello World…",接下來將其編譯成一個(gè)dll.
回到界面,選擇剛才編譯的Dll,然后直接加載.
到這里,我們的工作完成了一半了.呼呼.OK.我們的需求發(fā)生了變化,不再是彈出Hello World了.而時(shí)候彈出Hi,I'm Kinsen,我們修改剛才的子類,并再編譯一次.再將Dll替換剛才的Dll,這次,Dll沒有沒鎖定(因?yàn)槲覀兦懊鎲⒂昧薙hadowCopyFiles.).再加載一下程序集,你會(huì)發(fā)現(xiàn)結(jié)果并不是"Hi,I'm Kinsen",而是"Hello World.."為什么會(huì)這樣呢?這時(shí)候,借助SOS的力量(前面有SOS結(jié)果).
我們發(fā)現(xiàn)Domain1(Default Domain)和Domain2(新創(chuàng)建Domain)都引用了程序集ShowHelloPlug.DLL,但是兩個(gè)引用的Dll地址卻不相同,這是因?yàn)閱⒂昧薙hadowCopyFiles,它們加載的都是各自程序集的備份,我們根據(jù)Domain2的Assembly地址查看ShowHelloPlug的編譯代碼.
0:011> !dumpmt 00fc40ac 00fc40ac is not a MethodTable 0:011> !dumpmd 00fc40ac Method Name: Plug.ShowHelloPlug.Run() Class: 046812b4 MethodTable: 00fc40bc mdToken: 06000001 Module: 00fc3adc IsJitted: no CodeAddr: ffffffff Transparency: Critical
從IsJitted為no可以看出,該程序集并沒有被調(diào)用,那調(diào)用的是誰?我們再次查看Domain1(Default Domain
)中的ShowHelloPlug.
0:011> !dumpmd 001f8240 Method Name: Plug.ShowHelloPlug.Run() Class: 004446e4 MethodTable: 001f8250 mdToken: 06000001 Module: 001f7d78 IsJitted: yes CodeAddr: 00430de0 Transparency: Critical
已知每個(gè)AppDomain都有自己的堆棧信息,各自不互相影響,所以,當(dāng)我們在主域中獲取到了子域中的數(shù)據(jù),并非新建一個(gè)指向該實(shí)例的引用,而是在自己的堆棧上開辟出一塊空間"深度拷貝"該實(shí)例,那么必然就達(dá)不到我們我需的結(jié)果.
子域內(nèi)部調(diào)用
那么為了達(dá)到我們預(yù)期的效果,我們必須在子域內(nèi)部執(zhí)行我們所需的操作(調(diào)用),所以在RemoteLoader類中增加了一個(gè)Execute方法
public void ExecuteMothod(string typeName, string methodName) { if (_assembly == null) return; var type = _assembly.GetType(typeName); var obj = Activator.CreateInstance(type); Expression<Action> lambda = Expression.Lambda<Action>(Expression.Call(Expression.Constant(obj), type.GetMethod(methodName)), null); lambda.Compile()(); }
此處我暫時(shí)只想到了利用反射調(diào)用,這樣的代價(jià)就是調(diào)用所需消耗的資源更多,效率低下.目前還沒有
想出較好的解決方案,有經(jīng)驗(yàn)的童鞋歡迎交流.
這樣外部的調(diào)用就變成以下
loader = new AssemblyDynamicLoader(); loader.LoadAssembly(txt_dllName.Text); //var obj = loader.GetInstance<IPlug>("Plug.ShowHelloPlug"); //obj.Run(); loader.ExecuteMothod("Plug.ShowHelloPlug", "Run");
現(xiàn)在在將Dll替換,結(jié)果正常.
尾聲
做"插件式"開發(fā),除了利用AppDomain之外,也有童鞋給出了另一種解決方案,也就是在加載Dll的時(shí)候,先將Dll在內(nèi)存中復(fù)制一份,這樣原來的Dll也就不會(huì)被鎖定了.詳見插件的“動(dòng)態(tài)替換”.
以上實(shí)例本人皆做過實(shí)驗(yàn),但可能還存在一定不足或概念錯(cuò)誤,若有不當(dāng)之處,歡迎各位童鞋批評指點(diǎn).
更多
出處:http://kongyiyun.cnblogs.com
本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利。





浙公網(wǎng)安備 33010602011771號