我们知道JIT编译器将方法的IL代码编译成本地代码时,会查看IL代码中引用了哪些类型。在运行时,JIT编译器利用程序集的TypeRef和AssemblyRef元数据表来确定哪一个程序集定义了所引用的类型。在AssemblyRef元数据表的记录项中,包含了构成程序集强名称的各个部分。JIT编译器获取所有这些部分,包括名称(无扩展名和路径)、版本、语言文化和公钥标记,并把它连接成一个字符串。然后,JIT编译器尝试将与该标识匹配的一个程序集加载到AppDomain中(如果还没有加载的话)。如果被加载的程序集时弱命名的,那么标识中只包含程序集的名称(不包含版本、语言文化以及公钥标记信息)。
在内部,CLR使用System.Reflection.Assembly类的静态方法Load来尝试加载这个程序集。这个方法在.NET Framework SDK文档中是公开的,可调用它显式的将一个程序集加载到AppDomain中。这个方法是CLR中的与Win 32 LoadLibrary函数等价的方法。Assembly的Load方法实际上有几个重载版本。以下是最常用的重载版本的原型:
public static Assembly Load(AssemblyName assemblyRef);
public static Assembly Load(string assemblyString);
在内部,Load导致CLR向程序集应用一个版本绑定重定向策略,并在GAC(全局程序集缓存)中查找程序集。如果没找到,就接着去应用程序的基目录、私有路径子目录和codebase位置查找。如果调用Load时,传递时一个弱命名程序集,Load就不会向程序集应用一个版本绑定重定向策略,CLR也不会去GAC中查找程序集。如果Load找到程序集,会返回对代表已加载的那个程序集的一个Assembly对象的引用。如果Load没有找到指定的程序集,会抛出一个System.IO.FileNotFoundException。
重要提示:一些开发人员可能注意到,System.AppDomain提供了一个Load方法。和Assembly的Load方法不同,AppDomain的Load是一个实例方法,它允许将一个程序集加载到一个指定的AppDomain中。该方法设计供非托管代码调用,允许宿主将一个程序集”注入”到一个AppDomain中。托管代码的开发人员一般情况下不应调用它,因为在调用AppDomain的Load方法时,需要向它传递一个标识了程序集的字符串,该方法随后会应用策略,并在一些常规位置搜索一些程序集。我们知道,AppDomain关联了一些告诉CLR如何查找程序集的设置。为了加载这个程序集,CLR将使用与指定AppDomain关联的设置,而不是与发出调用的那个AppDomain关联的设置。
然后,AppDomain的Load方法会返回对程序集的一个引用System.Reflection.Assembly不是从MarshalByRefObject派生的,所以程序集对象必须按值封送回发出调用的那个AppDomain.但是,现在CLR就会用发出调用的那个AppDomain的设置来定位并加载程序集。如果使用发出调用的那个AppDomain的策略和搜索位置没有找到指定的程序集,就会抛出一个FileNotFoundException。这个行为一般不是你所期望的,应该避免使用AppDomain的Load方法。
在大多数动态可扩展的应用程序中,Assembly的Load时程序集加载到AppDomain的首选方式。但是,它要求你实现掌握构成程序集标识的各个部分。开发人员经常要写一些工具和实用程序,以便在程序集上执行一些处理。这些工具的例子包含:ILDasm.exe,PEVerify.exe,CorFlags.exe,GACUtil.exe, Sgen.exe,SN.exe,XSD.exe等,所有这些工具都要获取引用一个程序集文件的路径名(包括文件扩展名)的命令行实参。为了以指定路径名的的方式加载一个程序集,要调用Assembly的LoadForm方法。在内部,LoadForm首先会调用System.Reflection.AssemblyName类的静态方法GetAssemblyName,该方法打开指定的文件,查找AssemblyRef元数据表的记录项,提取程序集标识信息,然后以一个System.Reflection.AssemblyName对象的形式返回这些信息(文件同时会关闭)。随后,LoadForm方法在内部调用Assembly的Load方法,将AssemblyName对象传给他。然后,CLR会应用版本绑定重定向策略,并在各个位置查找匹配的程序集,如果Load找到匹配的程序集,就会加载它,并返回代表已加载一个程序集的对象Assembly,LoadForm将返回这个值。如果Load没有找到匹配的程序集,LoadForm就会加载通过LoadForm实参传递的路径中的程序集。当然,如果以加载了一个具有相同标识的程序集,LoadFrom方法会简单的返回代表已加载程序集的一个Assembly对象。
VS的UI设计人员和其他工具一般用的是Assembly的LoadFile方法。这个方法可以从任何路径加载一个程序集,并可将具有相同标识的一个程序集多次加载到一个AppDomain中。在设计器/工具中对应用程序的UI进行了修改,CLR不会自动解析任何依懒性问题;你的代码必须向AppDomain的AssemblyResolve事件登记,并让事件回调方法显示地加载任何依赖的程序集。
如果你构建的一个工具只是通过反射来分析程序集的元数据,并希望确保程序集中的任何代码都不会执行,那么加载程序集的最佳方式就是使用Assembly的ReflectionOnlyLoadFrom 或者ReflectionOnlyLoad方法。
ReflectionOnlyLoadFrom方法将加载由路径指定的文件;文件的强名称标识不会获取,也不会在GAC和其他位置搜索。ReflectionOnlyLoad方法会在GAC、应用程序基目录、私有路径和codebase指定的位置搜索指定的程序集。但是,和Load方法不同的是,ReflectionOnlyLoad不会应用版本控制策略,所以你指定了哪个版本,获得的就是哪个版本。如果要自行为一个程序集标识指定版本控制策略,可以将字符串传给AppDomain的ApplyPolicy方法。
ReflectionOnlyLoadFrom 或者ReflectionOnlyLoad方法加载程序集时,CLR禁止程序集中的任何代码执行,试图执行由这两个方法加载的程序集中的代码,会导致CLR抛出一个异常,这两个方法允许工具加载延迟签名的程序集,这种程序集正常情况下会因为安全权限不够而无法加载。另外,这种程序集也可能是为不同的CPU架构而创建的。
利用反射来分析由这两个方法之一加载的程序集时,代码经常需要向AppDomain的ReflectionOnlyAssemblyResovle事件注册一个回调方法,以便手动加载任何引用的程序集;CLR不会自动帮你做这个事情,回调方法被调用时,它必须调用Assembly的ReflectionOnlyLoadFrom或ReflectionOnlyLoad方法,以便显示加载一个程序集,并返回对程序集的一个引用。
许多应用程序都由一个要依赖于众多DLL文件的EXE文件组成。部署这个应用程序时,所有文件都必须部署。然后,有一个技术允许只部署一个EXE文件。首先,标识出EXE文件要依赖的、同时不是作为.NET FRAMEWORK本身的一部分发布的所有DLL文件,将这些DLL添加到你的VS项目中,对于添加的这些DLL,有显示它的属性,并将它的“生成操作”更改为”嵌入的资源”。这回导致C#编译器将DLL文件嵌入EXE文件中,以后只需部署这个EXE文件即可。
在运行时,CLR会找不到依赖的DLL程序集,为了解决这个问题,应用程序初始化时,向AppDomain的ResolveAssembly事件登记一个回调方法,
AppDomain.CurrentDomain.AssemblyResolve += (sender, arg) => { String resourceName = "AssemblyLoadingAndReflection:" + new AssemblyName(arg.Name) + ".dll"; using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)) { Byte[] assemblyData = new Byte[stream.Length]; stream.Read(assemblyData, 0, assemblyData.Length); return Assembly.Load(assemblyData); } };
现在一个线程首次调用一个方法时,如果发现该方法引用了依赖的DLL文件中的一个类型,就会引发一个ResolveAssembly事件,而上诉回调代码会找到所需的嵌入的DLL资源,并调用Assembly的Load方法重载版本,从而加载所需的资源。