




已阅读5页,还剩32页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
.NET本质论卷I公共语言运行库第二章组件CLR对组件代码的打包、部署和查找有自己一整套的概念和技术。这些概念和技术与COM、Java和Win32存在着根本上的差别。如果进一步认识CLR加载器,就能够很好地理解它们之间的差异。不过,我们必须先知道代码和元数据是如何打包的。模块定义CLR程序存在模块(module)中。一个CLR模块是一个字节流,通常作为一个文件存储在本地的文件系统中或者Web服务器上。如图2.1所示,CLR模块采用Windows NT的PE/COFF可执行文件格式的扩展版 PE:Portable Exectuable,译为可移植可执行文件。COFF:Common Object File Format,公共对象文件格式。当CLR编译器对C#、VB.NET等源程序编译后产生 MSIL(中间语言)和元数据。元数据描述代码中的类型,包括每种类型的定义、每种类型的成员的签名、代码引用的成员和运行库在执行时使用的其他数据。MSIL 和元数据包含在一个可移植可执行 (PE) 文件中,此文件基于并扩展过去用于可执行内容的已公布的 Microsoft PE 和公共对象文件格式 (COFF)。这种文件格式包含 MSIL 或本机代码以及元数据,使得操作系统能够识别公共语言运行库映像。文件中的元数据以及 MSIL 的存在使代码能够描述自身,这意味着不再需要类型库或接口定义语言 (IDL)。不过,CLR对PE/COFF文件格式进行了很大的扩展,而不是简单的沿袭。同时,CLR模块也是有效的Win32模块,可以通过LoadLibrary系统调用进行加载。不过,CLR模块用到的PE/COFF的功能极少。准确地说,CLR模块的大部分内容是作为不透明的数据,存放在PE/COFF文件的.text部分。CLR模块包含代码、元数据和资源。代码一般以公共中间语言common intermediate language(CIL)的格式存放(尽管代码也可能被存为特定处理器的机器指令)。模块的元数据描述了模块中定义的类型,包含名字、标头PE/COFF.text部分PE/COFFPE头COFF头IMAGE_COR20_HEADER代码(CIL和或本机机器码资源数据字符串BLOB BLOB:Binary Largr Object,二进制大对象,指那些位图、大的文本数据等。池元数据表图2.1: CLR模块格式 继承关系、方法签名和依赖信息等。模块的资源由静态的只读数据组成,例如,字符串、位图,以及其他没有被存储为可执行代码的部分。CLR模块使用的文件格式具有较好的文档性,开发人员很少也会遇到未加工的格式。因此,即使对于急于求成的开发人员,一般也能够使用CLR提供的两个实用部件中的一个,用来进行可编程地生成模块。IMetaDataEmit接口是低级的COM接口,可以用来由经典的C+编程生成模块元数据。System.Reflection.Emit命名空间是高级的类库,用来由任何CLR正式语言(例如,C#、VB.NET)编程生成元数据和CIL。CodeDOM则工作在更高级的抽象层面上,不必知道和理解CIL。然而,对绝大多数开发人员来说,他们只是需要在开发时生成代码,而不是运行时,对此CLR编译器完全能够胜任。C#编译器(CSC.EXE)、VB.NET编译器(VBC.EXE)和C+编译器(CL.EXE)都能够将源代码翻译成CLR模块。各个编译器通过命令行开关控制产生的表2.1模块输出选项C# / VB.NETC+直接可加载的?从Shell中可直接运行?可访问控制台?/t:exe/CLR是是总是/t:winexe/CLR /link/subsystem:windows是是从不/t:library/CLR /LD是否依赖主机(Host-dependent)/t:module/CLR:NOASSEMBLY/LD否否依赖主机(Host-dependent)模块种类。如表2.1所示,有4个可能的选项。在C#和VB.NET中,通过/target命令行开关(或者其快捷形式/t)选择目标文件的种类。C+编译器可以使用多个开关的组合;不过,通过/CLR开关,强制C+编译器生成CLR兼容的模块。下面所用到的C#和VB.NET开关,将采用它们的快捷形式。选项/t:module产生“未加工的(raw)”模块,其文件扩展名默认为.netmodule。这种格式的模块不能独立地部署,CLR也不能直接加载它们。准确地说,开发人员必须在部署前,将这些“未加工的”模块与成型的组件(被称为程序集)进行关联。相比之下,用/t:library选项编译产生的模块,能够包含附加的元数据,允许开发人员将其作为独立代码进行部署。选项/t:library编译产生的模块,其文件扩展名默认为.DLL。用/t:library编译产生的模块能被CLR直接加载,但不能从命令外壳或Windows资源管理器中作为可执行程序启动。如果要产生可执行程序,你必须采用/t:exe或者/t:winexe选项。这两个选项均产生扩展名为.EXE的文件,唯一的差别是:前者假定为控制台UI子系统使用,后者则假定为GUI子系统。如果没有指定/t选项,默认为/t:exe。不管是使用/t:exe还是/t:winexe选项产生的模块,都必须定义一个初始入口点(initial entry point)。初始入口点是程序启动时CLR将自动执行的方法。程序员必须将这个方法声明为static,并且,在C#或VB.NET中,还必须命名为Main。程序员能够将入口点方法声明为无返回值,或者返回int型值。他们也可以将其声明为无参数形式,或者接受一个字符串数组的参数,它包含从外壳程序输入的命令行参数。下面是C#中Main方法的四种合法的实现。static void Main() static void Main(string argv) static void Main() return 0; static void Main(string argv) return 0; 对应的VB.NET代码为:shared sub Main() : end subshared sub Main(argv as string() : end subshared function Main() : return 0 : end functionshared function Main(argv as string() return 0end function注意,这些方法并不是必须声明为public。不过,程序员只能在类型定义中声明Main方法,尽管类型名称并不重要。下面是最小的C#程序,只是向控制台打印字符串“Hello,world”。class myapp static void Main() System.Console.WriteLine(Hello, World); 在这个例子中,只有一个类,其中包含一个名为Main的静态方法。如果源文件包含多个类型,都有名为Main的静态方法,那么,C#或VB.NET编译可能无所适从(甚至导致错误)。为了解决这种二义性,程序员可以采用/main命令行开关,告诉C#或VB.NET编译器哪个类型将用作程序的初始入口点。程序集定义为了部署CLR模块,开发人员首先必须将其归属于一个程序集(assembly)。程序集就是一个或多个模块的逻辑集合。如前面讨论过的那样,模块是以字节流形式存在的物理构件,通常存放在文件系统中。程序集是逻辑构件,并且通过独立于位置的名字进行引用。而这个名字必须翻译为文件系统中或Internet上的物理路径。那些物理路径最终指向一个或多个包含类型定义、代码以及资源的模块。尽管程序集可能由多个模块组成,但是一个模块往往只属于一个程序集。假如出现两个程序集都引用一个公共模块的情况,将作如何处理?这时,CLR将这个公共模块视为两个不同的模块,即公共模块中的每个类型都有两个不同的拷贝。基于上述理由,本章剩余部分将假定一个模块只明确地属于一个程序集。CLR允许开发人员由多个模块组建程序集,主要是为了支持将那些不经常访问的代码的加载区分开来,同时不用为它们形成单独的封装边界。这个特征在开发人员采用代码下载时特别有用,因为他们可以先只下载初始模块,根据需求才会下载下一个模块。多模块程序集还可以是混合语言的。这样,开发人员既可以采用高生产率的语言(例如,Logo.NET),用于完成大部分工作,同时,采用更为灵活的语言(例如,C+)编写底层代码。通过将这两个模块结合为单个程序集,开发人员能够同时将C+和Logo.NET代码作为一个原子单元进行引用、部署以及版本控制。在CLR中,程序集是部署的“原子”,被用来对CLR模块进行打包、加载、分布以及版本控制。虽然程序集可能包括多个模块以及辅助文件,但程序集本身被作为原子单元进行命名和版本化。如果程序集的某个模块版本发生变化,那么,整个程序集必须重新部署,因为版本号是程序集名字的一部分,而不是底层模块名字的一部分。模块一般都依赖于来自其他程序集的类型。最起码每个模块都依赖于定义在mscorlib程序集中的类型,例如,System.Object和System.String等等。每个CLR模块都包含一个程序集名字的列表,指明该模块所使用的程序集。对于这些程序集以外的引用,它们只是使用了程序集的逻辑名字,而不包含底层模块名或者位置信息。CLR将负责在运行时将这些程序集的逻辑名字转换为模块的路径名。本章后面还将专门讨论。为了促使CLR能够找到程序集中不同部分,每个程序集都正好有一个模块,其元数据包含了程序集清单(assembly manifest)。程序集清单是CLR元数据中附加的一部分,相当于附加的类型定义和代码的附属文件目录。CLR能够直接加载包含程序集清单的模块。对于没有程序集清单的模块,CLR只能先加载含有程序集清单的模块,并且,该清单引用了这些没有清单的模块,从而间接地加载它们。图2.2展示了两个模块:一个含有程序集清单,一个则没有。注意4个/t编译选项,只有/t:module产生没有程序集清单的模块。图2.3展示了一个使用多模块程序集的应用程序,示例2.1则是产生它的MAKEFILE文件内容。在这个例子中,module就是不含程序集清单的模块。为了让这个模块有用,就需要第二个模块(本例为component.dll)提供一个程序集清单,并将module作为下级模块进行引用。当编译所包含的程序集时,要使用/addmodule开关。在这个程序集生成之后,所有在component.dll和module中定义的图2.2:模块和程序集图2.3:使用CSC.EXE编译多模块程序集类型都通过程序集的名字(component)确定作用域。应用程序(例如,application.exe)使用/r编译选项,来引用含有程序集清单的模块。这样,使得两个模块中的类型都能为程序所用。示例2.1:使用CSC.EXE和NMAKE编译多模块程序集# module cannot be loaded as is until an assembly# is module : code.cs csc /t:module code.cs# types in component.cs can see internal and public members# and types defined in code.cscomponent.dll : component.cs module csc /t:library /addmodule:module component.cs# types in application.cs cannot see internal members and# types defined in code.cs (or component.cs)application.exe : application.cs component.dll csc /t:exe /r:component.dll application.cs程序集清单存放在一个明确的模块中,并且包含了用于定位类型和资源的所有信息,而这些类型和资源则被定义为程序集的一部分。图2.4展示了被组合成单个程序集的一组模块,在构建它们时需要相应的CSC.EXE开关。注意在这个例子中,程序集清单包含对下级模块module和module的文件引用列表。除了这些文件引用之外,这些下级模块的每个公共类型都通过.class extern指令列出来。这样,就有了公共类型的完整列表,而不用对程序集中每个模块都遍历元数据。列表的各项指明了类型所在的文件名,以及唯一标识模块中类型的数值的元数据标记(numeric metadata token) 如图2.4所示,“ class nnnn ”中的nnnn,就是这种数值形式的元数据标记。图2.4:多模块程序集最后,含有程序集清单的模块将包含外部引用程序集的主要列表。列表由程序集中每个模块的依赖关系(dependency)组成,而不仅仅是当前模块的依赖关系。这样,通过加载单个文件,就能找到程序集所有的依赖关系。程序集形成一个封装边界(encapsulation boundary),在程序集之间的访问中保护内部实现细节。程序员可以对类型的成员(例如,字段、方法、构造函数等)实施保护,也可以保护整个类型。将类型成员标注为internal,将导致它只对同一程序集的模块是可用的。假如将类型成员标注为public,则导致它对所有代码(当前程序集内部以及外部)是可用的。假如类型中单独成员(例如,方法、字段、构造函数)还能标注为private,只有该声明类型中的方法和构造函数才能访问。这样对于组件内部的封装,编程上与传统的C+风格一致。类似的情形,程序员能将类型成员标注为protected,它放宽了private所允许的访问限制,使得派生类型的方法和构造函数也能访问该成员。访问修饰符protected和internal可以组合在一起使用,这样既能够访问当前类型派生的类型,也能够访问同一程序集中的类型。表2.2展示了特定语言修饰符运用到类型以及单独成员中的情形。注意在C#中,标注为protected internal的成员要么只对同一程序集中的访问方法开放,要么只对派生类型的访问方法开放。CLR还支持一种访问修饰符(在元数据中的标注为famandassem),既对同一程序集中的访问方法公开,又对派生类型的访问方法公开。不过,VB.NET和C#并不允许程序员指定这种访问修饰符。表2.2访问修饰符C#VB.NET意义类型publicPublic访问类型不受限制internalFriend类型只在程序集内部可访问成员publicPublic*访问成员不受限制internalFriend成员只在程序集内部可访问protectedProtected访问仅限于包含类或者从包含类派生的子类型protected internalProtected Friend访问仅限于包含类以及从包含类派生的子类型,或者当前程序集的其他类型PrivatePrivate*访问仅限于包含类型* 在VB.NET中,通过关键字Dim声明的方法默认为Public,而字段默认为Private。程序集中定义类型Customer,而在运行时却不发生混淆,但是,这并不能帮助程序员在单个程序中使用两个或多个同名的类型定义。因为符号化的类型名总是Customer,而不管哪个程序集定义它。为了解决这种大多数编程语言的限制,CLR类型名会有一个命名空间前缀(namespace prefix)。这个前缀是一个字符串,一般以开发人员的组织名(例如,Microsoft、AcmeCorp)开始;如果是.NET Framework的一部分话,则以System开始。程序集的命名约定通常是基于命名空间前缀。例如,.NET XML堆栈被部署在System.Xml程序集中,它包含的所有类型都使用System.Xml的命名空间前缀。这仅仅是一个约定,而不是规则。例如,类型System.Object存放在名为mscorlib程序集中,而不是名为System的程序集中,尽管也确实存在名为System的程序集。程序集名字每个程序集采用四部(four-part)名字,作为唯一的标识。这四部名字由名称、文化、开发人员以及组件版本构成。这些名字被存放于程序集自身的程序集清单(assembly manifest)中,以及引用它的所有程序集的程序集清单中。在加载时,CLR使用四部程序集名字,找到正确的组件。CLR提供System.Reflection.AssemblyName类型,便于开发人员对程序集名字进行可编程地访问,具体则是调用该类型的System.Reflection.Assembly.GetName方法。程序集名字的Name属性往往与程序集清单的底层文件名(不包含任何文件扩展名,尽管它们可能有用)相对应。这是程序集名字中唯一的不可任选的部分。对于简单的情形,CLR在加载时只需要Name属性就能够定位正确的组件。当构建(build)程序集时,名字的这个部分是由编译器根据目标文件名自动选择的。所有的程序集名字都有一个四部分版本号,其形式为Major.Minor.Build.Revision 主版本.次版本.内部版本号(构建版本号).修订号。例如,版本 1.5.1254.0 中的 1 表示主版本,5 表示次版本,1254 表示内部版本号,而 0 则表示修订号。如果你没有显式地设置这个版本号,表2.3 AssemblyVersion特性特性参数实际值.0.3.01.2.*1.2.d.s1.2.3.*1.2.3.s无参数* 这里,d是指从2000年2月1日以来的天数,s是从子夜以来的秒数除以2所得的值 如果将AssemblyVersion特性设为1.0.*,那么,在2003年10月10日上午9:00生成的程序集,它的版本号为.ver 1.0.1378.16200。默认值将是。版本号是在程序构建时设置的,比较典型的方式就是在源代码中使用定制特性。System.Reflection.AssemblyVersion特性能够接受各种的字符串格式,如表2.3所示。当你指定版本号时,Major版本号是必须的。任何缺省部分被假定为零。在构建时(build time),如果Revision部分被指定为*(星号),那么,编译器将利用时钟的时间,为每次编译生成一个单调递增的修订版本号。如果Build号被指定为*,那么,发射(emit)到程序集清单的Build号,是基于2000年2月1日起的天数,确保每一天程序集都有自己的唯一的构建号。不过,给定的构建号将只能适用于给定的24小时制。你不能将版本号的Major或Minor部分指定为*。稍后,本章将讨论程序集加载器和解析器如何使用程序集的版本。程序集名字能够包含CultureInfo特性,它能够标识组件开发所用到的语言和国家代码,也就是组件应用的语言环境。开发人员通过System.Reflection.AssemblyCulture特性指定CultureInfo。这是Internet工程任务组Internet Engineering Task Force(IETF)发布的1766号请求注释Request for Comments (RFC) 所规定的两部分字符串,即字符串的第一部分使用两个小写字母标识语言,第二(可选择)部分使用两个大写字符标识地理区域。例如,字符串”en-US”标识为美国英语。含有CultureInfo特性的程序集不能包括代码;准确地说,它们必须是纯资源(resource-only)的程序集也称为辅助(satellite)程序集,只能包含区域化的字符串(localized string)和用户界面元素。对于包含代码的单个DLL,辅助程序集允许它们根据被部署的区域有选择地加载(和下载)相应的区域化资源。包含代码的程序集(也可以说是绝大多数程序集)被认为是文化无关的(culture-neutral),因而也就没有文化标识符。最后,程序集名字包含一个公钥(public key),它可以标识组件的开发人员。一个程序集引用既可以使用完全的128字节的公钥,也可以使用8字节的公钥标记。公钥(或者公钥标记)被用作处理组织间的文件名冲突。例如,在内存中和磁盘上可能存在多个utilities.dll组件,它们来自不同的组织,每个组件都被确保拥有唯一的公钥。下一节我们将更详细地讨论公钥管理。由于有时必须手工引用程序集(例如,在配置文件中使用),因此,CLR定义了一个标准格式,用于将程序集的四部名字编写为字符串。这个格式被称为程序集的显示名字(display name)。程序集的显示名字总是以程序集的简单Name开始,接着是以逗号为分隔符的属性列表,分别与程序集名字的其它三个属性相对应,并且是可选的。如果四部名字完全被指定,对应的程序集引用被称为完全限定引用(fully qualified reference)。如果缺省一个或者更多的属性,则对应的程序集引用被称为部分限定引用(partially qualified reference)。图2.5展示了一个显示名字,以及用来控制每个属性的对应的CLR特性。注意,如果期望程序集没有文化限制,那么,显示名字必须使用Culture=neutral给予标明。同样,如果期望程序集不带公钥,显示名字也必须通过PublicKeyToken=null标明。这两种情形与不带Culture或PublicKeyToken属性的显示名字绝然不同。简单省略显示名字的某些属性,将导致生成一个部分限定名字,它允许匹配任何Culture或PublicKeyToken。图2.5:完全限定的程序集名一般来说,应该避免使用部分限定程序集名字;否则,CLR的许多部分将以非预期的(甚至令人不满意的)方式工作。然而,为了处理忽略该警告的代码,CLR允许在配置文件中,将部分程序集名字进行完全限定。例如,考虑下面的应用程序配置文件: 这个配置允许下列对Assembly.Load的调用:Assembly assm = Assembly.Load(AcmeCorp.Code);前面的调用与下面的调用一样:Assembly assm = Assembly.Load(AcmeCorp.Code,+ version=,publicKeyToken=a1690a5ea44bab32,+ culture=neutral);PartialName特性必须与Assembly.Load的参数完全匹配;也就是说,每个在对Assembly.Load调用中指定的属性,也必须存在于配置文件的PartialName特性中。并且,在PartialName特性中指定的每个属性,也必须在对Assembly.Load的调用中出现。稍后,本章将讨论如何定位配置文件。公钥和程序集CLR利用公钥技术对组件的开发人员进行唯一标识,同时也保护组件,使它在离开原创者之后不被篡改。每个程序集有能标识开发人员的公钥,它嵌入在程序集中。带有公钥的程序集还含有数字签名(digital signature),它是在程序集首次发布之前生成的;数字签名还提供了程序集清单的加密哈希值(hash),而程序集清单本身包含了所有附属模块的哈希值。这样就保证了程序集一旦被发布,程序集中的代码和资源将不再被改变。数据签名能够只使用公钥进行检验;而签名只能带有对应的私钥(private key)才能生成。由此,组织必须更为谨慎在保护私钥。现行的CLR版本利用RSA RSA是第一个既能用于数据加密也能用于数字签名的算法。它易于理解和操作,也很流行。算法的名字以发明者的名字命名:Ron Rivest、Adi Shamir 和Leonard Adleman。但RSA的安全性一直未能得到理论上的证明。它经历了各种攻击,至今未被完全攻破。的公钥/私钥技术和加密哈希算法Secure Hash Algorithm(SHA) 为了防止数据免受意外或者故意(恶意)的修改,数据的完整性通常是由消息身份验证代码或哈希值提供的。哈希值是从数据序列导出的固定长度的数值。哈希值用于验证通过非安全通道传送的数据的完整性。可以将收到的数据的哈希值与传送时数据的哈希值进行比较,以确定数据是否被篡改。技术产生数字签名。尽管被用作标识程序集的私钥是每个组织的唯一指纹(fingerprint),但它并不提供与数字证书(digital certificate) 数字证书不仅包含用户名和公钥,而且还包含与用户有关的其他信息。同样级别的认可(nonrepudiation)。例如,仅仅依靠程序集的公钥查找开发人员的身份行不通的。CLR的确支持将为数字认证嵌入到程序集中,但这已经超出了本章的范围(更多信息详见第九章)。.NET SDK发布了一个工具(SN.EXE),它能够简化开发和部署期间的公钥和私钥的工作环节。SN.EXE的-K选项将创建一个包含公钥/私钥对的新文件。由于这个新文件含有你的私钥,所以,你的操作在格外谨慎,切忌把这个文件存放在不安全的地方。正因为私钥至关重要,大多组织推迟了程序集的实际签署时间,直到正式发布前夕,这被称为延迟签署(delay signing)。为了允许组织中的开发人员不用访问私钥就能够访问公钥,SN.EXE支持使用-P选项消除私钥的影响。这个选项将创建一个只包含公钥的新文件。公钥/私钥文件以及单纯的公钥文件,它们的扩展名都约定为.SNK。公钥是由SN.EXE产生的,它是一个128字节的不透明的特定算法结构,并带有32字节的附加的头信息。为了压缩程序集引用(和它们的显示名字)的大小,程序集引用可以使用公钥标记(public key token),即完全公钥的8字节散列值。由大多编译器发射的程序集引用通过这个标记取代完全公钥,以保持程序集清单的总体尺寸较小。一个公钥的标记可以通过SN.EXE的-t选项或是-T选项来计算。前者是基于一个只包含一个公钥的.SNK文件来计算,后者是以一个存储在一个程序集清单中的公钥为依据来计算标记。图2.6展示了SN.EXE工具的工作情况。图2.6:使用SN.EXE管理公钥私钥支持CLR的开发工具必须提供某种机制,用于开发人员签署程序集;要么通过定制特性,要么通过命令行开关。System.Reflection.AssemblyKeyFile特性将告诉编译器哪儿能找到.SNK文件,而该文件含有开发人员的公钥。这个特性将与公钥/私钥对或者只与公钥一同发挥作用,允许开发人员在不访问组织的私钥的情形下,能够构建、测试和调试他们的组件。为了构建只使用公钥的程序集,你也必须使用System.Reflection.AssemblyDelaySign特性通知编译器:不存在私钥,也不能生成有意义的数字签名。当采用延迟签署时,空间将预留给数字签名,以便组织中的可靠成员能够重新签署程序集,而不必重复原创者的构建环境。一般情况下,有公钥但不具备有效签名的程序集不能被加载或执行。为了允许在开发时能够使用被延迟签署的程序集,对于特定的程序集或者公钥,这个策略将通过SN.EXE的-Vr选项停止。图2.7展示了从C#中使用的AssemblyKeyFile特性,同时也展示了结果的程序集,以及引用它的其他程序集。注意,128字节的公钥被存储在目标程序集清单中,同数字签图2.7:强名称程序集引用名一起来保护程序集免受篡改。还应注意的是,第二个程序集(引用目标程序集的)只包含了8位的公钥标记。由于构建目标程序集时,关闭了延迟签署,因此,程序集能够在安全环境中部署和加载。相比之下,如图2.8所示的C#编译器产生的目标程序集,则不适合部署;这是因为它在构建时,允许延迟签署。不过,当可信任的开发人员用私钥签署程序集之后,程序集就可以被部署了。注意在这个例子中,是带-R选项使用SN.EXE工具的,这将采用基于命令行提供的公钥/私钥覆盖目标程序集中的数字签名。为了手动检验已签署的程序集,你可以使用SN.EXE的-v选项或与-vf选项。其中,后者将覆盖可能取消签名检验的任何设置。图2.8:延迟签署程序集CLR加载器CLR加载器负责加载和初始化程序集、模块、资源以及类型。CLR加载器总是尽可能少地加载和初始化。与Win32加载器不同,CLR加载器不会处理与自动加载从属模块(或者程序集)。准确地说,从属部分只有当它们确实被需要时才会按需加载(类似于Visual C+ 6.0的延迟加载特征)。这样,不仅加速了程序的初始化时间,也减少运行程序所消耗的资源。在CLR中,加载通常是由基于类型的JIT编译器 JIT编译器:有时也做实时编译器、及时编译器或者即时编译器。前者为Microsoft .NET帮助的译法,后两种多见于Java的书籍。just in time(JIT) compiler触发的。当JIT编译器试图将方法体由CIL转化为机器码时,它需要访问声明类型的类型定义,以及用于类型字段的类型定义。此外,JIT编译器还需要访问将被实时编译的局部变量或者方法参数所用到的类型定义。加载类型意味着包含类型定义的程序集和模块都要被加载。按需加载类型(以及程序集和模块)的策略是意味着:程序中未被使用的部分将不会被载入内存中。同时也意味着:正在运行的应用程序在执行过程中,经常会遇到新程序集和新模块被加载的情形,因为应用程序需要它们当中的类型。如果你不想这么做,那么,你将有两个选择:一种方式就是简单地声明相关类型的隐藏静态字段,使相关类型在你的类型被加载的同时也随即被加载;另一种方式就是显式地操纵加载器。加载器往往是隐式地为你工作。开发人员能够通过程序集加载器(assembly loader)显式地与加载器进行交互。程序集加载器通过System.Reflection.Assembly类的LoadFrom静态方法向开发人员公开。这个方法接受了一个CODEBASE字符串,它既可以是文件系统路径,也可以是识别含有程序集清单模块的统一资源定位器uniform resource locator(URL)。如果不能找到指定文件,加载器将会抛出System.FileNotFoundException异常。如果找到了指定文件,却不是程序集清单中的CLR模块,加载器将会抛出一个System.BadImageFormatException异常。最后,如果CODEBASE是一个模式而不是file:的URL,那么,调用方必须具备WebPermission访问权限,否则将抛出System.SecurityException异常。此外,URL(而不是file:协议)的程序集将在被加载之前,先下载到缓存中。示例2.2展示了一个简单的C#程序。它将加载位于file:/C:/usr/bin/xyzzy下的一个程序集,然后创建名为AcmeCorp.LOB.Customer的包含类型的一个实例。在这个例子中,调用方提供的是程序集的物理位置。当程序按照这种方式使用程序集加载器时,CLR便忽略了程序集的四部名字,包括它的版本号。示例2.2:以显式的CODEBASE加载程序集using System;using System.Reflection;public class Utilities public static Object LoadCustomerType() Assembly a = Assembly.LoadFrom( file:/C:/usr/bin/xyzzy.dll); return a.CreateInstance(AcmeCorp.LOB.Customer); 尽管通过位置加载程序集很有意思,但是,大多数程序集是通过程序集解析器(assembly resolver)按名字加载的。程序集解析器使用四部程序集名字,从而决定哪个程序集将由程序集加载器加载到内存中。如图2.9所示,从名字到位置的解析过程考虑了各种因素,包括应用程序的所在目录、版本控制策略以及其它配置细节(这些将在本章后面讨论)。程序集解析器通过System.Reflection.Assembly类的Load方法向开发人员公开。如示例2.3所示,这个方法接受一个四部程序集名字(要么是一个字符串,要么作为一个AssemblyName引用),并且它与程序集加载器公开的LoadForm方法很相似。这种相似性仅仅只停留在表面上,因为Load图2.9:程序集的解析和加载方法先使用程序集解析器,通过一些稍微复杂的操作,才能找到适合的文件。这些操作的第一个就是运用版本策略,确定将被加载程序集的版本。示例2.3:使用程序集解析器加载程序集using System;using System.Reflection;public class Utilities public static Object LoadCustomerType() Assembly a = Assembly.Load( xyzzy, Version=, + Culture=neutral, PublicKeyToken=9a33f27632997fcc); return a.CreateInstance(AcmeCorp.LOB.Customer); 程序集解析器首先采用可能有效的版本策略(version policy),开始它的工作。版本策略将导致程序集解析器的重定向,从而能够加载所需程序集的替代版本。版本策略能够将给定程序集的一个或多个版本映射为不同版本;然而,除了版本之外,版本策略不能使解析器重定向名字不同的程序集(例如,一个名为Acme.HealthCare的程序集,不能重定向为名为Acme.Mortuary的程序集)。特别要注意版本策略只能应用于由四部程序集名字完全限定的程序集。如果程序集名字只是部分限定的(例如,缺少公钥标记、版本或文化),那么,不能使用任何版本策略。同样,如果绕过程序集解析器,直接调用Assembly.LoadForm方法,那么,也不会运用什么版本策略了。因为你指定了物理路径,而不是程序集的名字。版本策略可以通过配置文件被指定。这些配置文件包括一个计算机范围的配置文件,以及一个特定应用程序的配置文件。计算机范围(machine-wide)的配置文件总是被命名为machine.config,而且,存于%SystemRoot%Microsoft.NetFrameworkV1.0.nnnnCONFIG目录下。特定应用程序(application-specific)的配置文件总是在应用程序的APPBASE目录下。对于基于CLR的.EXE程序,APPBASE是用于定位的基本URI(或者目录),可执行文件就是从这里被加载的。对于ASP.NET应用程序,APPBASE是Web应用程序虚拟目录的根结点。对于基于CLR的.EXE程序,其配置文件的名字与可执行文件的名字相同,只是配置文件名的后缀为“.config”。例如,如果启动的CLR程序集为C:myappapp.exe,那么,相应的配置文件将是C:myappapp.exe.config。至于ASP.NET应用程序,其配置文件总是被命名为web.config。配置文件以可扩展标记语言Extensible Markup Language(XML)为基础,而且,总有一个名为configuration的根元素。配置文件将被程序集解析器、远程调用基础架构和ASP.NET所使用。对于用于配置程序集解析器的元素,图2.10展示了其基本模式。所有相关元素都是在assemblyBinding元素下,其名字空间为urn:schemas-microsoft-com:asm.v1。这些应用程序范围(application-wide)的设置控制了探测路径以及发行者版本策略模式(在本章后面都将有所描述)。此外,dependentAssembly元素被用于对每个依赖的程序集指定版本和定位设置。图2.10:程序集解析器配置文件的格式示例2.4展示了一个简单配置文件,它包含程序集的两个版本策略。第一个策略将指定程序集(Acme.HealthCare)的版重定向为版。第二个策略将那个程序集的版至99版重定向为版。示例2.4:设置版本策略 版本策略能在三个级别上被指定:按应用程序(per application)、按组件(per 图2.11:版本策略component)以及按机器(per machine)。每个级别都有机会处理版本号,高级别的结果将充当低级别的输入,如图2.11所示。注意,对于给定的程序集,如果应用程序和机器的配置文件各有一个版本策略,那么,应用程序的策略优先执行,接着通过执行计算机范围策略获得实际的版本号,以此定位程序集。在这个例子中,如果计算机范围的配置文件将Acme.HealthCare的版重定向为版,而应用程序的版本策略把版本映射为版本,那么,当请求版时,程序集解析器将使用版本。除了特定应用程序和计算机范围的配置文件之外,给定的程序集还能有发行者策略(publisher policy)。发行者策略是来自组件开发人员的声明,它标明了给定组件的哪些版本是互相兼容的。发行者策略作为配置文件,存储在计算机范围的全局程序集缓存中。这些文件的结构与应用程序和计算机的配置文件的结构是完全相同的。然而,为了在用户机器上安装,发行者策略的配置文件必须与包含它的程序集DLL一起封装,作为一个定制的资源。假定文件foo.config 包含发行者的配置策略,下面的命令行将调用程序集链接程序(AL.EXE),为AcmeCorp.Code的2.0版创建一个适合的发行者策略的程序集:al.exe /link:foo.config /out:policy.2.0.AcmeCorp.Code.dll /keyf:pubpriv.snk /v:发行者策略文件的名字形式为policy.major.minor.assmname.dll。由于这个命名约定,给定程序集对每个major.minor版本只能有一个发行者策略文件。在这个例子中,对于major.minor版本号为2.0的AcmeCorp.Code.DLL而言,所有请求将通过与policy.2.0.AcmeCorp.Code.DLL链接的策略文件转发。如
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
评论
0/150
提交评论