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

Why does Assembly.Load seem to not affect the current thread when resolving references (not through reflection)?

如果标题没有意义,我提前道歉。我对 appdomains 和程序集加载还很陌生,真的不知道如何表达我想问的问题。

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

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

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
}

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

我在此处找到了 dthorpe 关于 JIT 编译器如何工作的精彩解释:C# JIT compiling and .NET

这里引用 dthorpe:

Yes, JIT'ing IL code involves translating the IL into native machine instructions.

Yes, the .NET runtime interacts with the JIT'ed native machine code, in the sense that the runtime owns the memory blocks occupied by the native machine code, the runtime calls into the native machine code, etc.

You are correct that the .NET runtime does not interpret the IL code in your assemblies.

What happens is when execution reaches a function or code block (like, an else clause of an if block) that has not yet been JIT compiled into native machine code, the JIT'r is invoked to compile that block of IL into native machine code. When that's done, program execution enters the freshly emitted machine code to execute it's program logic. If while executing that native machine code execution reaches a function call to a function that has not yet been compiled to machine code, the JIT'r is invoked to compile that function "just in time". And so on.

The JIT'r doesn't necessarily compile all the logic of a function body into machine code at once. If the function has if statements, the statement blocks of the if or else clauses may not be JIT compiled until execution actually passes through that block. Code paths that have not executed remain in IL form until they do execute.

The compiled native machine code is kept in memory so that it can be used again the next time that section of code executes. The second time you call a function it will run faster than the first time you call it because no JIT step is necessary the second time around.

In desktop .NET, the native machine code is kept in memory for the lifetime of the appdomain. In .NET CF, the native machine code may be thrown away if the application is running low on memory. It will be JIT compiled again from the original IL code the next time execution passes through that code.

根据那个问题的信息,以及来自 Hans Passant 的信息,很清楚发生了什么:

  1. JIT 编译器尝试转换整个入口点代码 块(在本例中是我的 Main() 函数)到本机代码中。这个 要求它解析所有引用。
  2. 嵌入式程序集 LoaderLibrary.dll 尚未加载到 AppDomain,因为执行此操作的代码定义在 Main() 函数(并且不能执行未编译的代码)
  3. JIT 编译器尝试解析对 LoaderLibrary.dll 通过搜索 AppDomain、Global Assembly Cache, App.config/Web.config,并探测(环境路径,当前 工作目录等)更多信息可以在 MSDN 中找到 此处文章:How the Runtime Locates Assemblies
  4. JIT 编译器无法解析对 LoaderLibrary.LoaderLibrary.Test(); 并导致错误 Could not load file or assembly or one of its dependencies

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

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