前面讨论的Program.exe文件不止是一个带有metadata的PE文件, 它也是一个程序集(assembly). 程序集是一个或多个包含类型定义和资源的文件的集合体. 组成一个程序集的文件被放到manifest中, manifest是另一组metadata表, 其包括着组成程序集的文件的名字, 他们也描述着程序集的版本, 语言, 发行者, 公开暴露的类型, 以及组成程序集的所有文件.
CLR在程序集进行操作, 也就是说, CLR总会先载入包含manifest表的文件, 然后通过manifest来获得程序集中的其他文件. 下面是你应该记住的有关程序集的特性:
程序集定义了可重用的类型
程序集用版本号来标记
程序集可以有与之关联的安全性信息
程序集中包含的文件不必有这些属性--除了包含manifest metadata表的文件.
对于软件包, 版本, 安全性, 类型, 你必须把它们放在模块中, 在多数情况中, 一个程序集只包含一个文件, 例如上面的Program.exe例子, 然而, 一个程序可以包含多个文件: 一些包含metadata的PE文件, 和一些资源文件, 例如.gif或.jpg文件. 你可以把一个程序集想象成一个逻辑上的EXE或者DLL.
我认为很多人会问为什么微软要引入程序集这个新的概念, 其原因是程序集能让你将逻辑上的类型重用和物理上的类型重用分离开. 例如, 程序集可以由多个类型组成, 你可以把频繁使用的类型放到一个文件中, 不太常用的类型放到另一个文件中, 如果你的程序集是通过Internet发布的, 如果客户端从不访问不太常用的类型, 那么它们所在的文件永远都不需要被下载到客户端. 例如, 一个独立的软件厂商(ISV)专注于UI控件, 它可能会选择在单独的模块中实现Active Accessibility类型(来满足微软的Logo要求). 只有需要这个额外Accessibility功能的用户才需要下载这个模块.
你可以通过在应用程序的配置文件中指定codeBase元素,来配置应用程序来下载程序集文件. codeBase元素给出了一个URL来指向程序集文件所在的位置. 当企图载入一个程序集文件时, CLR获得codeBase元素的URL并检查机器的download cache来看文件是否存在, 如果文件存在, 那么就载入文件, 如果文件不在cache中, 那么CLR就会下载文件并放到cache中, 如果在URL所在的位置没找到哦文件, 那么会抛出FileNotFoundException.
我发现使用多文件的程序集的三个原因:
你可以在把你的不同类型放到不同的文件中, 这可以让文件逐个地下载, 例如上面提到的Internet下载的场合. 类型分离到不同的文件也能为你要购买的应用程序部分地打包, 部署.
你可以在你的程序集中添加资源或数据文件. 例如, 你可能有一个计算某种保险信息的计算器, 这个类型可能需要访问一些保险统计表格来完成它的计算. 不用将保险统计表格嵌入到你的源代码中, 你可以使用一个工具(例如Assembly Linker, AL.exe, 后面会讨论)使得数据文件成为程序集的一部分. 通过这种方式, 这个数据文件可以是任何类型的格式, 例如文本文件, Office Excel表格, Offfice Word表格, 或者其他你想用的任何数据, 只要你的应用程序知道如何解析文件的内容.
你可以创建包含用不同语言实现类型的程序集, 例如, 你可以用C#实现一些类型, VB实现另外一些类型, 以及其他语言实现其他类型. 当你编译用C#编写的源代码中的类型时, 编译器会产生一个模块. 当你编译用VB编写的源代码中的类型时, 编译器会产生另外一个模块. 然后你可以使用一个工具来组合所有的模块, 来产生一个程序集. 对于使用程序集的开发者来说, 程序集仅仅像是包含一群类型而已, 开发者不需要知道程序集使用了不同的语言. 通过这种方式, 如果你愿意, 你可以对每个模块运行ILDasm.exe来获得IL源代码文件, 然后你可以运行ILAsm.exe, 并传入所有的IL源代码文件, 它会产生一个单独的包含所有类型的文件, 这个技术需要你的源代码编译器产生IL-only代码.
重要: 总结一下, 一个程序集是一个重用, 版本控制, 和安全控制的单元. 它允许你把多个类型, 资源放到不同的文件中, 使得你和使用程序集的客户决定打包和部署哪些文件. 一旦CLR载入包含manifest的文件, 它可以决定程序集的那些文件包含着类型和资源, 使用程序集的人只需要知道包含manifest的文件名, 文件分割被抽象化出来, 将来可以改变分割的方式而不影响应用程序的行为.
如果你有多个类型, 它们可以共享一个版本号和安全设置, 那么推荐你把所有的类型放到一个单独的文件中, 而不是把它们分散到不同的文件, 更不要放到不同的程序集, 这是因为性能方面的原因, 载入一个文件/程序集要消耗CLR和Windows的时间来找到程序集, 载入程序集, 和对其进行初始化. 载入越少的文件/程序集越好, 因为载入较少的程序集会帮助你减少工作集, 减少进程地址空间的碎片. 最后, 在处理大文件时, nGen.exe能执行更好的优化.
为了构建一个程序集, 你必须选择一个PE文件来包含manifest, 或者你可以创建一个单独的文件来只包含manifest, 下表给出了manifest metadata表:
| Manifest metadata表 | 描述 |
| AssemblyDef | 如果这个模块标识一个程序集, 那么只包含一个条目. 这个条目包括程序集的名称(没有路径和扩展名), 版本(主版本号, 次版本号, build版本号, 修改版本号), 语言, 标志, hash算法, 发行者的公开密钥(可以是null). |
| FileDef | 对每个PE和资源文件(除了包含manifest的文件, 因为它在AssemblyDef表中)都包含一个条目, 条目包括文件名和扩展名(没有路径), hash值,标志. 如果这个程序集只包含一个文件, 那么FileDef表没有条目. |
| ManifestResourceDef | 对每个资源都包含一个条目, 条目包括资源的名字, 标志(如果对外部的程序集可见则为public, 否则为private), 以及指向FileDef表格的一个索引. 如果资源不是一个独立的文件(例如.jpg或.gif文件), 那么资源是一个包含在PE文件中的流. 对于一个嵌入的资源, 条目还包含了一个偏移量来指示资源流在PE文件中的起始位置. |
| ExportedTypesDef | 对每个公开的类型都包含一个条目, 条目包括类型的名字, 指向FileDef表的索引(表明那个文件实现了这个类型), 以及一个指向TypeDef表格的索引. 注意: 为了节约空间, 包含着manifest的文件中的导出类型不需要在这个表中重复了, 因为类型的信息可以在metadata中的TypeDef表中得到. |
Manifest为程序集的客户与程序集的分割细节之间提供了间接一级, 使得程序集是自描述的. 记住包含manifest的文件具有metadata信息, 这些信息表示那些文件是程序集的一部分, 但是单独的文件没有metadata信息.
注意: 包含manifest的程序集文件还有一个AssemblyRef表格, 这个表格为每个被该程序集引用的引用的其它程序集都包含一个条目, 这可以让工具打开一个程序集的manifest, 查看它引用的程序集, 而不必打开程序集的其他文件. 在AssemblyRef表格中的条目使得程序集是自描述的.
当你指定下面任何一个命令行开关时, C#编译器都产生一个程序集: /t[arget]:exe, /t[arget]:winexe, 或者/t[arget]:library. 这些开关都会使得编译器产生单独的PE文件, 其包含着manifest metadata表, 产生的文件或者CUI可执行程序, 或者是GUI可执行程序, 或者是一个DLL.
除了这些开关, C#编译器支持/t[arget]:module开关, 这个开关告诉编译器产生一个不包含manifest metadata表的PE文件, 产生的PE文件总是一个DLL PE文件, 这个文件必须被加入到一个程序集中, CLR 才能访问它中的类型. 当你使用/t:module开关时, C#编译器默认情况下会在输出文件中增加一个.netmodule扩展名.
重要: 不幸的是, 微软的Visual Studio集成开发环境(IDE)不支持创建多文件程序集, 如果你想创建多文件程序集, 你必须求助于命令行工具.
有很多方法可以给一个程序集增加模块, 如果你使用C#编译器来构建带有manifest的PE文件, 你可以使用/addmodule开关. 为了理解如何构建一个多文件程序集, 让我们假设你有两个源代码文件:
RUT.cs: 包含很少使用的类型
FUT.cs: 包含经常使用的类型
让我们把很少使用的类型编译为他们自己的模块, 使得程序集的用户不需要部署这个模块, 如果用户不需要访问这个很少使用的类型.
csc /t:module RUT.cs
这行命令使C#编译器创建一个RUT.netmodule文件, 这个文件是一个标准的DLL PE文件, 但是, 仅仅通过这一个文件, CLR还不能载入它.
下面让我们编译经常使用的类型为他们自己的模块, 我们将使得这个模块保存程序集的manifest, 因为这个模块中的类型是经常被使用的. 实际上, 因为这个模块将会代表整个程序集, 我将改变输出文件的名字为JeffTypes.dll, 而不是默认的FUT.dll.
csc /out:JeffTypes.dll /t:library /addmodule:RUT.netmodule FUT.cs
这行命令告诉C#编译器编译FUT.cs文件并产生JeffTypes.dll文件. 因为指定了/t:library, 一个包含manifest metadata表的DLL PE文件被放到JeffTypes.dll文件中. /addmodule:RUT.netmodule开关告诉编译器RUT.netmodule是一个文件, 这个文件应该被认为是程序集的一部分. 特别地, /addmodule开关告诉编译器在FileDef manifest metadata表中增加一个文件, 把RUT.netmodule的公开暴露的类型加入到ExportedTypesDef manifest metadata表中.
当编译器完成所有这些操作时, 在图2-1中显示了创建的两个文件, 在右边的模块包含了manifest.
RUT.netmodule文件包含着IL代码(通过编译RUT.cs所产生的), 这个文件也包含着metadata表, 其描述了RUT.cs中定义的类型, 方法, 字段, 属性, 事件, 等. 这个metadata标也描述了RUT.cs中引用的类型, 方法, 等. JeffTypes.dll是一个单独的文件, 类似于RUT.netmodule, 这个文件包含着IL代码(编译FUT.cs所产生的), 这个文件包含着类似的定义和引用metadata表. 然而, JeffTypes.dll还包含着额外的manifest metadata表, 使得JeffTypes.dll是一个程序集. 这个额外的manifest metadata表描述了组成程序集的所有的文件(JeffTypes.dll文件自身和RUT.netmodule文件). manifest metadata表也包含了所有JeffTypes.dll和RUT.netmodule公开暴露的类型.
注意: 实际上, manifest metadata表中没有”包含着manifest的PE文件”中暴露的类型, 这个优化的目的是为了减小PE文件中manifest信息的字节数量, 所以陈述” manifest metadata表也包含了所有JeffTypes.dll和RUT.netmodule公开暴露的类型”不是100%的准确. 然而这个陈述准确地反映了manifest在逻辑上所暴露的内容.
当构建了JeffTypes.dll程序集, 你可以使用IlDasm.exe来检查metadata的manifest表来验证程序集文件真的包含着RUT.netmodule文件中的类型. 下面摘取的是JeffTypes.dll元数据中FileDef部分和ExportedTypesDef metadata表的样子:
包含的外部文件清单:
File #1 (26000001)
-----------------------------------------------------
Token: 0x26000001
Name : RUT.netmodule
HashValue Blob : e6 e6 df 62 2c al 2c 59 97 65 0f 21 44 10 15 96 f2 7e db c2
Flags : [ContainsMetaData] (00000000)
清单文件里的类型:
ExportedType #1 (27000001)
-----------------------------------------------------
Token: 0x27000001
Name: ARarelyUsedType
Implementation token: 0x26000001
TypeDef token: 0x02000002
Flags : [Public] [AutoLayout] [Class] [Sealed] [AnsiClass]
[BeforeFieldlnit](00100101)
从这个例子可以看出, RUT.netmodule是一个文件, 是程序集的一部分, 其token是0x26000001. 从ExportedTypesDef表中, 可以看到有一个公开暴露的类型ARarelyUsedType, 这个类型的实现token是0x26000001, 这表明这个类型的IL代码包含在RUT.netmodule文件中.
注意: 出于好奇, metadata token是一个4字节的数值, 高位字节表示token的类型(0x01=TypeRef, 0x02=TypeDef, 0x23=AssemblyRef, 0x26=FileRef, 0x27=ExportedType). 完整的列表可以参考CorHdr.h中CorTokenType枚举的类型, 这个文件是.NET Framework SDK中包含的文件. 三个低位字节简单地表明了在对应的metadata表中的行. 例如, 实现token 0x000001引用了FileRef表的第一行. 对大多数表来说, 行是从1开始计数的, 而不是0. 对于TypeDef表, 行是从2开始计数的.
任何client代码要使用JeffTypes.dll程序集的类型, 它必须用/r[eference]: JeffTypes.dll编译器开关来构建. 这个开关告诉编译器载入JeffTypes.dll程序集以及在它的FileDef表中列出的所有文件(当搜索外部类型时). 编译器需要所有的程序集文件, 如果你删除了RUT.netmodule文件, C#编译器将会产生如下错误: "fatal error CS0009: Metadata file 'C:\JeffTypes.dll could not be opened—'Error importing module 'RUT.netmodule' of assembly 'C:\JeffTypes.dll'—The system cannot find the file specified'". 这意味着为了构建一个新的程序集, 被引用的程序集中的所有文件都必须存在.
当client代码执行时, 它会调用相应的函数. 如果函数是第一次调用, 那么CLR会检查函数引用的参数类型, 返回值类型, 以及局部变量的类型. 然后CLR尝试载入被引用的程序集文件(包含着manifest的程序集). 如果正在被访问的类型在这个文件中, 那么CLR执行内部的逻辑, 允许使用类型. 如果manifest表明被引用的类型在一个不同的文件中, CLR尝试载入必要的文件, 执行内部的逻辑, 并允许访问类型. 只有在一个方法引用一个类型,并且这个类型是在一个没有被载入的程序集中时, CLR才载入相应的程序集文件. 这意味着, 运行一个应用程序, 被引用程序集中的所有文件不必都存在.