Assembly Binding and Fusion
在.NET的开发中,偶尔会遇到assembly找不到的问题,相信.NET程序员对下面的错误信息都不陌生。大多数时候,我们去bin目录检查,把缺失的dll文件拷贝到bin目录,问题就解决了。但也有例外的时候,因此搞清楚.NET CLR是如何寻找assembly就很有必要了。
如果你仔细阅读错误页面的信息,你会发现其实.NET已经给我们指明了解决问题的办法。
这里面有两个重要的概念,一个是Assembly Binding
,另一个是Fusion
。
Assembly Binding
Binding 这个词翻译成中文“绑定”实在是巧妙的很,读音和含义都很贴合。
Assembly Binding,即程序集绑定,指的是.NET CLR在加载应用程序时试图寻找和解析程序集引用(assembly reference
)所执行的一系列动作。寻找程序集的这个动作,也有一个术语来描述,即Probing
。如果程序集绑定失败,CLR就会抛出FileNotFoundException。
Initiating the Bind
首先,程序集绑定起始于CLR遇到一个程序集引用。这个引用分为两种:
- 静态引用(static reference)
静态引用是在编译时写入到程序集manifest中的记录,可以使用ILDASM来查看。我们以Newtonsoft.Json.dll这个程序集为例:
所有标记为extern的assembly都是当前assembly所引用的程序集。当CLR试图加载Newtonsoft.Json.dll时,它不会立即加载这些引用的程序集,而是会在遇到相关的代码调用时才会加载,这时候才会发起一个Assembly Binding Request。 - 动态引用(dynamic reference)
动态引用是使用反射机制,如Assembly.Load这样的方法来动态的加载程序集。
在静态引用中,编译器会将引用的程序集的全名,即name、version、culture以及public key token写入到程序集的manifest中。如果目标程序集不是强命名的,public key token则会省略掉。在动态引用中,你可以只指定程序集的name, 但这会影响后续的寻找过程。
在CLR开始寻找目标assembly之前,以下的配置文件还会影响绑定请求:
- Application configuration file,应用程序配置文件
- Publisher policy file,发布者策略文件
- Machine configuration file,计算机配置文件
这些文件遵循相同的语法,并提供绑定重定向、代码位置和特定程序集的绑定模式等信息。在这些配置文件中,bindingRedirect
配置节会将程序集版本重定向到另一个版本。
Binding Redirection
在一个大型的项目中,多个不同的外部组件很有可能依赖同一个核心组件,比如Newtonsoft.Json.dll,但是它们依赖的版本却不一样。在项目编译后,同一个assembly在bin目录只能保留一份,这会导致运行时错误。bindingRedirect
能很好的解决这个问题。当然这样设置的前提是新旧版本的接口是没有变化的,否则一样会发生运行时错误。
1 | <configuration> |
Probing
Probing
是探测
、追根究底
的意思。在CLR开始Probe指定的assembly之前,它还会做一些检查:
- Checking for Previously Referenced Assemblies,检查以前引用的程序集
检查目标assembly是否已经加载,如果没有则继续下一步 - Checking the Global Assembly Cache,检查全局程序集缓存
如果目标assembly是强命名的,那么去GAC目录中去查找,如果没有找到则继续下一步 - Locating the Assembly through Codebases,通过基本代码定位程序集
配置文件定义了<codeBase>配置节,如果能找到相匹配的版本,则从中指定的位置下载assembly
我们常说assembly是加载到内存,具体到.NET Application而言,assembly是加载到
Application Domain
。Application Domain是.NET应用程序进程中的一种隔离机制。
经过了上述三步,如果还没有找到目标assembly,CLR将会开始探测的过程。
CLR使用以下4个条件来探测程序集:
- Application base,应用程序根目录,即正在执行应用程序的位置
- Culture,区域语言,即被引用的程序集的区域语言属性
- Name,名称,即被引用的程序集的名称
- The
privatePath
attribute of the <probing> element,配置节中定义的 privatePath 属性,这是根目录下用户定义的子目录列表
探测应用程序根目录和区域性目录
CLR将首先探测应用程序根目录。如果应用程序根目录中不存在引用的程序集且未提供区域性信息,则CLR将搜索具有程序集名称的所有子目录。 探测的目录包括:
[application base] / [assembly name].dll
[application base] / [assembly name] / [assembly name].dll
如果指定了被引用程序集的区域性信息,则只探测以下目录:
[application base] / [culture] / [assembly name].dll
[application base] / [culture] / [assembly name] / [assembly name].dll
使用privatePath属性进行探测
除了上述的目录之外,如果<probing>配置节中定义了privatePath
属性,则CLR还会探测 privatePath 属性指定的目录列表。使用privatePath
属性指定的目录必须是应用程序根目录的子目录。根据程序集绑定请求中是否包含区域性信息,探测的目录会所有不同。
如果不包括区域性信息,则探测以下目录:
[application base] / [binpath] / [assembly name].dll
[application base] / [binpath] / [assembly name] / [assembly name].dll
如果包括区域性,则探测以下目录:
[application base] / [binpath] / [culture] / [assembly name].dll
[application base] / [binpath] / [culture] / [assembly name] / [assembly name].dll
binpath
是在privatePath中指定的目录。
PrettyBin
常规的.NET Application在引用了很多外部的assembly之后,编译后的目录就会显得非常的混乱。但你又不能把那些依赖的dll文件放到其他的位置。PrettyBin 就是一个利用privatePath
的这个特性来使bin目录干净的一个NuGet Package。
使用非常简单:Install-Package PrettyBin
在安装过程中,它会在 Application configuration file 中加入以下配置信息, 并且将所有的dll文件移动到libs目录下。
1 | <runtime> |
Fusion
在上面提到的错误信息中,我注意到了一个注册表的路径:HKLM\Software\Microsoft\Fusion
。让我比较疑惑的是Fusion
是个什么东西?
经过一番搜索,我了解到Fusion
原来是CLR中实现Probing
这部分逻辑的代码代号(Codename)。
微软在2006年公开了.NET Framework 2.0大部分的CLR和BCL代码,在这个代码库里我们可以找到这部分的代码。现在微软公布的代码库地址已经不可用了,但我在GitHub上找到了一份,并且Fork了一份。
在这份代码库中,我们还可以看到很多熟悉的CLR组件和相关的工具,例如jit
、ilasm
、ildasm
等等。
Fusion Log Viewer
知道了Fusion的由来,Fusion Log Viewer
的用途也就很明确了。
默认情况下,Assembly binding logging 是关闭的,启用的方式有两种:
- 在注册表中添加以下配置信息:
reg add "HKLM\Software\Microsoft\Fusion" /v EnableLog /t REG_DWORD /d 1 /f
- 打开Developer Command Prompt,输入
fuslogvw
,在Settings
中选择Log in exception text
:在打开Developer Command Prompt时,需要右键选择
Run as Administrator
启用Assembly binding logging后,再次打开页面就可以看到程序集绑定的log了。从log中我们可以看到CLR从多个位置尝试加载Newtonsoft.Json.dll。
此外,我们还可以将log信息保存到磁盘上,方便以后分析问题。
对于ASP.NET程序而言,需要重启IIS或者Recycle AppPool才能使上述修改生效。
Process Explorer
有了Assembly binding logging和Fusion Log Viewer的帮助,相信你可以很快的解决文章开头出现的问题。当ASP.NET程序正常运行后,我们还可以通过Process Explorer这个工具来验证指定的Assembly的确加载到AppDomain了。
总结
本文从.NET开发过程的一个运行时错误出发,详细阐释了Assembly Binding的机制以及Fusion Log Viewer这个工具,希望对你今后的.NET开发有帮助。