C# 有關Assembly.Unload詳解

    CLR 產品單元經理(Unit Manager) Jason Zander 在前幾天一篇文章 Why isn’t there an Assembly.Unload method? 中解釋瞭為什麼 CLR 中目前沒有實現類似 Win32 API 中 UnloadLibrary 函數功能的 Assembly.Unload 方法。
他認為之所以要實現 Assembly.Unload 函數,主要是為瞭回收空間和更新版本兩類需求。前者在使用完 Assembly 後回收其占用資源,後者則卸載當前版本載入更新的版本。例如 ASP.NET 中對頁面用到的 Assembly 程序的動態更新就是一個很好的使用示例。但如果提供瞭 Assembly.Unload 函數會引發一些問題:

    1.為瞭包裝 CLR 中代碼所引用的代碼地址都是有效的,必須跟蹤諸如 GC 對象和 COM CCW 之類的特殊應用。否則會出現 Unload 一個 Assembly 後,還有 CLR 對象或 COM 組件使用到這個 Assembly 的代碼或數據地址,進而導致訪問異常。而為瞭避免這種錯誤進行的跟蹤,目前是在 AppDomain 一級進行的,如果要加入 Assembly.Unload 支持,則跟蹤的粒度必須降到 Assembly 一級,這雖然在技術上不是不能實現,但代價太大瞭。

    2.如果支持 Assembly.Unload 則必須跟蹤每個 Assembly 的代碼使用到的句柄和對現有托管代碼的引用。例如現在 JITer 在編譯方法時,生成代碼都在一個統一的區域,如果要支持卸載 Assembly 則必須對每個 Assembly 都進行獨立編譯。此外還有一些類似的資源使用問題,如果要分離跟蹤技術上雖然可行,但代價較大,特別是在諸如 WinCE 這類資源有限的系統上問題比較明顯。

    3.CLR 中支持跨 AppDomain 的 Assembly 載入優化,也就是 domain neutral 的優化,使得多個 AppDomain 可以共享一份代碼,加快載入速度。而目前 v1.0 和 v1.1 無法處理卸載 domain neutral 類型代碼。這也導致實現 Assembly.Unload 完整語義的困難性。

    基於上述問題, Jason Zander 推薦使用其他的設計方法來回避對此功能的使用。如 Junfeng Zhang 在其 BLog 上介紹的 AppDomain and Shadow Copy,就是 ASP.NET 解決類似問題的方法。

    在構造 AppDomain 時,通過 AppDomain.CreateDomain 方法的 AppDomainSetup 參數中 AppDomainSetup.ShadowCopyFiles 設置為 ”true” 啟用 ShadowCopy 策略;然後設置 AppDomainSetup.ShadowCopyDirectories 為復制目標目錄;設置 AppDomainSetup.CachePath + AppDomainSetup.ApplicationName 指定緩存路徑和文件名。
通過這種方法可以模擬 Assembly.Unload 的語義。實現上是將需要管理的 Assembly 載入到一個動態建立的 AppDomain 中,然後通過跨 AppDomain 的透明代理調用其功能,使用 AppDomain.Unload 實現 Assembly.Unload 語義的模擬。chornbe 給出瞭一個簡單的包裝類,具體代碼見文章末尾。

    這樣做雖然在語義上能夠基本上模擬,但存在很多問題和代價:

    1.性能:在 CLR 中,AppDomain 是類似操作系統進程的邏輯概念,跨 AppDomain 通訊就跟以前跨進程通訊一樣受到諸多限制。雖然通過透明代理對象能夠實現類似跨進程 COM 對象調用的功能,自動完成參數的 Marshaling 操作,但必須付出相當的代價。Dejan Jelovic給出的例子(Cross-AppDomain Calls are Extremely Slow)中,P4 1.7G 下隻使用內建類型的調用大概需要 1ms。這對於某些需要被頻繁調用的函數來說代價實在太大瞭。如他提到實現一個繪圖的插件,在 OnPaint 裡面畫 200 個點需要 200ms 的調用代價。雖然可以通過批量調用進行優化,但跨 AppDomain 調用效率的懲罰是肯定無法逃脫的。好在據說 Whidbey 中,對跨 AppDomain 調用中的內建類型,可以做不 Marshal 的優化,以至於達到比現有實現調用速度快 7 倍以上,…,我不知道該誇獎 Whidbey 實現的好呢,還是痛罵現有版本之爛,呵呵

    2.易用性:需要單獨卸載的 Assembly 中類型可能不支持 Marshal,此時就需要自行處理類型的管理。

    3.版本:在多個 AppDomain 中如何包裝版本載入的正確性。

    此外還有安全方面問題。對普通的 Assembly.Load 來說,載入的 Assembly 是運行在載入者的 evidence 下,而這絕對是一個安全隱患,可能遭受類似 unix 下面通過溢出以 root 權限讀寫文件的程序來改寫系統文件的類似攻擊。而單獨在一個 AppDomain 中載入 Assembly 就能夠單獨設置 CAS 權限,降低執行權限。因為 CLR 架構下的四級權限控制機制,最細的粒度隻能到 AppDomain。好在據說 Whidbey 會加入對使用不同 evidence 載入 Assembly 的支持。

    通過這些討論可以看到,Assembly.Unload 對於基於插件模型的程序來說,其語義的存在是很重要的。但在目前和近幾個版本來說,通過 AppDomain 來模擬其語義是比較合適的選擇,雖然要付出性能和易用性的問題,但能夠更大程度上控制功能和安全性等方面因素。長遠來說,Assembly.Unload 的實現是完全可行的,Java 中對類的卸載就是最好的例子,前面那些理由實際上都是工作量和復雜度方面的問題,並不存在無法解決的技術問題。

# re: AppDomain and Shadow Copy 4/30/2004 2:34 AM chornbe

You must also encapsulate the loaded assembly into another class, which is loaded by the new appdomain. Here’s the code as it’s working for me: (I’ve created a few custom exception types, and you’ll notice I had them back – they’re not descended from MarshalByRefObject so I can’t just throw them from the encapsulated code)

— cut first class file

using System;
using System.Reflection;
using System.Collections;

namespace Loader{

/* contains assembly loader objects, stored in a hash
* and keyed on the .dll file they represent. Each assembly loader
* object can be referenced by the original name/path and is used to
* load objects, returned as type Object. It is up to the calling class
* to cast the object to the necessary type for consumption.
* External interfaces are highly recommended!!
* */
public class ObjectLoader : IDisposable {

// essentially creates a parallel-hash pair setup
// one appDomain per loader
protected Hashtable domains = new Hashtable();
// one loader per assembly DLL
protected Hashtable loaders = new Hashtable();

public ObjectLoader() {/*...*/}

public object GetObject( string dllName, string typeName, object[] constructorParms ){
Loader.AssemblyLoader al = null;
object o = null;
try{
al = (Loader.AssemblyLoader)loaders[ dllName ];
} catch (Exception){}
if( al == null ){
AppDomainSetup setup = new AppDomainSetup();
setup.ShadowCopyFiles = "true";
AppDomain domain = AppDomain.CreateDomain( dllName, null, setup );
domains.Add( dllName, domain );
object[] parms = { dllName };
// object[] parms = null;
BindingFlags bindings = BindingFlags.CreateInstance | BindingFlags.Instance | BindingFlags.Public;
try{
al = (Loader.AssemblyLoader)domain.CreateInstanceFromAndUnwrap(
"Loader.dll", "Loader.AssemblyLoader", true, bindings, null, parms, null, null, null
);
} catch (Exception){
throw new AssemblyLoadFailureException();
}
if( al != null ){
if( !loaders.ContainsKey( dllName ) ){
loaders.Add( dllName, al );
} else {
throw new AssemblyAlreadyLoadedException();
}
} else {
throw new AssemblyNotLoadedException();
}
}
if( al != null ){
o = al.GetObject( typeName, constructorParms );
if( o != null && o is AssemblyNotLoadedException ){
throw new AssemblyNotLoadedException();
}
if( o == null || o is ObjectLoadFailureException ){
string msg = "Object could not be loaded. Check that type name " + typeName +
" and constructor parameters are correct. Ensure that type name " + typeName +
" exists in the assembly " + dllName + ".";
throw new ObjectLoadFailureException( msg );
}
}
return o;
}

public void Unload( string dllName ){
if( domains.ContainsKey( dllName ) ){
AppDomain domain = (AppDomain)domains[ dllName ];
AppDomain.Unload( domain );
domains.Remove( dllName );
}
}

~ObjectLoader(){
dispose( false );
}

public void Dispose(){
dispose( true );
}

private void dispose( bool disposing ){
if( disposing ){
loaders.Clear();
foreach( object o in domains.Keys ){
string dllName = o.ToString();
Unload( dllName );
}
domains.Clear();
}
}
}

}

— end cut

— cut second class file

using System;
using System.Reflection;

namespace Loader {
// container for assembly and exposes a GetObject function
// to create a late-bound object for casting by the consumer
// this class is meant to be contained in a separate appDomain
// controlled by ObjectLoader class to allow for proper encapsulation
// which enables proper shadow-copying functionality.
internal class AssemblyLoader : MarshalByRefObject, IDisposable {

#region class-level declarations
private Assembly a = null;
#endregion

#region constructors and destructors
public AssemblyLoader( string fullPath ){
if( a == null ){
a = Assembly.LoadFrom( fullPath );
}
}
~AssemblyLoader(){
dispose( false );
}

public void Dispose(){
dispose( true );
}

private void dispose( bool disposing ){
if( disposing ){
a = null;
System.GC.Collect();
System.GC.WaitForPendingFinalizers();
System.GC.Collect( 0 );
}
}
#endregion

#region public functionality
public object GetObject( string typename, object[] ctorParms ){
BindingFlags flags = BindingFlags.CreateInstance | BindingFlags.Instance | BindingFlags.Public;
object o = null;
if( a != null ){
try{
o = a.CreateInstance( typename, true, flags, null, ctorParms, null, null );
} catch (Exception){
o = new ObjectLoadFailureException();
}
} else {
o = new AssemblyNotLoadedException();
}
return o;
}
public object GetObject( string typename ){
return GetObject( typename, null );
}
#endregion

}
}

— end cut

相關的一些資源:
Why isn’t there an Assembly.Unload method?
http://blogs.msdn.com/jasonz/archive/2004/05/31/145105.aspx

AppDomains (“application domains”)
http://blogs.msdn.com/cbrumme/archive/2003/06/01/51466.aspx

AppDomain and Shadow Copy
http://blogs.msdn.com/junfeng/archive/2004/02/09/69919.aspx

到此這篇關於C# 有關Assembly.Unload詳解的文章就介紹到這瞭,更多相關C# 有關Assembly.Unload內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: