在上一篇我们提到加载程序集有两种方式,第一种根据程序集文件路径加载,这种比较简单,给了路径加载就是了。第二种就是给定程序集的名称(Name)加载程序集,这一种就必须通过Assembly Resolver先解析这个名称,定位到具体的程序集,然后再通过Assembly Loader加载程序集。
上面这张图取自《.NET本质论》这本书,上面的图说明了,如果通过程序集名称(Name)加载程序集,则会先通过Assembly Resolver定位程序集,而这个定位过程中就会应用到版本策略,找到正确的版本后,还会通过在CodeBase、AppBase、Private_BinPath等路径搜索正确的程序集。
为此我们来用代码来说明问题。
首先,我建立了一个空白解决方案,添加了两个工程,一个控制台工程,一个类库工程,控制台工程引用类库的工程。
代码很简单,控制台程序:
Program.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Lib; namespace MainApp { class Program { static void Main(string[] args) { Output output = new Output(); output.Write("from MainApp.Program"); Console.ReadLine(); } } }
类库程序:
Output.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Lib { public class Output { public void Write(string message) { Console.WriteLine(message); } } }
编译,通过,运行:
我们修改一下类库这个工程的程序集版本号,展开类库工程的“属性”文件夹,里面就有一个AssemblyInfo.cs文件,打开后,会看到这样的一行:
[assembly: AssemblyVersion("1.0.0.0")]
将版本号修改为1.1.0.0,然后重新编译类库工程(注意,仅仅编译类库这个工程),然后将类库工程Bin目录的Lib.dll拷贝到控制台工程的Bin目录下,覆盖原来的Lib.dll,再次运行成功了。
我们使用ILDasm打开控制台工程的程序MainApp.exe,在程序集清单里,我们发现MainApp.exe引用的是Lib的1.0.0.0版本
.assembly extern Lib { .ver 1:0:0:0 }
这是为什么呢,明明编译的时候控制台工程引用的是类库的1.0.0.0版本,为什么现在1.1.0.0版本也可以运行呢?留着这个疑问我们进一步探索。
为程序集签名,双击“属性”文件夹(不是展开),跳到“签名”那一项,为我们的Lib指定一个签名:
然后重新编译整个解决方案,再用ILDasm打开MainApp.exe,看看程序集清单中对Lib的引用:
.assembly extern Lib { .publickeytoken = (34 8A 2F E5 37 81 24 0B ) // 4./.7.$. .ver 1:0:0:0 }
和原来唯一的不同就是多了一个.publickeytoken,那这个时候MainApp.exe加载Lib.dll的方式又有什么不同呢?
这次我们按照刚才的方法,将Lib的版本号再次修改,然后单独编译Lib工程,将生成拷贝到MainApp的bin目录中,再次运行MainApp.exe,啊,情况不妙:
点击调试,打开VS,我们还可以看到异常:
由此我们是否可以得出这样的结论:
当只指定程序集的名称和版本号的时候,Assembly Resolver对版本号视而不见,而指定了版本号,名称、公钥标记后,Assembly Resolver却要考虑版本号了。
实际上确实如此,如果我们只指定了程序集名称和版本号,那相当于我们没有指定程序集的完全限定名,实际上效果就相当于上一篇介绍的Assembly.LoadFrom,当我们制定了完全限定名后就会应用版本策略了。
那现在既然“错误”已经造成了,是不是有什么办法不重新编译,然后MainApp.exe加载1.1.0.0版本的Lib呢?有:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="Lib" publicKeyToken="348a2fe53781240b"/> <bindingRedirect oldVersion="1.0.0.0" newVersion="1.1.0.0" /> </dependentAssembly> </assemblyBinding> </runtime> </configuration>
assemblyIdentity元素就是用来指定要在哪个程序集上施加这个版本映射的,bindingRedirect,意思很明显就不解释了。只要将这样的一个配置文件往那里一放,呵呵,程序又正常运行了。
不过对于这个版本策略却有三种级别可以设置:应用程序级别、发布者、机器级别
而且是一层一层的应用,先应用了应用程序级别的,然后发布者,然后机器级别的。
比如在应用程序的配置文件中,将Lib的版本从1.0.0.0映射到了1.1.0.0,然后在机器级别,将1.1.0.0映射到1.2.0.0,那么最终就会加载1.2.0.0版本。
对于发布者的策略,这个应用起来还有些小麻烦,发布者策略就是开发程序集的开发人员指定的,在这里就是Lib的开发者指定的版本策略。它也是放在配置文件中,然后放在全局程序集缓存(GAC)中。而我们要将这个策略应用上去还得用这样的命令:
al.exe /link:mylib.config /out:policy.1.0.Lib.dll /keyf:publicprivate.snk /v:1.0.0.0
al.exe是程序集连接器
发布者策略遵循这种格式:policy.主版本号.次版本号.程序集名.dll。因为这样的名字,那相同主版本号.次版本号的策略是唯一的,所有加载主版本号.次版本号为1.0的请求都会转移到这个文件中,这个文件存储在GAC中,如果在GAC中没有发现这样的文件,则不会应用发布者策略。上面说到,发布者策略在应用程序级策略之后,在机器级策略之前,但是我们可以通过在应用程序的配置文件中决定是否应用发布者策略:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="Lib" publicKeyToken="348a2fe53781240b"/> <publisherPolicy apply="no" /> </dependentAssembly> </assemblyBinding> </runtime> </configuration>
就是这个publisherPolicy元素的作用,在这里仅仅指定对于Lib程序集不应用发布者策略,我们还可以使用下面这样的配置:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <publisherPolicy apply="no" /> </assemblyBinding> </runtime> </configuration>
这样,整个应用程序中都不应用发布者策略了。
通过上面的讨论,我们知道了Assembly Resolver如何决定加载哪个版本的程序集,但是光有版本不够啊,还得决定加载哪个文件,然后传递给底层的Assembly Loader,让它加载啊。
首先,CLR会到操作系统的DEVPATH环境变量里指定的路径找,这个环境变量一般在开发机器上设置,为了那些延迟签名的程序集提供一个全局共享的位置,不建议在部署的机器上设置,要让这个环境变量有效还得在配置文件里做如下设置:
<configuration> <runtime> <developmentMode developerInstallation="true"/> </runtime> </configuration>
为true的时候表示要在这个环境变量中找,为false的时候则忽略。
如果上面这步过去了,还没找到要求的程序集,那么就要在GAC里找找了,如果在GAC里找到了,则使用这个。
如果没有找到,就要看看配置文件里是否提供了<codeBase>:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="Lib" publicKeyToken="348a2fe53781240b"/> <codeBase version="1.0.0.0" href="file://c:/lib.dll" /> <codeBase version="1.1.0.0" href="http://www.yourdomain.com/lib.dll" /> </dependentAssembly> </assemblyBinding> </runtime> </configuration>
在codeBase元素里,根据版本号,映射到具体的文件,该文件可以在本地硬盘上,也可以在远程web服务器上。如果提供了codeBase那就看看是不是我们想要的那个程序集,如果是那就传递给底层的Assembly Loader加载之。如果不是那就加载失败了。如果没有提供codeBase。那Assembly Resolver就要搜索一系列的目录,这个过程就称之为probing(探测)。probing只会搜索应用程序的根目录,以及其子目录,比如应用程序放在c:\app下面,那么probing就只会探测app以及app下面的目录,而不会探测到c:\other这个目录里面去。那如果c:\app的子目录极其复杂,那搜索起来岂不是非常费劲,如果真的是这样,我们可以通过配置文件指定探测的目录,而将其余的排除:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <probing privatePath="common;shared" /> </assemblyBinding> </runtime> </configuration>
这样就只会在common和shared这两个子目录里探测了,其余的目录一律排除在外。
探测的过程又是怎样的呢?CLR会使用程序集的简单的名称,然后加上dll或exe不断的尝试:
对于语言文化中性的程序集:
file://c:/app/shared/lib/lib.dll
file://c:/app/shared/lib/lib.exe
如果是语言文化相关的,则搜索的时候还会加上语言字符串,比如:
file://c:/app/en-US/lib.dll 等等
写了这么多,我们来总结一下:
第一步:加载程序集时是否提供了public key token,如果未提供则直接使用probing(探测)
第二步:如果提供了public key token,则应用版本策略,版本策略有三种级别:应用程序级,发布者,机器级
第三步:应用版本策略后就在GAC里找
第四步:如果未找到则查看是否提供了<codeBase>,如果提供了,则查看引用是否匹配,如果不匹配则加载失败。
第五步:如果为提供<codeBase>则开始探测了。如果探测到了,则检查引用是否匹配。
在上面过程中,如果程序集成功加载后,第二次再需要加载的时候则直接使用前面加载的就行了,如果程序集加载失败,第二次再次这样加载的时候,则直接失败(这个特性是.NET 2.0新增的)。