CLR via C# 3 读书笔记(12):第2章 生成、打包、部署和管理应用程序与类型 — 2.4 将模块组合为程序集(上)

上一篇随笔中讨论的Program.exe不仅仅是一个包含元数据的PE文件,它还是一个程序集。程序集是一个或多个包含类型定义的文件和资源文件的集合。程序集的某个文件用来保存清单(manifest)。清单是另外一组元数据表的集合,主要包含了程序集中一部分文件的名称。这些元数据表还描述了程序集的版本、语言文化、发布者、公有导出类型和组成程序集的所有文件。

CLR操作程序集,也就是说,CLR总是先加载包含清单元数据表的文件,然后利用该清单来获取程序集中的其他文件名称。一下是一些需要牢记的程序集的特性:

  • 程序集定义了可重用的类型。
  • 程序集标识有一个版本号。
  • 程序集可以包含相关的安全信息。

除了包含清单元数据表的文件以外,程序集中的其他文件不具备上述特性。

要对类型进行打包、版本控制、实施安全策略,并使用它们,你必须将类型放在座位程序集一部分的模块中。在大多数情况下,程序集只包含一个文件,如前面的Program.exe。但是,程序集也可以由多个文件组成。例如,一些带有元数据的PE文件和一些gif、jpg之类的资源文件。我们可以将它们视为一个逻辑上的EXE或DLL。

微软为什么引进程序集这个概念呢?程序集允许我们分离可重用类型的逻辑表示和物理表示。例如,一个程序集可以包含多个类型。你可能把常用的类型放到一个文件中,把不常用的类型放到另一个文件中。如果你的程序集是通过网络下载来部署的,并且客户端不会访问不常用的类型,那么包含不常用类型的文件就不必下载到客户端了。例如,专门开发UI控件的ISV(独立软件开发商)可能会选择在一个单独的模块中实现Active Accessibility类型。这样只有那些需要额外辅助(accessibility)特性的用户才需要下载该模块。

你可以通过在应用程序的配置文件中指定一个codeBase元素来配置应用程序需要下载的程序集文件。codeBase元素标识了一个可以找到所有程序集文件的URL地址。在加载程序集文件时,CLR获取codeBase元素中的URL,并检查计算机的下载缓存看看是否已经下载。如果不在缓存中,CLR从URL指定的位置下载文件并放入缓存。如果找不到文件,CLR在运行时抛出FileNotFoundException。

使用多文件程序集有以下三个原因:

  • 你可以将类型实现在不同的文件中,使文件在互联网环境中进行增量下载。还使得你购买和安装的应用程序能够进行分段打包和部署。
  • 你可以将资源或数据文件添加到程序集中。例如,一个计算保险信息的类型可能需要访问一些保险统计表,与其在源代码中嵌入一个统计表,不如使用某种工具(如Assembly Linker,AL.exe)使数据文件作为程序集的一部分。数据文件可以是任何格式的,文本文件、Excel表格、Word表格或其他格式,只要你的程序知道如何去解析文件的内容。
  • 你可以创建由不同语言实现的类型所组成的程序集。例如,你可以用C#实现一些类型,用VB实现一些类型,用其他语言实现一些类型。当你编译用C#创建的类型时,编译器生成一个模块。当你编译用VB创建的类型时,编译器生成另一个模块。你可以使用工具将这些模块合并成一个程序集。对于使用这个程序集的开发者来说,它只是包含了许多类型,开发者甚至不知道它使用了不同的编程语言。如果你愿意,可以对每一个模块运行ILDasm.exe来获取IL源代码文件。然后运行ILAsm.exe传入所有的IL源代码文件。ILAsm.exe将生成一个包含所有类型的文件。该技术要求你的源代码编译器产生的只是IL代码。

总而言之,程序集是一个可重用、可实施版本策略和安全策略的单元。它允许你将类型和资源划分到不同的文件中去,这样你和程序集的使用者便可以决定哪些文件可以一起打包和部署。一旦CLR加载了包含清单的文件,它就可以决定程序集中的其他文件中哪些包含了程序正在引用的类型和资源。任何一个程序集的使用者只需要知道包含清单的文件名称。文件的划分是不考虑使用者的,并且将来可以在不破坏程序行为的情况下进行改变。

如果你有多个类型可以共享一个版本号和安全设置,推荐你将这些类型放置在同一个文件中,而不是置于多个文件,更不用说多个程序集了。这么做的理由是性能。查找、加载和初始化一个文件/程序集将耗费CLR和Windows时钟。文件/程序集加载得越少越好,这是因为加载的程序集越少,越有助于减少工作组和进程的地址空间碎片。最终,在处理较大文件时,nGen.exe可以进行更好的优化。

要创建一个程序集,你必须选择一个PE文件作为清单的载体。或者创建一个单独的PE文件只保存清单而不包含其他内容。下表显示了一个清单元数据表,正是有了它们一个托管模块才得以成为一个程序集:

  • AssemblyDef:如果模块标识为一个程序集,那么它将在AssemblyDef表中有一个对应的条目。该条目包含程序集名称(不包含路径和扩展名)、版本号(主版本号、次版本号、生成版本号和修订版本号)、语言文化、一些标记、散列算法和发布者的公有密钥(可能为null)。
  • FileDef:程序集中所有PE文件和资源文件都在FileDef表中有一个对应的条目(除了清单所在的文件,因为它对应的条目在AssemblyDef表中)。该条目包含文件名和扩展名(不包含路径)、散列值、一些标记。如果程序集只包含一个文件(即清单所在文件),那么FileDef表中没有任何条目。
  • ManifestResourceDef:程序集中所有资源都在ManifestResourceDef表中有一个对应的条目。该条目包含资源名称、一些标记(public表示对程序集外部可见,private表示不可见)、和一个资源文件或流所在的文件在FileDef表中的索引。如果资源文件不是一个单独的文件(如jpg、gif),那么所谓资源就是指嵌入在PE文件中的流。对于嵌入的资源,该条目还包含表示资源流在PE文件中起始位置的偏移量。
  • ExportedTypesDef:程序集中所有的PE模块导出的所有公有类型,都在ExportedTypesDef表中有一个对应的条目。该条目版含类型名称、到FileDef表的索引(指明哪一个程序集文件实现了该类型)、到TypeDef表的索引。注意:为节省文件空间,从包含清单的文件导出的类型不包含在该表中,因为这些类型信息可以通过元数据的TypeDef表获得。

清单的存在为程序集的使用者和程序集的各个部分之间提供了一个间接层,并使得程序集可以自描述。另外需要注意的是,包含清单的文件拥有元数据信息,指明哪些文件是程序集的一部分,但是其他文件本身并不包含这些指明它们是程序集一部分的元数据信息。

注意,包含清单的程序集文件中还有一个AssemblyRef表。所有该程序集中的文件引用的所有程序集都在AssemblyRef表中有一个对应的条目。这允许一些工具打开程序集清单查看其引用的程序集组,而不必打开程序集的其他文件。AssemblyRef表的条目也是程序集得以实现自描述的原因。

当使用命令行开关/t[arget]:exe、/t[arget]:winexe或/t[arget]:library时,C#编译器将生成一个程序集。所有这些开关都会使编译器生成一个包含清单元数据表的PE文件。文件分别为CUI可执行文件、GUI可执行文件或DLL。

除了这些开关以外,C#编译器开支持/t[arget]:module开关。它使编译器生成一个不包含清单元数据表的PE文件,并且这个PE文件永远是一个DLL,在CLR访问它定义的任何类型之前,该文件必须被添加到一个程序集中。当使用/t:module开关时,C#编译器生成的文件在默认情况下扩展名为.netmodule。

Visual Studio本身不支持创建多文件程序集。要创建多文件程序集,必须使用命令行工具。

有很多方法可以将模块添加到一个程序集中。如果你使用C#编译器创建不包含清单的PE文件,可以使用/addmodule。为了了解如何创建多文件程序集,假设有以下两个源代码文件:

  • RUT.cs,包含不常用的类型。
  • FUT.cs,包含常用的类型。

让我们将不常用的类型编译为一个模块,这样如果不会用到这里面的类型,程序集的使用者就不必部署该模块:

csc /t:module RUT.cs

C#编译器将创建RUT.netmodule文件,它是一个标准的DLL PE文件,但是CLR无法加载它。

接下来将常用的类型编译为一个模块。由于很常用,我们将该模块作为程序集清单的保存者。事实上,由于该模块将代表全部程序集,可以把输出文件的名字由FUT.dll改为JeffTypes.dll。

csc /out:JeffTypes.dll /t:library /addmodule:RUT.netmodule FUT.cs

由于指定了/t:library,JeffTypes.dll就是包含元数据清单表的DLL PE文件。/addmodule:RUT.netmodule告诉编译器RUT.netmodule是程序集的一部分。具体而言,/addmodule告诉编译器将文件添加到FileDef清单元数据表中,并将RUT.netmodule里公有的导出类型添加到ExportedTypesDef清单元数据表中。

一旦编译器完成了所有过程,将创建如下图所示的两个文件。右边的模块包含清单。

image

RUT.netmodule包含编译RUT.cs时生成的IL代码和元数据表(描述定义在RUT.cs中的类型、方法、字段、属性、事件等,以及RUT.cs引用的类型、方法等)。JeffTypes.dll是一个单独的文件,和RUT.netmodule一样,包含编译FUT.cs时生成的IL代码以及类似的定义和引用元数据表。JeffTypes.dll还包含额外的清单元数据表,这使JeffTypes.dll成为一个程序集。这个额外的清单元数据表描述了组成程序集的所有文件(JeffTypes.dll文件本身和RUT.netmodule文件)。清单元数据表还包含了JeffTypes.dll和RUT.netmodule中导出的公有类型。

注意,事实上清单元数据表并不真的包含清单本身所在PE文件的导出类型。这种优化的目的是减少PE文件中清单程序集所需的字节数。所以像“清单元数据表还包含了JeffTypes.dll和RUT.netmodule中导出的公有类型”并非100%正确,但还是准确地反映了清单在逻辑上展现的内容。

一旦生成了JeffTypes.dll程序集,你就可以使用ILDasm.exe观察元数据中的清单表来验证程序集文件是否确实引用了RUT.netmodule文件中的类型。其中FileDef和ExportedTypesDef元数据表如下所示:

File #1 (26000001)
-------------------------------------------------------
Token: 0x26000001
Name : RUT.netmodule
HashValue Blob : e6 e6 df 62 2c a1 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]
[BeforeFieldInit](00100101)

可以看到RUT.netmodule作为程序集的一部分,标记为0x26000001。在ExportedTypesDef表中,可以看到一个公有的导出类型ARarelyUsedType。它的实现标记为0x26000001,也就是说该类型的IL代码包含在RUT.netmodule文件中。

注意,元数据标记是一个4字节的数值。高位字节标识标记的类型(0x01=TypeRef,0x02=TypeDef,0x23=AssemblyRef,0x26=FileDef,0x27=ExportedType)(注:原文为0x26=FileDef,这是错误的,元数据表中没有FileRef,作者在第一版就出现了这个错误,这都第三版了,不知道为什么还没有修订)。更完整的列表可以参考.NET Framework SDK中CorHdr.h文件中的CorTokenType枚举类型。标记中低位的3个字节标识相关元数据表中的行。例如,实现标记0x26000001表示FileDef表的第1行。对于大多数表来说,行从1开始,而不是0.对于TypeDef表,行从2开始。

任何使用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'

这意味着要生成一个程序集,它所引用的所有文件都必须存在。

客户端代码的执行过程,就是调用方法的过程。当方法第一次调用时,CLR检测该方法引用了哪些类型,如参数、返回值或本地变量等。CLR然后尝试加载引用的程序集中包含清单的那个文件。如果访问的类型在该文件里,CLR将执行内部记账(bookkeeping),并允许该类型被使用。只有在调用引用了未被加载的程序集中类型的方法时,CLR才加载程序集文件。这意味着运行一个应用程序并不需要所引用的程序集中所有的文件都存在。

posted @ 2010-03-18 14:06  麒麟.NET  阅读(2346)  评论(6编辑  收藏  举报