http://flier_lu.blogone.net/?id=2164751
CLR 產(chǎn)品單元經(jīng)理(Unit Manager) Jason Zander 在前幾天一篇文章 Why isn't there an Assembly.Unload method? 中解釋了為什么 CLR 中目前沒(méi)有實(shí)現(xiàn)類(lèi)似 Win32 API 中 UnloadLibrary 函數(shù)功能的 Assembly.Unload 方法。
他認(rèn)為之所以要實(shí)現(xiàn) Assembly.Unload 函數(shù),主要是為了回收空間和更新版本兩類(lèi)需求。前者在使用完 Assembly 后回收其占用資源,后者則卸載當(dāng)前版本載入更新的版本。例如 ASP.NET 中對(duì)頁(yè)面用到的 Assembly 程序的動(dòng)態(tài)更新就是一個(gè)很好的使用示例。但如果提供了 Assembly.Unload 函數(shù)會(huì)引發(fā)一些問(wèn)題:
1.為了包裝 CLR 中代碼所引用的代碼地址都是有效的,必須跟蹤諸如 GC 對(duì)象和 COM CCW 之類(lèi)的特殊應(yīng)用。否則會(huì)出現(xiàn) Unload 一個(gè) Assembly 后,還有 CLR 對(duì)象或 COM 組件使用到這個(gè) Assembly 的代碼或數(shù)據(jù)地址,進(jìn)而導(dǎo)致訪(fǎng)問(wèn)異常。而為了避免這種錯(cuò)誤進(jìn)行的跟蹤,目前是在 AppDomain 一級(jí)進(jìn)行的,如果要加入 Assembly.Unload 支持,則跟蹤的粒度必須降到 Assembly 一級(jí),這雖然在技術(shù)上不是不能實(shí)現(xiàn),但代價(jià)太大了。
2.如果支持 Assembly.Unload 則必須跟蹤每個(gè) Assembly 的代碼使用到的句柄和對(duì)現(xiàn)有托管代碼的引用。例如現(xiàn)在 JITer 在編譯方法時(shí),生成代碼都在一個(gè)統(tǒng)一的區(qū)域,如果要支持卸載 Assembly 則必須對(duì)每個(gè) Assembly 都進(jìn)行獨(dú)立編譯。此外還有一些類(lèi)似的資源使用問(wèn)題,如果要分離跟蹤技術(shù)上雖然可行,但代價(jià)較大,特別是在諸如 WinCE 這類(lèi)資源有限的系統(tǒng)上問(wèn)題比較明顯。
3.CLR 中支持跨 AppDomain 的 Assembly 載入優(yōu)化,也就是 domain neutral 的優(yōu)化,使得多個(gè) AppDomain 可以共享一份代碼,加快載入速度。而目前 v1.0 和 v1.1 無(wú)法處理卸載 domain neutral 類(lèi)型代碼。這也導(dǎo)致實(shí)現(xiàn) Assembly.Unload 完整語(yǔ)義的困難性。
基于上述問(wèn)題, Jason Zander 推薦使用其他的設(shè)計(jì)方法來(lái)回避對(duì)此功能的使用。如 Junfeng Zhang 在其 BLog 上介紹的 AppDomain and Shadow Copy,就是 ASP.NET 解決類(lèi)似問(wèn)題的方法。
在構(gòu)造 AppDomain 時(shí),通過(guò) AppDomain.CreateDomain 方法的 AppDomainSetup 參數(shù)中 AppDomainSetup.ShadowCopyFiles 設(shè)置為 "true" 啟用 ShadowCopy 策略;然后設(shè)置 AppDomainSetup.ShadowCopyDirectories 為復(fù)制目標(biāo)目錄;設(shè)置 AppDomainSetup.CachePath + AppDomainSetup.ApplicationName 指定緩存路徑和文件名。
通過(guò)這種方法可以模擬 Assembly.Unload 的語(yǔ)義。實(shí)現(xiàn)上是將需要管理的 Assembly 載入到一個(gè)動(dòng)態(tài)建立的 AppDomain 中,然后通過(guò)跨 AppDomain 的透明代理調(diào)用其功能,使用 AppDomain.Unload 實(shí)現(xiàn) Assembly.Unload 語(yǔ)義的模擬。chornbe 給出了一個(gè)簡(jiǎn)單的包裝類(lèi),具體代碼見(jiàn)文章末尾。
這樣做雖然在語(yǔ)義上能夠基本上模擬,但存在很多問(wèn)題和代價(jià):
1.性能:在 CLR 中,AppDomain 是類(lèi)似操作系統(tǒng)進(jìn)程的邏輯概念,跨 AppDomain 通訊就跟以前跨進(jìn)程通訊一樣受到諸多限制。雖然通過(guò)透明代理對(duì)象能夠?qū)崿F(xiàn)類(lèi)似跨進(jìn)程 COM 對(duì)象調(diào)用的功能,自動(dòng)完成參數(shù)的 Marshaling 操作,但必須付出相當(dāng)?shù)拇鷥r(jià)。Dejan Jelovic給出的例子(Cross-AppDomain Calls are Extremely Slow)中,P4 1.7G 下只使用內(nèi)建類(lèi)型的調(diào)用大概需要 1ms。這對(duì)于某些需要被頻繁調(diào)用的函數(shù)來(lái)說(shuō)代價(jià)實(shí)在太大了。如他提到實(shí)現(xiàn)一個(gè)繪圖的插件,在 OnPaint 里面畫(huà) 200 個(gè)點(diǎn)需要 200ms 的調(diào)用代價(jià)。雖然可以通過(guò)批量調(diào)用進(jìn)行優(yōu)化,但跨 AppDomain 調(diào)用效率的懲罰是肯定無(wú)法逃脫的。好在據(jù)說(shuō) Whidbey 中,對(duì)跨 AppDomain 調(diào)用中的內(nèi)建類(lèi)型,可以做不 Marshal 的優(yōu)化,以至于達(dá)到比現(xiàn)有實(shí)現(xiàn)調(diào)用速度快 7 倍以上,...,我不知道該夸獎(jiǎng) Whidbey 實(shí)現(xiàn)的好呢,還是痛罵現(xiàn)有版本之爛,呵呵
2.易用性:需要單獨(dú)卸載的 Assembly 中類(lèi)型可能不支持 Marshal,此時(shí)就需要自行處理類(lèi)型的管理。
3.版本:在多個(gè) AppDomain 中如何包裝版本載入的正確性。
此外還有安全方面問(wèn)題。對(duì)普通的 Assembly.Load 來(lái)說(shuō),載入的 Assembly 是運(yùn)行在載入者的 evidence 下,而這絕對(duì)是一個(gè)安全隱患,可能遭受類(lèi)似 unix 下面通過(guò)溢出以 root 權(quán)限讀寫(xiě)文件的程序來(lái)改寫(xiě)系統(tǒng)文件的類(lèi)似攻擊。而單獨(dú)在一個(gè) AppDomain 中載入 Assembly 就能夠單獨(dú)設(shè)置 CAS 權(quán)限,降低執(zhí)行權(quán)限。因?yàn)?nbsp;CLR 架構(gòu)下的四級(jí)權(quán)限控制機(jī)制,最細(xì)的粒度只能到 AppDomain。好在據(jù)說(shuō) Whidbey 會(huì)加入對(duì)使用不同 evidence 載入 Assembly 的支持。
通過(guò)這些討論可以看到,Assembly.Unload 對(duì)于基于插件模型的程序來(lái)說(shuō),其語(yǔ)義的存在是很重要的。但在目前和近幾個(gè)版本來(lái)說(shuō),通過(guò) AppDomain 來(lái)模擬其語(yǔ)義是比較合適的選擇,雖然要付出性能和易用性的問(wèn)題,但能夠更大程度上控制功能和安全性等方面因素。長(zhǎng)遠(yuǎn)來(lái)說(shuō),Assembly.Unload 的實(shí)現(xiàn)是完全可行的,Java 中對(duì)類(lèi)的卸載就是最好的例子,前面那些理由實(shí)際上都是工作量和復(fù)雜度方面的問(wèn)題,并不存在無(wú)法解決的技術(shù)問(wèn)題。
以下為引用:
// ObjectLoader.cs
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();
}
}
}
}
以下為引用:
// Loader.cs
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
}
}