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内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!