简述C#中应用程序集的装载过程

了解程序集如何在C#.NET中加载

咱们一直在处理库和NuGet软件包。不论是好是坏,高级.NET开发人员都须要了解.NET运行时如何加载程序集。git

这些库依赖于其余流行的库,而且有不少共享的依赖项。有了足够大的依赖关系网络,您最终将陷入冲突或困境。处理此类问题的最佳方法是了解该机制在内部的工做方式。github

在本文中,您将看到.NET进程如何以及什么时候加载引用的程序集。web

您将了解加载了哪一个库版本,当有多个可用版本时会发生什么,以及为何有时因为版本冲突而出现问题。算法

您将看到如何调试这些类型的问题,查看程序集绑定日志(融合日志)以及一些解决冲突的方法。c#

程序集,模块和引用

让咱们从围绕.NET流程的一些基本术语开始。缓存

一个装配在.NET是一个DLL或EXE文件。Visual Studio解决方案中的每一个项目都被编译为一个程序集。安全

每一个程序集能够包含多个模块,可是实际上,咱们几乎老是在一个程序集中有一个模块,该模块的名称与该程序集相同。网络

在Visual Studio中启动进程或单击F5时,将执行启动项目程序集。除了.NET Framework或.NET Core程序集以外,它将是第一个加载的程序集。app

以后,该过程将根据须要在运行时加载其余程序集。仅当须要调用该程序集的方法或使用该程序集的类型时,它才会延迟加载程序集。dom

这里是为一个简单的“ Hello World” .NET Framework项目加载的模块(出于咱们全部的意图和目的,模块和程序集都是相同的)。MyStartup.dll是此处的启动项目:

.NET Core项目启动时加载的模块

当您从另外一个项目引用一个项目时,在构建时,被引用项目的DLL或EXE被复制到启动项目的Bin文件夹中。

一般是Bin \ DebugBin \ Release。在运行时,当您第一次使用引用的项目中的类型时,CLR在应用程序目录中查找具备与指望的名称和版本相同的DLL文件。而后将程序集加载到流程中。这也称为绑定到装配件。

这是一个例子:

假设咱们有一个名为MyStartup的简单控制台应用程序,它引用了另外一个名为Lib1的项目。MyStartup使用Lib1程序集中的某些类。

MyStartup中

class Program
{
    static void Main(string[] args)
    {
        int a = int.Parse(Console.ReadLine());
        int b = int.Parse(Console.ReadLine());
        Console.WriteLine("A + B = " + Add(a, b));
    }


    private static int Add(int a, int b)
    {
        var calculator = new Lib1.Calculator();
        return calculator.Sum(a, b);
    }
}

Lib1中

public class Calculator
{
    public int Sum(int a, int b)
    {
        return a + b;
    }
}

输入Main方法时,还没有加载Lib1程序集。可是,在输入Add方法时,CLR尝试解析Calculator类型,找出它在引用的程序集Lib1中,而后尝试加载该程序集。

.NET中的程序集绑定

当CLR须要加载程序集时,逻辑实际上比在Bin文件夹中查找要复杂一些。这是执行的实际逻辑(有关详细说明,请参见Microsoft文档[1]):

1.根据配置文件(app.configweb.config)肯定须要加载的程序集的版本。该配置文件的名称为(在生成以后) [executable name].exe.configweb.config。绑定重定向在这里发挥了做用(稍后会详细介绍)。2.查看程序集是否已加载。若是加载了其余版本,则将抛出FileLoadException,除非它是一个能够同时加载多个版本的强命名程序集。3.若是它是强名称程序集,请检查全局程序集缓存[2](GAC)。GAC是机器上共享多个应用程序部件的地方。若是须要的话,程序集会缓存。它只能存储强命名程序集。它能够存储同一程序集的不一样版本。您可使用gacutil.exe[3]本身将其安装到GAC 。4.若是它是一个强名称的程序集,而且配置文件包含<codeBase>节点,那么它将检查那里的程序集位置。若是该<codeBase>节点存在而且找不到程序集,FileNotFoundException则将引起a。5.根据启发式算法检查程序集DLL或EXE。此过程称为“探测”。算法以下:1.检查文件夹[application base] / [assembly name].dll。应用程序库是应用程序可执行文件所在的位置。一般,您的Bin \ Debug或Bin \ Release文件夹。2.检查一下 [application base] / [assembly name] / [assembly name].dll3.若是为引用的程序集指定了区域性信息,则仅检查如下目录: [application base] / [culture] / [assembly name].dll [application base] / [culture] / [assembly name] / [assembly name].dll4.若是该<probing>节点存在于配置文件中,则它将在该privatePath节点的属性指定的文件夹中查找程序集。

他们为何要使全部事情变得如此困难,对吗?

实际上,这种逻辑很是有助于咱们发展,而不会使事情变得困难。它的存在是为了实现一些重要目标:

•为了确保您引用的是特定的程序集和版本,则将加载该确切版本。不然,将引起异常。并且,若是您知道本身在作什么,则能够在配置文件中指定覆盖规则(绑定重定向)。•为了灵活地在您要加载的程序集中进行。例如,若是要根据不一样的区域性(语言)加载不一样的程序集,则能够轻松地作到这一点。或者,若是您要根据客户配置加载不一样的程序集,那也能够。•为了安全起见,咱们使用了全称的程序集。他们确保您不能“伪造”程序集。例如,若是某个进程但愿加载Lib1 v4.5,那么您将没法加载具备相同名称和版本的恶意软件程序集。加载时会引起异常。这就是为何在计算机上全部进程都共享的GAC只接受强名称程序集的缘由。

在大多数应用程序中,您无需记住程序集加载和探测的复杂逻辑。您无需了解或考虑GAC,全名程序集或操做配置文件。

您几乎根本不须要考虑库的版本,由于可能的冲突经过称为“绑定重定向”的机制自动解决了。

绑定重定向

若是有一件事对于了解这笔交易很是重要,那就是绑定重定向。可以告诉运行时它将实际加载哪一个版本,而无论其引用的版本如何。

这是一个示例:您的流程有两个项目(模块):项目A和项目B。项目A引用log4net.dll v1.1,项目B引用log4net.dll v1.2。两个log4net DLL文件都复制到输出文件夹,可是只能有一个log4net.dll文件。

假设复制到输出文件夹的文件是log4net.dll v1.2。假设到达的第一个代码是Project A中的代码,该代码引用了log4net v1.1。运行时将在输出文件夹中查找,找到不一样版本的log4net,并失败FileLoadException

还有另外一种可能。假设首先执行了项目B中的代码,而且在尝试使用log4net时,它成功加载了log4net.dll v1.2。片刻以后,Project A中的代码将尝试使用log4net v1.1,请参见该程序集已经加载了其余版本,并抛出FileLoadException

若是您知道哪一个log4net版本将在输出文件夹中,在这种状况下能够作的就是告诉运行时应该使用哪一个版本。只需app.config在该runtime部分的文件中添加如下几行:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  ...
  <runtime>
    ...
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="log4net" 
                          publicKeyToken="669e0ddf0bb1aa2a" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="1.2.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
  ...
</configuration>

这意味着,只要运行时想绑定到版本范围为0.0.0.0to的程序集log4net 5.0.0.0,它就会尝试绑定到version 1.2.0

实际上,您没必要手动添加这些重定向,由于它们是自动添加的。若是转到启动项目的“属性”,则会看到如下设置:

图片-20200711101325128

默认状况下选中此选项。它会自动检测版本冲突并在.config文件中生成绑定重定向。

当问题开始发生时

乍一看,绑定重定向可能看起来像是对全部问题的答案,但事实并不是如此。使用绑定重定向时,基本上使用的库版本与预期不一样。若是删除方法怎么办?或方法的签名已更改?在这种状况下,调用该方法时,程序将因运行时错误而失败。毕竟,建立版本是有缘由的。

若是确实存在此类问题,则有解决方法。查看个人文章:如何解决.NET引用和NuGet软件包版本冲突[4]

故障排除

当您有一个FileLoadException或相似的东西时,我建议作的第一件事是查看Visual Studio中的“模块”窗口。在这里,您将看到全部已加载的模块,并肯定您要加载的程序集是否已加载,使用哪一个版本以及从哪一个路径加载。

除此以外,您还能够查看程序集绑定日志,也称为融合日志。这些日志将显示在程序集绑定尝试过程当中到底发生了什么。您将看到运行时查找的程序集版本,运行时查找的文件夹以及故障点。

有几种查看融合日志的方法。首先,您必须启用它们,由于默认状况下它们是禁用的。您能够经过将HKLM\Software\Microsoft\Fusion\ForceLog值设置为1并将HKLM\Software\Microsoft\Fusion\LogPath值设置为来在注册表中手动启用它们C:\FusionLogs。日志将自动出现。或者,您可使用Fusion Log Viewer,该软件应以方式安装在PC上fuslogvw.exe。我建议使用“一切窗口”搜索之[5]类的程序来查找它。确保以管理员权限运行融合日志查看器,以便可以启用和禁用日志。最近更流行的一种更现代的工具是Fusion ++[6]

边注

也许您不须要,可是我之前讨厌不得不处理这类问题。例如一个逻辑上的问题,让我构建一些东西,甚至解决一个生产错误,但其余问题都好说,惟独这个……。

在这件事上别无选择,我不得不艰难地学习程序集绑定的内部工做。我发现,就像其余全部内容同样,一旦您理解了某些内容,它就会变得不那么可怕,甚至变得再也不那么有趣了。

所以,我但愿本文对您有意义,并会在我走过的道路上为您提供快速帮助。

References

[1] Microsoft文档: https://docs.microsoft.com/en-us/dotnet/framework/deployment/how-the-runtime-locates-assemblies
[2] 全局程序集缓存: https://docs.microsoft.com/en-us/dotnet/framework/app-domains/gac
[3] gacutil.exe: https://docs.microsoft.com/en-us/dotnet/framework/tools/gacutil-exe-gac-tool
[4] 如何解决.NET引用和NuGet软件包版本冲突: https://michaelscodingspot.com/how-to-resolve-net-reference-and-nuget-package-version-conflicts/
[5] 一切窗口”搜索之: https://www.voidtools.com/
[6] Fusion ++: https://github.com/awaescher/Fusion/

相关文章
相关标签/搜索