浅谈.NET中程序集的动态加载

我想有不少人像我一样,刚开始使用.NET中动态加载程序集的功能时,会被Assebmly中那么多加载程序集的方法搞得无所适从。当求助于MSDN和Baidu、Google后,可能会更加迷茫——说实话MSDN中相关的说明确实很难理解甚至有自相矛盾的地方,网上的大多数资料也讲得不甚明了。所以,我在这里分享一下自己对这些函数及其背后相关概念的理解,希望能帮到大家。文中如有错误,还请大家指正。

本文的内容主要基于MSDN和Steven Pratschner的《Customizing the Microsoft® .NET Framework Common Language Runtime》一书,这本书应该算是每个想深入研究CLR的程序员的必读书目。


 

首先,向大家介绍一个非常好的工具——fuslogvw.exe(程序集绑定日志查看器)。使用它,我们可以查看CLR加载每一个程序集的决策过程。fuslogvw是与.NET Framework SDK一起发布的,如果你安装了Visual Studio,那么就可以通过在Visual Studio命令提示中键入fuslogvw运行它,其运行界面如下图所示。

image

在使用之前,需要点击“设置”按钮,勾选“记录所有绑定到磁盘”选项。如下图所示。

image

要查看代码中某个程序集的加载过程,可以先点击“全部删除”以清空当前日志,然后运行要检查的程序,之后点击“刷新”按钮,就可以看到相应程序集对应的条目了。左侧列表中的每一项对应一个应用程序加载的一个程序集,选中一个条目,点击“查看日志”按钮即可显示相应的内容,如下所示。

 1 *** 程序集联编程序日志项 (################) ***
 2 
 3 操作成功。
 4 绑定结果: hr = 0x0。操作成功完成。
 5 
 6 程序集管理器加载位置:  C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll
 7 在可执行文件下运行  D:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\TraceDebugger Tools\IntelliTrace.exe
 8 --- 详细的错误日志如下。
 9 
10 === 预绑定状态信息 ===
11 日志: 用户 = ##########12 日志: DisplayName = System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
13  (Fully-specified)
14 日志: Appbase = file:///D:/Program Files (x86)/Microsoft Visual Studio 10.0/Team Tools/TraceDebugger Tools/
15 日志: 初始 PrivatePath = NULL
16 日志: 动态基 = NULL
17 日志: 缓存基 = NULL
18 日志: AppName = IntelliTrace.exe
19 调用程序集: (Unknown)。
20 ===
21 日志: 此绑定从 default 加载上下文开始。
22 日志: 正在使用应用程序配置文件: D:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\TraceDebugger Tools\IntelliTrace.exe.Config
23 日志: 使用主机配置文件: 
24 日志: 使用 C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config 的计算机配置文件。
25 日志: 通过在 GAC 中查找找到了程序集。
26 日志: 绑定成功。从 C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Xml\v4.0_4.0.0.0__b77a5c561934e089\System.Xml.dll 返回程序集。
27 日志: 在 default 加载上下文中加载了程序集。

此日志中大部分内容都比较直观,需要特别注意的是:

  • 第12-13行:表示此次绑定使用的是Fully specified reference。
  • 第21-27行:表示此次绑定是从“default加载上下文”开始的,并成功从全局程序缓存中找到了程序集,而加载到了“default加载上下文”中。如果你还不是很清楚什么是“default加载上下文”也不要紧,这正是本文想要澄清的内容之一。而且这个名词与MSDN或其它资料中的名称也不一致,相关情况我们会在后面说明。

此外,如果在加载程序集时出现异常,也可以参考异常中的FusionLog属性,其内容与fuslogvw的内容类似。

然后,我们明确一些概念,以便在后面的讨论不至于产生混淆。现在回想起来,我在当初学习时,也正因为没有明确区分这些概念,而造成了不小的困惑。

* Strong-Named的程序集和Weak-Named的程序集:也许这两个单词并不标准,但我觉得在本文中,这样比“具有强名称的程序集”和“不具有强名称的程序集(或者说具有弱名称的程序集)”更简洁,也更能明确表达我的意思。一个程序集如果经过了密钥签名,则称之为Strong-Named,否则称之为Weak-Named。更精确的定义,请参考MSDN。

*程序集的Friendly Name和File Name(程序集对应的文件系统的文件名):一般情况下二者是相同的,但没有什么能阻止你把Utilities.dll改名为Util.dll,而这两者之间有什么关系呢?我总结得出以下几点:

  • 当试图调用 gacutil –i Util.dll 将改名后的程序集安装到GAC中时,会出现错误“将程序集添加到缓存失败: 文件或程序集名称无效。文件名必须是程序集名称后加上 .dll或 .exe 扩展名。”;
  • 当Load(“Utilities”)加载程序集时,如果GAC查找失败,运行时将在某些目录中搜索名称为Utilities.dll或Utilities.exe的文件,这时改名后的文件将不可能被搜索到;
  • 所以改名后的Util.dll只可能会在Assembly.LoadFrom(“path\to\Util.dll”)和Assembly.LoadFile(“path\to\Util.dll”)这样的调用中被加载,而加载时的具体行为也会依赖于程序的具体执行状态。详细内容我们会在后面讲述。

* Assembly’s identity:关于这个概念,各种资料中至少有几种不同的定义:

  • MSDN 1:http://msdn.microsoft.com/en-us/library/wd40t7ad(v=vs.100).aspx中有这样一段文字——“A strong name consists of the assembly's identity—its simple text name, version number, and culture information (if provided)—plus a public key and a digital signature”。按这种说法,Assembly’s identity包含Friendly name, Version number, Culture三部分;
  • MSDN 2:http://msdn.microsoft.com/en-us/library/system.reflection.assemblyname.aspx中有这样一段文字——“An assembly's identity consists of the following: Simple name. Version number. Cryptographic key pair. Supported culture”。按这种说法,Assembly’s identity包含Friendly name, Version number, Culture, Cryptographic key pair;
  • MSDN 3:http://msdn.microsoft.com/en-us/magazine/dd727509.aspx中有这样一段文字——“A fully qualified assembly identity consists of four fields: the simple name of the assembly, the version, the culture, and the public key token”。按这种说法,Assembly’s identity包含Friendly name,Version number, Culture, Public key token;
  • 《Customizing the Microsoft® .NET Framework Common Language Runtime》:中有这样一段文字——“An assembly's identity consists of four parts: Friendly name,Version,Public key, Culture”。按这种说法,Assembly’s identity包含Friendly name,Version number, Culture, Public key;
  • 在分析fuslogvw的输出时,有时会看到如下文字“ 建议为程序集提供完全指定的文字标识,并由简单名称、版本、区域性和公钥标记组成”。

你糊涂了吗?

可以看到,Friendly name, Version number和Culture作为Assembly’s identity的一部分是没有问题的,主要分歧在于和密钥相关的内容是否应该包含在Assembly’s identity中,如果应该包含,那么要包含什么内容。我个人比较倾向于MSDN 3的定义,即Public key token是Assembly’s identity的第四项,理由如下:

  • 既然是Identity,就应该能够唯一标识程序集的内容,而不同的组织完全可以放出Friendly name, Version number和Culture都相同的程序集,而(经过认证的)公钥则被用于标识不同的组织,所以MSDN 1不合理;
  • 当我们拿到一个程序集时,是不可能知道其相关的Private key的信息的,所以Private key不应该算做程序集Identity的一部分,所以MSDN 2不合理;
  • Public key token是Public key经过SHA1变换得来的,可以认为它们二者是等价的,而我们在引用程序集时大多数情况下都会使用前者。相比之下,将前者作为程序集Identity的一部分更简洁直观些。

在本文中提到Assembly’s Identity时将使用MSDN 3的定义。

* Fully specified reference和Partially specified reference:当我们调用Load(string)或Load(AssebmlyName)加载程序集时所指定的参数即为"reference”,以string类型的参数为例,形如 

ExampleAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a5d015c7d5a0b012
ExampleAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

这样指定了程序集的Friendly name, Version, Culture和PublicKeyToken的形式称之为Fully specifed reference。也许你对第二种情况也是Fullly specified reference感到奇怪,但规则就是这样——null也是值,它与没有指定值是不同的。如果Version, Culture, PublicKeyToken不完整,则为Partially specified reference。所谓的“不完整”包括以下两种情况: 

缺少某一项。比如:
ExampleAssembly, Version=1.0.0.0, PublicKeyToken=a5d015c7d5a0b012 
ExampleAssembly, Version=1.0.0.0

版本号不全。比如:
ExampleAssembly, Version=1.0, Culture=neutral, PublicKeyToken=a5d015c7d5a0b012 

应该特别注意其中“版本号不全”的情况!

针对这两种引用方式,CLR在加载程序集时的处理方式是不同的。对于Fully specified的情况,CLR会按以下顺序进行处理:

  1. 根据Version policy确定要加载的程序集版本;
  2. 在GAC中查找程序集;
  3. 在Codebase中查找程序集;
  4. 在ApplicationBase及其子目录中查找程序集。

而对于Partially specified的情况,CLR会按以下方式处理:

Load

其中尤其需要注意的是使用提取的Fully specified reference重新查找程序依集的步骤,后面讨论LoadFrom时也会有类似的过程。

* load, context, load context, load from, load from context ...: 是的,你没看错,就是这些概念。我想这也是动态加载程序集相关资料难以理解的原因之一,当遇到这几个词(或者相应的中文翻译)时,你要非常小心的断句,弄明白作者到底在说什么。归纳起来有如下几点(这些说法有些不太严谨,但有利于大家理解相关概念):

  • 运行时会将加载进来的程序集放到四个可能的地方之一,这里的“地方”即为context,你可以把它们想像成四个链表;
  • 这四个地方分别叫做:load context, load from context, reflection only context和no context。【其中no context是我杜撰的,其它的资料中把第四种情况加载的程序集说成“程序集不加载到任何上下文中”——这其实有两个层次的意思,一是程序集被加载了,二是程序集不在任何上下文中——如果看到这句话你已经迷糊了,请重新读此段文本,同时忽略蓝色的字】。这些名字是有些奇怪,但它们也仅仅是名字而已。 

前面提到的fuslogvw输出中的“default加载上下文”其实指的是load context。


 

终于到了介绍Load, LoadFrom, LoadFile三个函数的地方了。其实搞清楚了上面的概念,这一部分就很简单了。

Load(AssemblyName)、Load(string)

  • string和AssemblyName是程序集引用(fully specified reference或partially specified reference)的两种指定方式。
  • 使用这两个方法加载的程序集,会被放到load context中。
  • 使用这种方式加载的程序集的依赖项会被自动以相同的方式加载。
  • 当参数为fully specified reference或者partially specified reference时,具体步骤请参见前面关于两种引用的叙述。
  • 由应用程序静态引用的程序集,可以视为是通过Load方式加载的。

LoadFrom(string)

  • string是某个程序集文件的路径。
  • CLR在加载此文件后会提取Assembly’s Identity,并以此重新执行Fully specified reference查找,如果查找到了某个程序集A,而且A对应的文件刚好是string指定的文件,则保留A,并把A放到load context中;否则,保留之前加载的程序集,并把它放到load from context中。但是,
  • 在将程序集放到load from context中之前,CLR会判断load from context中是否已经存在与之Identity相同的程序集,如果没有就保留它;否则丢弃它并返回已经存在的那个程序集。所以如果你在c:\library.dll和c:\lib\library.dll中放了两个具有相同Identity的程序集,则:
    var a1 = Assembly.LoadFrom(@"c:\library.dll");
    var a2 = Assembly.LoadFrom(@"c:\lib\library.dll");

    这段代码中的a1与a2将引用同一个对象。

  • 使用这种方式加载的程序集的依赖项会被自动以相同的方式加载,而且这此被依赖的程序集可以将在string所表示的目录中(除去文件名)——这个目录可能在ApplicationBase之外。
  • 为什么要有load from context和load context的区别?考虑这种情况:应用程序c:\apps\a.exe引用了Weak-Named私有程序集library.dll,其Identity为library, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null,这个文件被放在a.exe所在的目录下;而a.exe又提供了以插件形式动态加载其它程序集的功能,用户通过在菜单中指定要加载的程序集路径来加载插件。假设有一个插件对应的程序集为c:\addins\library.dll,它的Identity与前者相同。但它们确实是完全不相干的程序集。那么CLR为了保证不论先加载哪一个a.exe都能正常运行,就采用了不同context的机制。更权威的说明,请参考《Customizing the Microsoft® .NET Framework Common Language Runtime》。

LoadFile(string)

  • Load和LoadFrom的行为那么复杂,而且加载的不一定就是我指定的程序集,如果我真的确定以及肯定就想加载某个程序集文件怎么办呢?这就是为什么会有LoadFile的原因了。其实,在.NET Framework 1.0中并没有LoadFile,因为有了前面提到的原因,才在.NET Framework 1.1中加入了LoadFile。
  • 使用此方法加载的程序集的依赖项不会被自动加载,可以通过AppDomain.AssemblyResolve事件来处理相关程序集的加载。
  • LoadFile把程序集加载到no context中,而且允许多个Identity相同但路径不同的程序集同时存在。

好了,就这些了。欢迎大家指正!

posted @ 2013-05-22 12:22  Bruce Bi  阅读(4362)  评论(6编辑  收藏