【问题标题】:Why does Assembly.Load seem to not affect the current thread when resolving references (not through reflection)?为什么在解析引用(不是通过反射)时,Assembly.Load 似乎不会影响当前线程?
【发布时间】:2016-02-18 11:56:54
【问题描述】:

如果标题没有意义,我提前道歉。我对 appdomains 和程序集加载非常陌生,并且真的不知道如何陈述我要问的内容。

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

这是我的示例库,它将在运行时作为嵌入式资源加载(需要对 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 的项目引用ALSO:需要设置 CopyLocal对于 LoaderLibrary 参考,strong> 到 false

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
}

【问题讨论】:

  • 程序集由即时编译器加载。它需要将您的 Main() 方法转换为机器代码,然后才能开始运行。这当然发生在它运行之前,因此 AssemblyResolve 还不能做任何事情来帮助找到程序集。在发布版本中变得更糟,优化器期待内联方法的机会。您需要另一种方法,例如 RealMain(),将 Test() 调用移到该方法中。并赋予它 [MethodImpl(MethodImplOptions.Nolinling)] 属性以减慢优化器的速度。
  • 所以基本上它需要任何函数作为入口点并将其转换为 IL,然后再尝试执行任何代码?并且由于该程序集在技术上尚未添加到 AppDomain 中,因此会导致依赖项解析失败?
  • 好的,我很确定我现在明白了。基本上在入口点的 JIT 编译期间,它无法解决依赖关系,因为它显然无法在编译之前执行任何代码,从而导致我看到的错误。将对动态加载的程序集的调用移动到不同的函数范围并使用 [MethodImpl(MethodImplOptions.Nolinling)] 标记调用它们的方法有助于防止优化器将引用动态加载的程序集的代码移动到将在加载程序集之前进行编译。

标签: c# .net reflection appdomain


【解决方案1】:

非常感谢 Hans Passant 和 dthorpe 解释发生了什么。

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

在这里引用 dthorpe:

是的,JIT'ing IL 代码涉及将 IL 转换为本机机器 说明。

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

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

当执行到达函数或代码块时会发生什么(例如, if 块的 else 子句)尚未被 JIT 编译成 本机机器代码,调用 JIT'r 来编译 IL 块 转换为本机机器码。完成后,程序执行进入 新发出的机器代码来执行它的程序逻辑。如果 在执行本机机器代码执行时到达一个函数 调用尚未编译为机器码的函数, 调用 JIT'r 以“及时”编译该函数。以此类推。

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

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

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

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

  1. JIT 编译器尝试转换整个入口点代码 块(在这种情况下是我的 Main() 函数)进入本机代码。这 要求它解析所有引用。
  2. 嵌入式程序集 LoaderLibrary.dll 尚未加载到 AppDomain 尚未,因为执行此操作的代码在 Main() 函数(它不能执行未编译的代码)。
  3. JIT 编译器尝试解析对 LoaderLibrary.dll 通过搜索 AppDomain、Global Assembly Cache、 App.config/Web.config 和探测(环境 PATH,当前 工作目录等)更多信息可以在 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)] 添加到引用动态加载的程序集的方法中,它将阻止优化器尝试内联方法代码。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-08-12
    • 1970-01-01
    • 2012-03-20
    • 1970-01-01
    • 2015-05-05
    • 1970-01-01
    • 1970-01-01
    • 2010-10-20
    相关资源
    最近更新 更多