通過應用程式域AppDomain加載和解除安裝程式集
微 軟裝配車的大門似乎隻為貨物裝載敞開大門,卻将解除安裝勞工拒之門外。車門的鑰匙隻有一把,若要獲得還需要你費一些心思。我在學習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)
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加載,删除程式集檔案時會有什麼差別?
作者:KKcat