为什么 Assembly.Load 在解析引用(不是通过反射)时似乎不会影响当前线程

本文关键字:线程 前线 影响 反射 Load Assembly 引用 为什么 | 更新日期: 2023-09-27 18:35:15

如果标题没有意义,我提前道歉。我对应用程序域和程序集加载非常陌生,真的不知道如何陈述我想问的问题。

我一直在摆弄在运行时将嵌入式 DLL 加载到应用程序中,但我似乎无法弄清楚为什么它以一种方式工作而不是另一种方式。似乎如果您尝试将 DLL(从字节数组)加载到当前 appdomain 中,之后创建的任何对象/线程都将能够针对新加载的库解析引用,但是原始上下文中的对象将不会针对新加载的库解析。

下面是我的示例库,它将在运行时作为嵌入式资源加载(需要引用 WPF PresentationFramework.dll for MessageBox):

namespace LoaderLibrary
{
    public class LoaderLibrary
    {
        public static void Test()
        {
            System.Windows.MessageBox.Show("success");
        }
    }
}

在我的控制台应用程序 .csproj 文件中,我手动为该项目添加了以下嵌入式资源,并包括对 LoaderLibrary 的项目引用

  <ItemGroup>
    <EmbeddedResource Include="..'LoaderLibrary'bin'$(Configuration)'LoaderLibrary.dll">
      <LogicalName>EmbeddedResource.LoaderLibrary.dll</LogicalName>
    </EmbeddedResource>
  </ItemGroup>

以下是我的控制台应用程序的代码,用于加载该库(需要对 LoaderLibrary csproj 的项目引用)另外:需要将 CopyLocal 设置为 false 才能引用 LoaderLibrary 参考:

namespace AssemblyLoaderTest
{
    class Program
    {
        static void Main(string[] args)
        {
            EmbeddedAssembly.Load("EmbeddedResource.LoaderLibrary.dll");
            System.AppDomain.CurrentDomain.AssemblyResolve += (s, a) => { return EmbeddedAssembly.Get(a.Name); };
            var app = new TestApp();
        }
    }
    public class TestApp
    {
        public TestApp()
        {
            LoaderLibrary.LoaderLibrary.Test();            
        }
    }
    public class EmbeddedAssembly
    {
        static System.Collections.Generic.Dictionary<string, System.Reflection.Assembly> assemblies = new System.Collections.Generic.Dictionary<string, System.Reflection.Assembly>();
        public static void Load(string embeddedResource)
        {
            using (System.IO.Stream stm = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(embeddedResource))
            using (var mstream = new System.IO.MemoryStream())
            {
                stm.CopyTo(mstream);
                var assembly = System.Reflection.Assembly.Load(mstream.ToArray());
                assemblies.Add(assembly.FullName, assembly);
                return;
            }
        }
        public static System.Reflection.Assembly Get(string assemblyFullName)
        {
            return (assemblies.Count == 0 || !assemblies.ContainsKey(assemblyFullName)) ? null : assemblies[assemblyFullName];
        }
    }
}

这段代码能够成功加载和执行 LoaderLibrary.LoaderLibrary.Test() 函数。

我的问题是为什么以下内容不起作用?

static void Main(string[] args)
{
    EmbeddedAssembly.Load("EmbeddedResource.LoaderLibrary.dll");
    System.AppDomain.CurrentDomain.AssemblyResolve += (s, a) => { return EmbeddedAssembly.Get(a.Name); };
    LoaderLibrary.LoaderLibrary.Test(); // very unhappy line of code
}

这也不起作用:

static void Main(string[] args)
{
    EmbeddedAssembly.Load("EmbeddedResource.LoaderLibrary.dll");
    System.AppDomain.CurrentDomain.AssemblyResolve += (s, a) => { return EmbeddedAssembly.Get(a.Name); };
    var app = new TestApp();
    LoaderLibrary.LoaderLibrary.Test(); // very unhappy line of code
}

为什么 Assembly.Load 在解析引用(不是通过反射)时似乎不会影响当前线程

非常感谢Hans Passant和dthorpe解释正在发生的事情。

我在这里找到了dthorpe关于JIT编译器如何工作的很好的解释:C# JIT编译和.NET

在这里引用dthorpe的话:

是的,JIT 的 IL 代码涉及将 IL 转换为本机计算机 指示。

是的,.NET 运行时与 JIT 的本机机器代码交互, 从某种意义上说,运行时拥有 本机机器代码,运行时调用本机机器代码, 等。

您是正确的,.NET 运行时不解释 IL 代码 在您的程序集中。

发生的情况是,当执行到达函数或代码块(例如, 尚未 JIT 编译为 if 块的 else 子句 本机机器代码,调用 JIT'r 来编译该 IL 块 转换为本机机器代码。完成后,程序执行进入 新发出的机器代码来执行其程序逻辑。如果 在执行本机机器代码执行时到达函数 调用尚未编译为机器代码的函数, 调用 JIT'r 是为了"及时"编译该函数。等等。

JIT'r 不一定编译函数体的所有逻辑 一次变成机器代码。如果函数具有 if 语句,则 if 或 else 子句的语句块不能进行 JIT 编译 直到执行实际通过该块。代码路径 在执行之前,未执行将保留为 IL 形式。

编译的本机机器代码保存在内存中,以便可以 下次执行该段代码时再次使用。第二个 当你调用一个函数时,它会比你第一次运行得更快 调用它是因为第二次不需要 JIT 步骤。

在桌面 .NET 中,本机机器代码保存在内存中,以便 应用域的生存期。在 .NET CF 中,本机机器代码可能是 如果应用程序内存不足,则丢弃。这将是 下次执行时从原始 IL 代码再次编译 JIT 通过该代码传递。

根据这个问题的信息,以及汉斯·帕桑特的信息,非常清楚发生了什么:

  1. JIT 编译器尝试转换整个入口点代码块(在本例中为我的Main()函数)放入本机代码中。这要求它解析所有引用。
  2. 嵌入式程序集 LoaderLibrary.dll 尚未加载到AppDomain 尚未,因为执行此操作的代码是在 Main()函数(它不能执行尚未编译的代码)。
  3. JIT 编译器尝试将引用解析为加载器库.dll通过搜索 AppDomain、全局程序集缓存、App.config/Web.config 和探测(环境路径,当前工作目录等)有关此内容的详细信息,请参阅 MSDN此处的文章:运行时如何定位程序集
  4. JIT 编译器无法解析对 LoaderLibrary.LoaderLibrary.Test();并导致错误 Could not load file or assembly or one of its dependencies

按照 Hans Passant 的建议,解决此问题的方法是将程序集加载到代码块中,该代码块比引用这些程序集的任何代码块更早进行 JIT 编译。

通过将 [MethodImpl(MethodImplOptions.NoInlineing)] 添加到引用动态加载程序集的方法中,它将防止优化器尝试内联方法代码。