標籤:
微軟裝配車的大門似乎只為貨物裝載敞開大門,卻將卸載工人拒之門外。車門的鑰匙只有一把,若要獲得還需要你費一些心思。我在學習Remoting的時候,就遇到一個擾人的問題,就是Remoting為遠程對象僅提供Register的方法,如果你要登出時,只有另闢蹊徑。細心的開發員,會發現Visual Studio.Net中的反射機制,同樣面臨這個問題。你可以找遍MSDN的所有文檔,在Assembly類中,你永遠只能看到Load方法,卻無法尋覓到Unload的蹤跡。難道我們裝載了程式集後,就不能再將它卸載下來嗎?
想一想這樣一個情境。你通過反射動態載入了一個dll檔案,如今你需要在未關閉程式的情況下,刪除或覆蓋該檔案,那麼結果會怎樣?很遺憾,系統會提示你無法訪問該檔案。事實上該檔案正處於被調用的狀態,此時要對該檔案進行修改,就會出現爭用的情況。
顯然,為程式集提供卸載功能是很有必要的,但為什麼微軟在其產品中不提供該功能呢?CLR 產品單元經理(Unit Manager) Jason Zander 在文章 Why isn‘t there an Assembly.Unload method? 中解釋了沒有實現該功能的原因。Flier_Lu在其部落格裡(Assembly.Unload)有詳細的中文介紹。文中介紹瞭解決卸載程式集的折中方法。Eric Gunnerson在文章《AppDomain 和動態載入》中也提到:Assembly.Load() 通常運行良好,但程式集無法獨立卸載(只有 AppDomain 可以卸載)。Enrico Sabbadin 在文章《Unload Assemblies From an Application Domain》也有相關VB.Net實現該功能的相關說明。
尤其是Flier_Lu的部落格裡已經有了很詳細的代碼。不過,這些代碼沒有詳細地說明。我在我的項目中也需要這一項功能。這段代碼給了我很大的提示。但在實際的實現中,還是遇到一些具體的問題。所以我還是想再談談我的體會。
通過AppDomain來實現程式集的卸載,這個思路是非常清晰的。由於在程式設計中,非特殊的需要,我們都是運行在同一個應用程式定義域中。由於程式集的卸載存在上述的缺陷,我們必須要關閉應用程式定義域,方可卸載已經裝載的程式集。然而主程式域是不能關閉的,因此唯一的辦法就是在主程式域中建立一個子程式域,通過它來專門實現程式集的裝載。一旦要卸載這些程式集,就只需要卸載該子程式域就可以了,它並不影響主程式域的執行。
不過現在看來,最主要的問題不是子程式域如何建立,關鍵是我們必須實現一種機制,來達到兩個程式域之間完成通訊的功能。如果大家熟悉Remoting,就會想到這個問題不是和Remoting的機制有幾分相似之處嗎?那麼答案就可以呼之欲出了,對了,就是使用代理的方法!不過與Remoting不同的是兩個程式域之間的關係。因為子程式域是在主程式域中建立的,因此對該域的控制顯然就與Remoting不相同了。我想先用一副圖來表述實現的機制:
說明:
1、Loader類提供建立子程式域和卸載程式域的方法;
2、RemoteLoader類提供裝載程式集方法;
3、Loader類獲得RemoteLoader類的代理對象,並調用RemoteLoader類的方法;
4、RemoteLoader類的方法在子程式域中完成;
5、Loader類和RemoteLoader類均放在AssemblyLoader.dll組件檔中;
我們再來看代碼:
Loader類:
SetRemoteLoaderObject()方法:
private AppDomain domain = null; private Hashtable domains = new Hashtable(); private RemoteLoader rl = null;public RemoteLoader SetRemoteLoaderObject(string dllName){ AppDomainSetup setup = new AppDomainSetup(); setup.ShadowCopyFiles = "true"; domain = AppDomain.CreateDomain(dllName,null,setup); domains.Add(dllName,domain); try { rl = (AssemblyLoader.RemoteLoader)domain.CreateInstanceFromAndUnwrap( "AssemblyLoader.dll","AssemblyLoader.RemoteLoader"); } catch { throw new Exception(); }}
代碼中的變數rl為RemoteLoader類對象,在Loader類中是其私人成員。SetRemoteLoaderObject()方法實際上提供了兩個功能,一是建立了子程式域,第二則是獲得了RemoteLoader類對象。
請大家一定要注意語句:
rl = (AssemblyLoader.RemoteLoader)domain.CreateInstanceFromAndUnwrap("AssemblyLoader.dll","AssemblyLoader.RemoteLoader");
這條語句就是實現兩個程式域之間通訊的關鍵。因為Loader類是在主程式域中,RemoteLoader類則是在子程式域中。如果我們在Loader類即主程式域中顯示執行個體化RemoteLoader類對象rl,此時調用rl的方法,實際上是在主程式域中調用的。因此,我們必須使用代理的方式,來獲得rl對象,這就是CreateInstanceFromAndUnwrap方法的目的。其中參數一為要建立類對象的組件檔名,參數二則是該類的類型名。
CreateCreateInstanceFromAndUnwrap方法有多個重載。代碼中的調用方式是當RemoteLoader類為預設建構函式時的其中一種重載。如果RemoteLoader類的建構函式有參數,則方法應改為:
object[] parms = {dllName};BindingFlags bindings = BindingFlags.CreateInstance |BindingFlags.Instance | BindingFlags.Public;rl = (AssemblyLoader.RemoteLoader)domain.CreateInstanceFromAndUnwrap("AssemblyLoader.dll","AssemblyLoader.RemoteLoader",true,bindings,null,parms,null,null,null);
詳細的調用方式可以參考MSDN。
以下Loader類的Unload方法和LoadAssembly方法():
public Assembly LoadAssembly(string dllName){ try { SetRemoteLoaderObject(dllName); return rl.LoadAssembly(dllName); } catch (Exception) { throw new AssemblyLoadFailureException(); }}
public void Unload(string dllName){ if (domains.ContainsKey(dllName)) { AppDomain appDomain = (AppDomain)domains[dllName]; AppDomain.Unload(appDomain); domains.Remove(dllName); } }
當我們調用Unload方法時,則程式域domain載入的程式集也將隨著而被卸載。LoadAssembly方法中的異常AssemblyLoadFailureException為自訂異常:
public class AssemblyLoadFailureException:Exception { public AssemblyLoadFailureException():base() { } public override string Message { get { return "Assembly Load Failure"; } } }
既然在Loader類獲得的RemoteLoader類執行個體必須通過代理的方式,因此該類對象必須支援被序列化。所以我們可以令該類派生MarshalByRefObject。RemoteLoader類的代碼:
public class RemoteLoader:MarshalByRefObject { public RemoteLoader(string dllName) { if (assembly == null) { assembly = Assembly.LoadFrom(dllName); } } private Assembly assembly = null; public Assembly LoadAssembly(string dllName) { try { assembly = Assembly.LoadFrom(dllName); return assembly; } catch (Exception) { throw new AssemblyLoadFailureException(); } } }
通過上述的兩個類,我們就可以實現程式集的載入和卸載。另外,為了保證應用程式定義域的對象在記憶體中被清除,應該令這兩個類都實現IDisposable介面,和實現Dispose()方法。
然而在實際的操作過程中,我發現在RemoteLoader類的LoadAssembly方法,是存在遺患的。在我的LoadAssembly方法中,會返回一個Assembly對象。令我百思不得其解的是,雖然都是Assembly對象,但在載入某些程式集並返回Assembly時,在Loader類中會拋出SerializationException異常,並報告還原序列化的對象狀態不足。這個異常是在序列化獲還原序列化過程中發生的。我反覆比較了兩個程式集,一個可以正常載入並序列化,一個會拋出如上異常。會拋出異常的程式集並沒有什麼特殊之處,且我在程式中的其他地方也沒有重複載入該程式集。這是一個疑問!!
不過通常我們在RemoteLoader類中,要實現的方法並非返回一個Assembly對象,而是通過反射載入程式集後,建立該程式集的對象。由於類對象都為object類型,此時序列化就不會出現問題。在我的項目中,因為要獲得程式集的版本號碼,比較版本號碼在確定是否需要更新,因此我在RemoteLoader類中,只需要在載入程式集後,返回程式集的版本號碼字串類型就可以了。字串類型是絕對支援序列化的。
AssemlbyLoader.Dll的原始碼可以點擊這裡獲得。在應用程式中,顯示添加對該程式集的引用,然後執行個體化Loader類對象,來調用該方法即可。我還做了一個簡單的測試程式,用的是LoadAssembly方法。大家可以測試一下,是否如我所說,對於某些程式集,可能會拋出序列化的異常!?
測試的代碼請點擊這裡獲得,測試介面如下:
同時,大家也可以測試一下,直接載入和通過AppDomain載入,刪除組件檔時會有什麼區別?
原文連結:
通過應用程式定義域AppDomain載入和卸載程式集
通過應用程式定義域AppDomain載入和卸載程式集