在.NET框架平台中,装配件(Assembly)是一个崭新的概念。它是.NET框架应用程序的基础生成区块。它是一个功能的集合,可以被当作一个独立的实现单元被生成、版本化和发布。从本质上讲,装配件是一个逻辑上的动态链接库。
作为一名开发人员,应当对装配件有如下了解:
● 装配件包含CLR可以执行的程序代码
中间代码包含在可执行模块文件(如EXE、DLL)中,而这些模块文件是组成装配件的重要成员。
● 装配件组成了一个安全边界
在执行任何托管的代码时都需要符合两个条件:一是这些代码必须和清单(Manifest)一起封装在装配件中,二是CLR在加载时确认装配件内容正确而且未被修改过。关于清单的知识我们稍后会做详细介绍。
● 装配件组成了一个类型边界
它本身具有的边界可以被某个类型当作其身份标识的一部分。例如,一个名叫ABCType的类型是从AssemblyA中加载而来的,它和另一个从AssemblyB中加载而来的同名类型是完全不同的,这种关系如图2-31所示。
![](http://cimg2.163.com/catchpic/D/DF/DFD877FDBEDD717F5AF60B47C0936FB2.gif)
图2-31 ABCType != ABCType
● 装配件组成了一个引用范围边界
在它内部含有一个清单,这个清单具有对类型和资源引用的分辨能力。清单指明了宿主装配件中哪些类型和资源可以暴露给外界。另外,清单还可以记录宿主装配件对其他装配件的依赖情况。
● 装配件组成了一个版本边界
它是CLR中最小的版本化单元—— 在同一个装配件中所有的类型和资源都被版本化成一个单元。它的清单描述了对其他有装配件的版本依赖。具体内容我们稍后会做介绍。
● 装配件组成了一个发布单元
当应用程序启动时,仅仅需要那些在它初始化时必须要调用的装配件存在即可,不必让所有与之相关的装配件都存在。例如本地化资源(Localization Resources),只要在被需要的时侯存在即可。这种机制可以更好地实现软件分割,使下载的应用程序尽可能小些。有关托管的应用程序发布方面的更详细知识,稍后我们就要介绍。
● 装配件组成了一个可以支持“肩并肩”运行的单元
“肩并肩”执行方式是解决“DLL灾难”问题的有效办法,详细信息我们稍后会进行介绍。
单纯从物理存储的角度看,一个装配件中包含四个组成部分—— 清单元数据、类型元数据、IL以及资源,如图2-32所示。
从一个装配件消费者(Consumer)的角度看,一个装配件是一个被命名的和版本化了的输出类型和资源的集合;从装配件开发人员的角度,一个装配件是一个或多个文件的集合—— 一个单独的PE文件到多个PE文件、资源文件、HTML文件等等文件组成的集合体。
![](http://cimg2.163.com/catchpic/C/CA/CA260F2C68F0EB25B8E7437B39609177.gif)
图2-32 装配件的组成(物理存储)
既然装配件是一群可能由形形色色的文件组成的组合体,却不能将系统中随便几个文件放在一起就称其为装配件。装配件不是“乌合之众”的代名词,在装配件中存在着一个清单,这个清单包含组成装配件所需要的所有条目(Items)的信息。清单明确地指出了哪些条目可以对外开放,哪些条目只可以在装配件内部进行访问,另外清单中也包含一个对其他装配件进行引用的集合。为了便于理解,可以用仓库和仓库保管员之间的关系作为例子来说明装配件和清单之间的关系—— 仓库相当于装配件,而保管员手中的仓储明细登记表就相当于清单,因为在这个登记表中,记录了所有库存商品的明细。
清单中存储的信息在运行时被运行时用来进行引用分析,从而实现类型安全,以及进行安全检查等。清单在装配件消费者和装配件具体实现之间构成了一个间接的层。清单的存在使装配件可以实现自我描述(Self Description)。
如果一个装配件仅仅由一个文件组成,则这个装配件的清单必然存在于这个文件当中;如果装配件是由多个文件组成的,那么要么选择这些文件当中的一个文件(必须是PE文件)作为清单保管员(Manifest Keeper),要么就需要单独建立一个文件,存放此清单。总而言之,只要是装配件,就不能没有清单。装配件和清单的关系如图2-33所示。
![](http://cimg2.163.com/catchpic/A/AD/ADD38F00E3AAA5F9656B3827E0546591.gif)
图2-33 清单
在装配件清单中,主要包含如下信息:
● 装配件的名称
包含一个文本字符串。
● 版本信息
包含主版本号、次版本号、修正版本号以及生成版本号。这些版本号可以让运行时实现严格的版本策略,关于版本的具体内容,在第10章中将进行详尽的介绍。
● 共享名称信息
包含由公开密钥和数字签名组合而成的信息,关于共享名的具体内容,在第10章中将进行详尽的介绍。
● 国别、处理器和操作系统信息
包含装配件支持的国别、处理器以及操作系统信息。
● 装配件中所有文件的列表
包含在装配件中的每个文件的散列(hash)和一个与清单所在文件相关的路径。
注意:
组成装配件的所有文件必须和清单所在的文件在同一个目录下。
● 类型引用信息
包含为运行时所使用的类型引用信息。运行时可以将运行时中的类型引用根据描述映射到包含这个类型定义和实现的文件当中。
● 被当前装配件引用的其他装配件的信息
包含当前装配件静态引用的其他装配件的列表。每个引用包含所依赖的装配件的名称、元数据以及共享名称(如果这个装配件是共享的)。有关共享装配件以及元数据的讨论,稍后进行介绍。
接下来,让我们了解另外一个核心概念—— 元数据(Metadata),首先我们说明一下什么是元数据。
元数据是包含类型描述和本身对其他装配件的引用的详细信息的数据块。为了让运行时能够为托管的代码的执行提供服务,语言编译器必须可以产生元数据。元数据和中间代码被一起存储到PE文件,即每一个可加载的CLR映像(Image)都要包含元数据。运行时使用元数据来定位并加载类,在内存中完成布置对象实例、分析方法调用、产生本机代码、实施安全,以及设置运行时上下文(Context)边界等任务。
为了能尽早让开发人员对元数据有一个感性认识,下面我们动手生成一个简单的.NET应用程序,并通过这个程序中来了解元数据的有关知识。
这是一个简单的输出“Hello,World!”的小程序。如果阅读该程序时,对里面出现的一些语法感到困惑,请先不要急于搞懂它,稍后的的章节中会使用足够的篇幅来讲解这些代码的含义,现在我们只是借用这个程序来说明问题。这段程序的源代码可以在配书光盘的/SourceList/_Chap02/Hello目录下找到,其代码如下所示:
Hello.cpp
#using <mscorlib.dll>
// 使用名称空间,
// 便于程序对存在于该名称空间中的类进行调用
using namespace System;
// 全局函数main是应用程序的入口点
void main()
{
// 向控制台输出一行文本
Console::WriteLine(L"Hello World!");
}
请确保已经在机器上妥善地安装了.NET Framework SDK或者Visual Studio.NET,然后按照如下步骤生成这个应用程序:
(1) 进入控制台(也就是MS-DOS窗口)。
(2) 在Hello.cpp的所在的目录下,输入编译命令:
cl.exe /CLR Hello.cpp
(3) 按Enter键确认,屏幕上会出现如下编译信息:
hello.cpp
Microsoft (R) Incremental Linker Version 7.00.9030
Copyright (C) 1992-2000 Microsoft Corporation. All rights reserved.
/out:hello.exe
hello.obj
(4) 到此编译成功结束。此时可以得到一个名为hello.exe的可执行文件。
现在,我们要用到微软提供的一个很有用的工具ILDasm.exe。使用这个工具可以对托管的应用程序进行反汇编,并可以形象地显示出被查看应用程序所包含的元数据。开发人员可以在.NET框架所在的目录下找到这个工具。
下面我们就使用这个工具来对hello.exe进行查看。在桌面上选择“开始”→“程序” →Microsoft .NET Framework SDK→“工具”→ILDasm命令后,ILDasm.exe就会运行,它的执行结果如图2-34所示。
![](http://cimg2.163.com/catchpic/7/77/778815CF0B00828D909AB633FC4D6CF4.jpg)
图2-34 ILDasm工具界面
可以清楚地看到,Hello.exe中的元数据被该工具以树型结构显示出来。使用鼠标双击MANIFEST节点,开发人员还可以看到该应用程序包含的清单内容,如图2-35所示。
![](http://cimg2.163.com/catchpic/3/32/320A63D1EC3EF6D7F1BE4F589EA4F3CB.jpg)
图2-35 MANIFEST信息窗口
请特别注意被着重标记的地方,前两个是对引用其他装配件进行的描述,而第三个是对本身的描述。显然,这是一个单独文件构成的装配件。
现在回到图2-34,将注意力集中在其他节点上,通过查阅工具的联机帮助,我们可以了解到那些色彩鲜艳的图标的含义,因为在后面的讨论中还要经常用到它们,所以在这里将它们整理出来,如表2-3所示。
表2-3 ILDasm工具图例含义
图 例 | 含 义 |
| 名称空间(Namespace) |
| 类(Class) |
| 接口(Interface) |
| 值类(Value Class) |
| 枚举(Enum) |
| 方法(Method) |
| 静态方法(Static Method) |
| 字段(Field) |
| 静态字段(Static field) |
| 事件(Event) |
| 属性(Property) |
| 清单或类信息条目(Manifest or a Class info item) |
对照表2-3,会发现在Hello.exe的元数据中包含了一个清单、一个枚举类型(带有两个类信息条目)、一个静态字段以及一个静态方法。双击静态方法的所在的节点,可以得到关于静态方法Main的反汇编信息,如图2-36所示。
![](http://cimg2.163.com/catchpic/D/D6/D6B9FBF8000CA32FC23E0FE6BD834CE8.jpg)
图2-36 静态方法Main的反汇编代码
一下子说了这么多,可能读者一时难以理解。造成这种理解上的障碍主要原因是因为元数据、清单和装配件之间层层嵌套、纠缠不清的关系。这个时候,我想应当好好整理一下思路,将三个概念之间的关系搞清楚。
为了说明问题,我们举个例子,假设AssemblyABC是一个多文件的装配件,它由两个文件组成,第1个是可执行文件A.EXE,第2个是动态链接库文件B.DLL,则元数据、清单和装配件的关系可以用图2-37来表示。
在图2-37中,我们可以看到几个明显的关系,这些关系分别是:
● 装配件包含了一个或多个PE文件,在本例中是两个PE文件。
● 注意元数据的范围。在每个PE文件都存在一个元数据,并且和IL代码没有包含关系,它们是构成新型PE文件的重要组成部份中的两个代表。
● 注意清单的位置。非常明显,它是元数据的一部分。
● 注意清单扮演的角色。清单中阐述了组成装配件的文件列表以及所在装配件的输出类型(还有其他)。
![](http://cimg2.163.com/catchpic/1/1F/1F9E34585F989B6144F4DE0DD70FF824.gif)
图2-37 元数据、清单和装配件的关系
元数据和清单的不同之处在于元数据在每个PE文件中都存在一个,而清单在一个装配件中只能存在一个;清单是元数据中概念上比较特殊的一部分,但是在物理存储上没有什么两样。另外,还记得持久化(Persistence)的概念吗?由于CLR要在运行时从PE文件的元数据中读取并创建对象,所以元数据就是实现应用程序持久化的基础。
*持久化:就是将在内存中的实体对象永久保存.
*持久层:就是把对象从内存中持久化到数据库,文件等的数据库操作层,为业务逻辑层提供了面向对象的API.
*PE文件指的是可移植可执行(Portable Executable)文件。这些文件通常是DLL和EXE文件。一个 PE 文件中可以有多个命名空间,也可以包括嵌套的命名空间。而一个命名空间可以拆分到多个 PE 文件。一个或多个 PE 文件(可能还有其它非PE文件,如资源文件)可以组合在一起创建程序集。程序集是可部署、可进行版本编号和可复用的物理单元。