第18章 程序集
概述
程序集是 .NET 中的基本 部署 单元,也是所有 类型 的容器。
程序集包含:
- 已编译的 类型
- IL(中间语言) 代码
- 运行时 资源
- 用于版本控制、安全及其他程序集的引用信息
- 定义了类型解析和安全许可的边界
一般来说,一个程序集包含单个的 Windows 可移植执行文件(WindowsPortableExecutable,PE)。如果是应用程序则带有 .exe 扩展名;如果是可重用的类库则带有 .dll 扩展名。WinRT 类库的扩展名为 .winmd,它和.dll 相似,但是它仅仅包含元数据,而并不包含中间语言代码。
18.1 程序集的组成部分
程序集包含 4 项内容:
- 程序集 清单:向 .NET 运行时提供各种信息,例如程序集的名称、版本、请求的权限以及其引用的其他程序集。
- 应用程序 清单:向操作系统提供必要的信息,例如如何部署该程序集以及是否需要管理员权限。
- 编译后的类型 :程序集中定义的类型的元数据以及编译后的 IL 代码。
- 资源 :嵌入程序集中的其他数据,例如图像和本地化文本。
18.1.1 程序集清单
程序集清单的目的有二:
- 向 托管宿主环境 描述程序集。
- 像一个目录一样存储着程序集的 模块 、 类型 和 资源 。
因此,程序集是 自描述 的。消费者无须额外的文件就可以发现程序集的数据、类型、函数等所有内容。
Tips
程序集清单并不是显式添加到程序集中的,而是在编译过程中自动嵌入到程序集中的。
以下总结了程序集清单中存储的主要数据:
- 程序集的简单名称
- 版本号(AssemblyVersion)
- 程序集的公钥和已签名的散列码(如果该程序集是强命名程序集)
- 一系列引用的程序集、包括它们的版本和公钥
- 组成程序集的一系列模块
- 程序集中定义的一系列类型和包含各个类型的模块
- 一组程序集需要的或拒绝的安全权限(SecurityPermission)(可选)
- 如果该程序集是一个附属程序集,则还包含该程序集的文化设定(AssemblyCulture)
还可以存储如下信息:
- 该程序集的完整标题和描述信息(AssemblyTitle 以及 AssemblyDescription)
- 公司和版权信息(AssemblyCompany 和 AssemblyCopyright)
- 该程序集的显示版本(AssemblyInformationalVersion)
- 自定义数据的其他属性
18.1.1.1 指定程序集的特性
可以利用程序集特性指定大部分程序集清单内容。例如:
[assembly: AssemblyCopyright("\x00a9 Corp Ltd. All rights reserved.")]
[assembly: AssemblyVersion("2.3.2.1")]
上述声明通常定义在 Properties 文件夹下的 AssemblyInfo.cs 文件中。我们可以在该文件添加相应的程序集信息。
Info
在新版 .NET 中,部分程序集清单内容通过 .csproj 文件指定。那些 .csproj 不支持的特性仍可使用 AssemblyInfo.cs 指定。
18.1.2 应用程序清单
应用程序清单是一个 XML 文件,它向操作系统提供程序集相关的信息。应用程序清单若存在,则会在 .NET 托管宿主环境加载该程序集之前进行读取和处理,从而影响操作系统启动应用程序进程的方式。
一个简单的应用程序清单文件内容如下,该清单内容指示操作系统当前程序集要求获得管理员权限:
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
18.1.2.1 部署应用程序清单
部署 .NET 应用程序清单有两种方式:
-
部署为 程序集所在文件夹中的一个特殊命名的文件
该文件名称必须匹配 程序集 名称,且后缀为 .manifest。以 MyApp.exe 为例,清单文件的名称则应为 MyApp.exe.manifest。
-
嵌入到 程序集 中
需要先构建程序集,再使用 .NET 的 mt 工具执行嵌入操作:
mt -manifest MyApp.exe.manifest -outputresource:MyApp.exe;#1
Tips
在《C#12 核心技术指南》中,只讲解了第二种方式,并且 VS 集成了该操作,无需使用命令行。
18.1.3 模块
程序集的内容实际存储在“ 模块 ”中,一个程序集可以有 多 个模块,每个模块包含了程序集的组成部分。下面是一个单模块(文件)程序集的结构示例:
C7.0 核心技术指南 第7版.pdf - p778 - C7.0 核心技术指南 第 7 版-P778-20241121131255-ujz367u
VS 没有对多文件按程序集的编译提供支持,需要开发者通过命令行手动集成:
- 使用 csc 编译器的 /t 开关创建每个模块;
- 使用程序集链接工具 al.exe 将其链接
下面是一个多模块(文件)程序集的结构示例:
C7.0 核心技术指南 第7版.pdf - p779 - C7.0 核心技术指南 第 7 版-P779-20241121131520-9bqn6bj
多文件程序集的使用场景很少,主要用于反射(见 19.3 反射程序集和 #ref#19.6)
Tips
.NET、.NET Core 弃用了多文件程序集,仅 .NET Framework 支持。
18.1.4 Assembly
类
System.Reflection 命名空间下的 Assembly
类是在运行时访问程序集元数据的入口。获取 Assembly
的方式有:
-
Type
的Assembly
属性Assembly a = typeof(Program).Assembly
在 UWP 中略有不同:
Assembly a = typeof(Program).GetTypeInfo().Assembly;
-
Assembly
类的静态方法仅适用于桌面应用程序:
-
GetExecutingAssembly()
方法:返回 定义当前正在执行的函数的类型 所在程序集 -
GetCallingAssembly()
方法:返回 调用当前执行函数的函数的类型 所在程序集 -
GetEntryAssembly()
方法:返回 定义应用程序初始入口方法的 程序集
-
Assembly
有如下常用成员:
成员 | 用途 | 参考章节 |
---|---|---|
FullName , GetName() |
返回程序集的完全限定名称,或返回 AssemblyName 对象 |
#ref#18.3 |
CodeBase , Location |
返回程序集文件的位置 | #ref#18.7 |
Load() , LoadFrom() , LoadFile() |
手动将程序集加载到当前应用程序域中 | #ref#18.7 |
GlobalAssemblyCache |
判断程序集是否在 GAC(全局程序集缓存)中 | #ref#18.5 |
GetStatelliteAssembly() |
找到给定文化的附属程序集 | #ref#18.6 |
GetType() , GetTypes() |
返回程序集中定义的一个类型/所有类型 |
19.1.1 获取类型 |
EntryPoint |
返回应用程序的入口方法的 MethodInfo |
19.2 反射并调用成员 |
GetModules() , ManifestModule |
返回程序集中的所有模块或者主模块 | 19.3.2 模块 |
GetCustomAttributes() |
返回程序集的特性标记 | 19.4.4 在运行时检索特性 |
18.2 强名称和程序集签名 #suspend#
对于签名这部分内容我有诸多不明白之处,现在看完全不懂,推后
强命名的程序集具有唯一的和不可更改的标识。它通过在清单中添加两种元数据实现:
- 程序集作者的唯一编号。
- 程序集签名后的散列值,以证实该程序集是由持有其唯一编号的作者生成的。
18.3 程序集名称
程序集的标识由 4 个数据构成(皆来自程序集清单):
-
简单名称
来自编译时的文件名省去后缀,如“System.Xml.dll”的简单名称为“ System.Xml ”
-
版本
来自 AssemblyVersion
特性
-
文化设定
来自
AssemblyCulture
特性,适用于 附属 程序集 -
公钥令牌
见上一节
Info
关于“附属程序集”,在《WPF 编程宝典》种我们也有过提及:7.4 本地化
这 4 个标识刚好组成了程序集的完全限定名称
18.3.1 完全限定名称
程序集的完全限定名称是所有标识组成的字符串,可以通过 Assembly.FullName
属性获得,其格式为:
- 程序集没有
AssemblyVersion
特性:版本会显示为“ 0.0.0.0 ” - 程序集未签名:公钥令牌会显示为“ null ”
18.3.2 AssemblyName
类
通过 Assembly.FullName
获取程序集信息(清单内容)显然很繁琐。为此 .NET 提供了 AssemblyName
类型,它通过相应的属性暴漏程序集信息。它有两个用途:
- 解析或者构建完全限定程序集名称。
- 存储额外数据用于查找、解析程序集。
获取方式如下:
-
实例化
AssemblyName
对象时 传入完全限定名称 ; -
通过
Assembly
实例的 GetName()
方法; -
通过
AssemblyName.GetAssemblyName()
静态方法,需传入 程序集文件在磁盘上的路径 。仅限于桌面应用
-
实例化
AssemblyName
后再分别设置每一个属性以构建完全限定名称
以下是 AssemblyName
的一些重要成员:
返回值 | 成员 | 说明 |
---|---|---|
string |
FullName { get; } |
完全限定名称 |
string |
Name { get; set; } |
简单名称 |
Version |
Version { get; set; } |
程序集版本,内部具有 Major 、Minor 等属性,可以使用 ToString() 获得其版本 |
CultureInfo |
CultureInfo { get; set; } |
仅附属程序集 |
string |
CodeBase { get; set; } |
见 18.7.3.3 Location 与 CodeBase 属性 |
byte[] |
GetPublicKey(); |
获取公钥,长度为 160 bytes |
void |
SetPublicKey(byte[] key); |
|
byte[] |
GetPublicKeyToken(); |
公钥标记,该标记为应用程序或程序集签名时所用公钥的 SHA-1 哈希值的最后 8 个字节。 |
void |
SetPublicKeyToken(byte[] publicKeyToken); |
18.3.3 程序集的信息版本和文件版本
由于版本(AssemblyVersion)是程序集名称的一部分,改变该特性会改变程序集的标识,进而影响程序集 引用 的兼容性。如果不包含破坏性更新,使用者并不希望程序集的引用被破坏。
此时可以使用如下两种方式指定版本相关信息:
-
AssemblyInformationVersion
特性:展示给客户的版本号,会展示在 Windows 文件属性对话框的“Product Version(产品版本)”项中,可以包含任意字符,如“5.1 Beta2”。 -
AssemblyFileVersion
特性:原本用于指定程序集的构建号(Build)。该版本号会展示在 Windows 文件属性对话框的“File Version(文件版本)”项中。和 AssemblyVersion 一样,它是由点分隔的 4 组数字构成。
18.4 认证代码签名 #suspend#
18.5 全局程序集缓存 #suspend#
18.6 资源和附属程序集
应用程序通常不仅仅包含可执行代码,还包含文本、图像或者 XML 文件等内容。这些内容可以作为程序集中的资源。
资源的主要使用场景有二:
-
容纳无法进入 源代码 的数据,例如图像
这些程序集资源最终是一个带有名称的 字节流 。嵌入方式见 18.6.1 直接嵌入资源
-
存储在多语言应用程序中需要进行 翻译 的数据
这些内容一般通过中间的 .resources 容器添加内容,最终打包成独立的 附属 程序集,在运行时根据用户操作系统的语言自动进行加载。生成方式见 18.6.2 .resources 文件
下图演示了一个程序集结构,包含了上述提到的两种资源:
C7.0 核心技术指南 第7版.pdf - p792 - C7.0 核心技术指南 第 7 版-P792-20241121224613-debye79
18.6.1 直接嵌入资源
通过 VS 中嵌入资源方式如下:
- 将文件添加至项目中
- 在其属性页面设置其 Build Action(构建操作)为“ Embedded Resource(嵌入的资源) ”
VS 会将 项目的默认命名空间 作为资源名称的前缀,并添加其所在的 文件夹和子文件夹 名称。例如项目默认命名空间为 Westwind.Reports
,资源文件存储在 pictures 文件夹内,名称为 banner.jpg,那么其资源名称为 Westwind.Reports.pictures.banner.jpg
Notice
资源名称是区分大小写的,因此 VS 中包含资源的 子文件夹 也是区分大小写的。
Summary
在 WPF 中构建操作似乎有所不同,见 10.3.2 添加多媒体文件、7.3.1 添加资源。
关于“嵌入的资源”我仅在 2.2.5 自定义控件的图标处见过使用。
C7.0 核心技术指南 第7版.pdf - p794 - C7.0 核心技术指南 第 7 版-P794-20241121232638-pmh6jmo
Tips
嵌入资源还可以通过命令行完成,在编译时使用“/resource”开关:
csc /resource:banner.jpg /resource:data.xml MyApp.cs
还可以为资源指定不同的名称(可选):
csc /resource:<file-name>,<resource-name>
18.6.1.1 嵌入资源的使用
获取资源可以在包含该资源的程序集(Assembly
)上调用 GetManifestResourceStream()
方法,该方法会返回一个可 查找(seekable) 的 流 。
若想获得程序集中所有资源的名称,可以使用 GetManifestResourceNames()
方法。
获取流相应的代码如下:
Assembly a = Assembly.GetEntryAssembly();
using (Stream s = a.GetManifestResourceStream("TestProject.data.xml"))
using (XmlReader r = XmlReader.Create(s))
...
System.Drawing.Image image;
using (Stream s = a.GetManifestResourceStream("TestProject.banner.jpg"))
image = System.Drawing.Image.FromStream(s);
// 流是可查找的,因此还可以这么用:
byte[] data;
using (Stream s = a.GetManifestResourceStream ("TestProject.banner.jpg"))
data = new BinaryReader (s).ReadBytes ((int) s.Length);
上述方法是通过命令行添加的资源,资源名称不包含各种前缀。使用 VS 嵌入的资源则需要包含上一节提到的各种前缀。为了避免手动添加导致的错误,可以使用 类型( Type
) 作为独立参数指明前缀(类型的命名空间会作为前缀):
using (Stream s = a.GetManifestResourceStream (typeof (X), "data.xml"))
其中 X
可以是和资源具有相同命名空间的任意类型(一般情况下就是相同项目文件夹下的类型)。
18.6.2 .resources 文件
.resources 文件包含的内容多是 潜在的本地化 内容。和其他文件一样,.resources 文件最终会成为程序集的一个嵌人式资源。但它们区别在于:
- .resources 文件要求首先将内容打包至 .resources 文件中;
- 可以通过
ResourceManager
类、 pack URI 对资源内容进行访问,但不通过GetManifestResourceStream()
访问
.resources 是二进制文件,开发者无法直接编辑。为此微软设计了 .resx 文件,VS 和 .NET 通过它对 .resources 文件进行管理,VS/resgen 工具可以将其转化为 .resources 文件。
.resx 文件适用于 Windows Forms 和 ASP.NET 应用,WPF 需要使用 URI 应用图像等二进制内容,必须将其构建为“Resource(资源)”
18.6.3 .resx 文件
.resx 文件是一种能够生成 .resources 文件的设计时格式。.resx 文件是一个 XML 格式的键值对文件:
<root>
<data name="Greeting">
<value>hello</value>
</data>
<data name="DefaultFontSize" type="System.Int32, mscorlib">
<value>10</value>
</data>
</root>
VS 中可以通过添加“Resource File(资源文件)”添加. resx 文件,VS 会自动完成以下工作:
- 创建正确的头部
- 提供设计器界面。可以从设计器中添加字符串、图像、文件以及其他类型的数据。
- 将 .resx 自动转换为 .resources 格式并在编译时嵌入到程序集中。
- 生成一个 类 来访问这些数据。
Notice
资源设计器会将图像添加为
Image
类型的对象(System.Drawing.dll),而非字节数组,因此资源设计器并不适用于 WPF 应用程序。
18.6.3.1 使用命令行创建 .resx 文件 #suspend#
18.6.3.2 读取 .resources 文件
在 VS 中创建 .resx 文件时,有:
- 生成一个 类 来访问这些数据。
我们可以通过该类的属性访问每一个资源,也可以用:
-
ResourceManager
类读取嵌入程序集中的 .resources 文件; - 调用
GetString()
或 GetObject()
(并强制类型转换)来访问其中的内容:
ResourceManager r = new ResourceManager("welcome", Assembly.GetExecutingAssembly())
string greeting = r.GetString ("Greeting");
int fontSize = (int) r.GetObject ("DefaultFontSize");
Image image = (Image) r.GetObject ("flag.png");
还可以通过 ResourceSet
枚举 .resources 文件的内容:
ResourceManager r = new ResourceManager (...);
ResourceSet set = r.GetResourceSet(CultureInfo.CurrentUICulture,
true, true);
foreach (System.Collections.DictionaryEntry entry in set)
Console.WriteLine (entry.Key);
Info
另可见 ResourceManager.GetResourceSet() 方法
18.6.3.3 在 Visual Studio 中创建 pack URI 资源
WPF 的 XAML 文件需要通过 URI 访问资源,而 .resx 文件中的资源是无法通过 URI 加载的(即 .resx 不适用于 WPF)。
在 WPF 中必须将文件添加至 项目 中,并将其构建行为设置为“ Resource(资源) ”(注意,不是“Embedded Resource”)。VS 会将这些文件编译为一个名为 \(<程序集名称>.g.resources\) 的 .resources 文件(XAML(即 .baml)也会编译到该文件中)。
访问该资源的方式有三:
-
XAML 中通过 URI 访问
-
通过
Application.GetResourceStream()
方法加载资源// 如下为相对 URI,也可以使用绝对 URI: // Uri u = new Uri ("pack://application:,,,/flag.png"); Uri u = new Uri ("flag.png", UriKind.Relative); using (Stream s = Application.GetResourceStream (u).Stream)
-
为
ResourceManager
指定 Assembly
对象,再进行检索Assembly a = Assembly.GetExecutingAssembly(); ResourceManager r = new ResourceManager (a.GetName().Name + ".g", a); using (Stream s = r.GetStream ("flag.png")) ...
Info
另见 7.3.1 添加资源
18.6.4 附属程序集
嵌入 .resources 文件中的数据是可以进行本地化的。资源本地化的典型配置方式如下:
- 主程序集包含 默认的或者备用 语言的 .resources 文件。
- 独立的附属程序集包含 翻译为不同语言的本地化 .resources 文件。
应用程序运行时,Framework 会检测当前操作系统的语言(使用 CultureInfo.CurrentUICulture
)。当使用 ResourceManager
请求资源时:
- Framework 首先查找 附属 程序集,若存在且包含所需资源的 键 ,则使用 附属 程序集中的资源;
- 未找到,使用 主 程序集中的资源
Warn
附属程序集不得包含可执行代码,只能包含资源。
附属程序集会部署在程序集所在文件夹的子文件夹中,结构大致如下:
programBaseFolder\MyProgram.exe
\MyLibrary.exe
\XX\MyProgram.resources.dll
\XX\MyLibrary.resources.dll
其中“XX”代表两个字母的语言代码(例如“de”代表德语)或区域代码(例如“en-GB”代表应该英语)。
18.6.4.1 构建附属程序集
构建附属程序集分以下几步:
-
创建默认 .resx 文件
假设该默认 .resx 文件名为 welcome.resx 内容为:
<root> ... <data name="Greeting" <value>hello</value> </data> </root>
-
创建其他文化的 .resx 文件,并令文件名包含文化信息
假设我们要添加的其他文化是德语,则新创建的 .resx 文件需命名为 * welcome.de.resx *,并将内容翻译:
<root> <data name="Greeting"> <value>hallo<value> </data> </root>
-
如果使用 VS,至此全部工作完成。如果使用命令行,则需要使用 resgen 工具(用于将 .resx 转为 .resources)和 al 工具(构建附属程序集):
resgen MyApp.de.resx al /culture:de /out:MyApp.resources.dll /embed:MyApp.de.resources /t:lib
若要导入主程序集的强名称,则需要使用
/template:MyApp.exe
开关
18.6.4.2 测试附属程序集
我们可以更改 Thread
类的 CurrentUICulture
属性来模拟不同语言的操作系统:
System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("de");
修改程序的主题文化要在显示窗口之前进行,因此一般在Application 派生类的 构造函数 ,或在
Startup
事件中设置 Thread.CurrentThread.CurrentUICulture
属性:
CultureInfo.CurrentUICulture
是该属性的只读版本。
18.6.5 文化和子文化
文化分为:
- 文化:特定的语言
- 子文化:该语言的特定地区变种
Framework 遵循 RFC1766 标准,以“语言-文化”分别代表文化和子文化。例如英语的文化为“en”,德语的文化为“de”;澳大利亚英语子文化为“en-AU”,奥地利德语子文化为“de-AT”。
.NET 使用 System.Globalization.CultureInfo
类表示文化。我们可以通过如下两个属性获取文化:
-
System.Threading.Thread.CurrentThread.CurrentCulture
:反映了 Windows 控制面板中的区域设置该设置(区域设置)包括时区、货币和日期格式等内容。因此该属性决定了
DateTime.Parse()
等方法的默认行为。我们可以根据需要修改其文化。 -
System.Threading.Thread.CurrentThread.CurrentUICulture
:反映了当前操作系统的语言该设置决定了人机交互所使用的语言
通过这两个属性,我们可以分别设置时区和语言,以便适应当地使用条件。以澳大利亚的计算机为例,对上述属性打印将分别输出“EN-AU”和“EN-US”,这是因为澳大利亚可以直接使用美式英语,但时区却和美国不同。
默认情况下 ResourceManager
会使用当前线程的 CurrentUICulture
属性决定加载哪一个附属程序集。ResourceManager
加载资源时会采用“后备机制”:
- Framework 首先查找 附属 程序集,若存在且包含所需资源的 键 ,则使用 附属 程序集中的资源;
- 未找到,使用 主 程序集中的资源
18.7 程序集的解析和加载
程序集的解析是定位引用程序集的过程。程序集的解析发生在:
-
编译 时:解析较为简单,我们(或 VS)会告知程序集地址,如果引用的程序集不在当前目录下,我们(或 VS)会提供它的完整路径。
-
运行 时:CLR 会先查找主程序所在文件夹,如果遇到如下两种情况将变得复杂:
-
引用的程序集部署在其他位置
-
需要动态加载程序集
18.7.1 程序集和类型解析规则
当我们引用了一个程序集类型时,CLR 会在首次执行时加载程序集。假设我们有一个程序 AdventureGame.exe,该程序实例化了一个名为 TerrainModel.Map
的类型,CLR 会这样工作:
- 在 AdventureGame.exe 的 程序集清单 中,
TerrainModel.Map
所在的程序集的完全限定名是什么? - 该程序集是否已经 加载(至内存) ?
如果答案是肯定的(已加载),它会使用 内存中的现有副本 ,否则它(CLR)会继续寻找程序集:
-
先检查 GAC
-
再检查 探测路径
通常是 应用程序 所在目录
-
触发
AppDomain.AssemblyResolve
事件 -
最终匹配失败,抛出异常
18.7.2 AssemblyResolve
事件
AssemblyResolve
事件允许在 CLR 无法找到程序集时进行干预乃至手动加载程序集。处理该事件,就可以加载那些散布在其他位置的程序集。
在该事件中,可以通过 Assembly
的静态方法定位并加载程序集,这些静态方法有:
-
Load()
-
LoadFrom()
-
LoadFile()
关于这些方法的使用,见下一节。
加载到的程序集通过事件处理器的 返回值 给到调用者:
static void Main()
{
AppDomain.CurrentDomain.AssemblyResolve += FindAssembly;
}
static Assembly FindAssembly(object sender, ResolveEventArgs args)
{
string fullQualifiedName = args.Name;
Assembly a = Assembly.LoadFrom(...);
return a;
}
因 AssemblyResolve
事件具有返回值,如果有多个事件处理器,它会采用 第一 个返回非 空 Assembly
对象的事件处理器。
18.7.3 加载程序集
Assembly
类的 Load*()
方法并不仅限于在 AssemblyResolve
事件处理器中使用。它可以加载、执行那些非编译时引用的程序集(如插件程序)。
上一节提到的三个方法有如下区别:
-
Load()
方法:-
完全限定名称加载程序集,CLR 将采用通常的方式寻找并自动解析程序集
CLR 本身也会使用该方法查找程序集
-
从字节数组加载程序集
-
-
LoadFrom()
方法:- 从文件名加载程序集
- 从 URI 加载程序集
-
LoadFile()
方法:- 从文件名加载程序集
若想得知当前已加载到内存中的程序集列表,可以使用 AppDomain
的 GetAssemblies()
方法:
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
assembly.Location.Dump();
assembly.CodeBase.Dump();
assembly.GetName().Name.Dump();
}
Warn
请慎重调用
Load()
、LoadFrom()
和LoadFile()
:因为不论是否使用这些加载的Assembly
对象,这些方法都会永久地将程序集加载到当前的应用程序域中。加载程序集是有副作用的,它会锁定程序集文件,还会影响后续的类型解析。卸载程序集的唯一方法是卸载整个应用程序域。(另一个避免程序集锁定的方式是对在探测路径上的程序集进行影子拷贝(shadowcopy),请参考卷影复制程序集 - .NET Framework | Microsoft Learn)。如果仅希望检查程序集但是并不执行其代码,那么可以将其加载到只反射上下文(reflection-onlycontext)中(请参见 19.3 反射程序集)。
18.7.3.1 从文件名加载
LoadFrom()
和 LoadFile()
都可以从文件名加载程序集,但是它们的行为略有不同:
-
传入的程序集位置和内存中已加载的程序集位置不同、标识相同:
LoadFrom()
会返回已加载的副本, LoadFile()
会提供新的副本如下两段代码分别输出: true 、 false
Assembly a1 = Assembly.LoadFrom(@"c:\temp1\lib.dll"); Assembly a2 = Assembly.LoadFrom(@"c:\temp2\lib.dll"); (a1 == a2).Dump();
Assembly a1 = Assembly.LoadFile(@"c:\temp1\lib.dll"); Assembly a2 = Assembly.LoadFile(@"c:\temp2\lib.dll"); (a1 == a2).Dump();
-
传入的程序集位置和内存中已加载的程序集位置相同、标识相同:
LoadFrom()
和LoadFile()
行为一致,均返回 已加载的 副本 -
传入的是字节数组(仅
Load()
方法支持),每次都返回 不同的 副本
Warn
在内存中,来自两个相同程序集的类型是不能互相兼容的。这是尽量避免加载相同程序集的主要原因,开发者应尽量使用
LoadFrom()
而非 LoadFile()
。
LoadFrom()
更为健壮一点的是,它在加载程序集时会自动加载关联程序集。假设我们要加载的 \folder2\TestLib.dll 依赖 \folder2\Another.dll, LoadFrom()
会自动查找并加载 Another.dll, LoadFile()
则会 抛出异常 /引发 AssemblyResolve
事件
18.7.3.2 静态引用类型与 LoadFrom()
/LoadFile()
方法
-
静态引用:直接在代码中引用的类型称为静态引用(即在项目中添加引用)。
编译器会:
- 添加该类型的引用
- 添加该类型所在的程序集名称
-
动态加载:通过
Assembly.Load*()
方法加载程序集Type t = Assembly.LoadFrom(@"d:\temp\foo.dll").GetType("Foo"); var foo = Activator.CreateInstance(t);
若对同一程序集混用上述两种方法,将造成内存出现程序集的两个副本(其类型不兼容)。开发者应避免这种情况,在使用 LoadFrom()
/LoadFile()
前要小心确认该程序集是否存在于应用程序的基础目录。这里建议在 AssemblyResolve
事件中加载程序集避免此类错误。
Tips
无论使用
LoadFrom()
还是LoadFile()
,CLR 都一定会先在 GAC 中查找请求的程序集。若希望跳过 GAC,则可以使用ReflectionOnlyLoadFrom()
方法(见19.3.1 将程序集加载至 reflection-only 的上下文中),该方法只会将程序集加载到只反射上下文中。虽然从字节数组加载程序集可以解决程序集文件锁定的问题,但是这种方式也无法跳过 GAC:byte[] image = File.ReadAllBytes(assemblyPath); Assembly a = Assembly.Load(image);
如果需要从字节数组加载程序集,则必须处理
AppDomain
的AssemblyResolve
事件才能解析加载程序集本身引用的其他程序集,并跟踪所有已加载的程序集(请参见18.9 打包单个可执行文件)。
18.7.3.3 Location
与 CodeBase
属性
Assembly
的 Location
和 CodeBase
属性用于获取程序集路径。它们二者有如下区别:
-
Location
属性:文件系统的 物理路径 (如果存在的话) -
CodeBase
属性:程序集的 URI 值
硬盘上的程序集 | 程序集从 Internet 加载 | 影子拷贝(ASP.NET 和 NUnit 支持该机制,以便在网站运行/单元测试时仍能更新程序集) | |
---|---|---|---|
Location |
物理路径 | 下载后的临时存储路径 | 空(书中内容和 MSDN 文档存在出入。根据 MSDN 文档,因子拷贝后的程序集其 Location 属性值和背靠背的文件的 Location 属性值相同。 请参见 https://learn.microsoft.com/zh-cn/dotnet/api/system.reflection.assembly.location?view=netframework-4.8.1) |
CodeBase |
物理路径的 URI 值 | Internet 的 URI 值 | 源 程序集的位置 |
Location
属性可能为空,因此不能仅靠 Location
获取程序集在磁盘上的位置,应同时使用二者:
public static string GetAssemblyFolder(Assembly a)
{
try
{
if (!string.IsNullOrEmpty(a.Location))
return Path.GetDirectoryName(a.Location);
if (string.IsNullOrEmpty(a.CodeBase)) return null;
var uri = new Uri(a.CodeBase);
if (!uri.IsFile) return null;
return Path.GetDirectoryName(uri.LocalPath);
}
catch (NotSupportedException)
{
return null; // 使用 Reflection.Emit 生成的动态程序集不支持
}
}
18.8 在基础目录之外部署程序集
本节演示 AssemblyResolve
事件的使用:
class Loader
{
static void Main()
{
AppDomain.CurrentDomain.AssemblyResolve += FindAssembly;
Program.Go();
}
static Assembly FindAssembly(object sender, ResolveEventArgs args)
{
string simpleName = new AssemblyName(args.Name).Name;
string path = @$"c:\ExtraAssemblies\{simpleName}.dll";
if (!File.Exists(path)) return null;
return Assembly.LoadFrom(path);
}
class Program
{
internal static void Go()
{
// 现在我们可以使用定义在 c:\ExtraAssemblies 中的类型了
}
}
}
18.9 打包单个可执行文件
假设我们编写了一个包含 10 个程序集的应用程序:1 个主要的可执行文件,外加 9 个 DLL。这样的划分虽然有助于设计和调试,但是用户需要执行安装或解压之类的操作。
我们可以将编译好的(9 个)程序集 DLL 作为 资源 嵌入到(1 个可执行文件)主项目中,并处理 AssemblyResolve
事件,以便在需要时直接加载其二进制映像。
以下展示了具体的做法:
class Loader
{
static Dictionary<string, Assembly> _libs = new Dictionary<string, System.Reflection.Assembly>();
static void Main()
{
AppDomain.CurrentDomain.AssemblyResolve += FindAssembly;
Program.Go();
}
static Assembly FindAssembly(object sender, ResolveEventArgs args)
{
string shortName = new AssemblyName(args.Name).Name;
if (_libs.ContainsKey(shortName)) return _libs[shortName];
using (Stream s = Assembly.GetExecutingAssembly().GetManifestResourceStream($"Libs.{shortName}.dll"))
{
byte[] data = new BinaryReader(s).ReadBytes((int)s.Length);
Assembly a = Assembly.Load(data);
_libs[shortName] = a;
return a;
}
}
class Program
{
internal static void Go()
{
// 现在我们可以使用定义在 c:\ExtraAssemblies 中的类型了
}
}
}
将所有程序集缓存在字典中是为例确保 CLR 再次请求同一程序集时返回相同对象,毕竟:
- 传入的是字节数组(仅
Load()
方法支持),每次都返回 不同的 副本
该例子的一个变体是在编译时压缩引用的程序集,然后在 FindAssembly
时使用 DeflateStream
将其解压。
18.10 处理未引用的程序集
本节演示动态加载程序集的使用。
18.10.1 可执行程序的执行
对于可执行程序,如果希望执行该程序,可调用当前应用程序域(AppDomain
类型)的 ExecuteAssembly()
方法,并传入程序路径。该方法会使用 LoadFrom()
方法加载可执行文件:
string dir = AppDomain.CurrentDomain.BaseDirectory;
AppDomain.CurrentDomain.ExecuteAssembly(Path.Combine(dir, "test.exe"));
Notice
ExecuteAssembly()
是 同 步方法,该方法在调用的程序集结束之前会保持阻塞。若想异步调用,必须在另一个线程/任务上调用该方法。
18.10.2 程序集类型的使用
对于动态加载的库文件,一般通过如下两步调用其中的成员:
- 使用
LoadFrom()
加载程序集; - 通过 反射 处理程序集中的类型。
下面是一个简单的用例:
string ourDir = AppDomain.CurrentDomain.BaseDirectory;
string plugInDir = Path.Combine(ourDir, "plugins");
Assembly a = Assembly.LoadFrom(Path.Combine(plugInDir, "widget.dll"));
Type t = a.GetType("Namespace.TypeName");
object widget = Activator.CreateInstance(t);
...
上述代码通过反射得到的是 object
实例,进一步通过反射调用其中的成员并不方便。一个常用的做法是定义 接口 ,并令反射得到的实例实现了该 接口 :
public interface IPluggable
{
void ShowAboutBox();
...
}
Type t = a.GetType("Namespace.TypeName");
IPluggable widget = (IPluggable)Activator.CreateInstance(t);
widget.ShowAboutBox();
上述策略常用在 WCF、Remoting 服务器中动态发布服务。假设我们要公开的库均以“server”结尾,我们可以通过如下方式动态加载、执行程序集中的方法:
string dir = AppDomain.CurrentDomain.BaseDirectory;
foreach (string assFile in Directory.GetFiles(dir, "*Server.dll"))
{
Assembly a = Assembly.LoadFrom(assFile);
foreach (Type t in a.GetTypes())
{
if (typeof(MyBaseServerType).IsAssignableFrom(t))
{
// 暴漏类型 t
}
}
}
上述发布方式虽然好用,但可能被非法入侵。这里建议对程序集使用同一 公钥 签名,并显式检查 公钥 :
byte[] ourPK = Assembly.GetExecutingAssembly().GetName().GetPublicKey();
foreach (string assFile in Directory.GetFiles(dir, "*Server.dll"))
{
byte[] targetPK = AssemblyName.GetAssemblyName(assFile).GetPublicKey();
if (Enumerable.SequenceEqual(ourPK, targetPK))
{
Assembly a = Assembly.LoadFrom(assFile);
...
}
}
标签:文件,Assembly,18,程序,资源,resources,加载 From: https://www.cnblogs.com/hihaojie/p/18646153/chapter-18-2ufcqz