皇一族 發表於 2025-8-30 12:19:00

聊一聊 .NET 的 AssemblyLoadContext 可插拔程序集

<h2 id="一背景">一:背景</h2>
<h3 id="1-讲故事">1. 讲故事</h3>
<p>最近在分析一个崩溃dump时,发现祸首和<code>AssemblyLoadContext</code>有关,说实话这东西我也比较陌生,后来查了下大模型,它主要奔着替代 .NetFrameWork 时代的 AppDomain 的,都是用来做晚期加卸载,实现对宿主程序的可插拔,AppDomain.Create 是在AppDomain级别上,后者是在 Assembly 级别上。</p>
<h2 id="二assembly-插拔分析">二:Assembly 插拔分析</h2>
<h3 id="1-一个简单的案例">1. 一个简单的案例</h3>
<p>简单来说这东西可以实现 Assembly 的可插拔,这个小案例有三个基本元素。</p>
<ol>
<li>IPlugin 组件接口</li>
</ol>
<p>这块比较简单,新建一个类库,里面主要就是组件需要实现的接口。</p>
<pre><code class="language-C#">
namespace MyClassLibrary.Interfaces
{
    public interface IPlugin
    {
      string Name { get; }
      string Version { get; }
      void Execute();
      string GetResult();
    }
}

</code></pre>
<ol start="2">
<li>SamplePlugin 组件实现</li>
</ol>
<p>新建一个组件,完成这些接口方法的实现。</p>
<pre><code class="language-C#">
    public class SamplePlugin : IPlugin
    {
      public string Name =&gt; "Sample Plugin";
      public string Version =&gt; "1.0.0";

      public void Execute()
      {
            Console.WriteLine("SamplePlugin is executing...");
      }

      public string GetResult()
      {
            return "Hello from SamplePlugin!";
      }
    }

</code></pre>
<ol start="3">
<li>自定义的 CustomAssemblyLoadContext 上下文</li>
</ol>
<p>最后就是在调用处自定义下 AssemblyLoadContext 以及简单调用,参考代码如下:</p>
<pre><code class="language-C#">
namespace Example_1_6
{
    internal class Program
    {
      static void Main(string[] args)
      {
            Console.WriteLine("=== 插件系统启动 ===");

            // 设置插件目录
            string pluginsPath = @"D:\sources\woodpecker\Test\MyClassLibrary\bin\Debug\net8.0\";

            Console.WriteLine($"插件路径: {pluginsPath}");

            var dllFile = Directory.GetFiles(pluginsPath, "MyClassLibrary.dll").FirstOrDefault();

            var _loadContext = new CustomAssemblyLoadContext("MyPluginContext", pluginsPath);

            var assembly = _loadContext.LoadAssembly(dllFile);

            var type = assembly.GetType("MyClassLibrary.SamplePlugin");

            IPlugin plugin = (IPlugin)Activator.CreateInstance(type);

            Console.WriteLine($"- {plugin.Name} v{plugin.Version}");

            Console.WriteLine($"\n执行插件: {plugin.Name} v{plugin.Version}");
            plugin.Execute();
            string result = plugin.GetResult();
            Console.WriteLine($"插件返回: {result}");

            Console.ReadKey();
      }
    }

    public class CustomAssemblyLoadContext : AssemblyLoadContext
    {
      private readonly string _dependenciesPath;

      public CustomAssemblyLoadContext(string name, string dependenciesPath)
            : base(name, isCollectible: true)
      {
            _dependenciesPath = dependenciesPath;
      }

      public Assembly LoadAssembly(string assemblyPath)
      {
            return LoadFromAssemblyPath(assemblyPath);
      }

      public new void Unload()
      {
            base.Unload();
      }
    }
}

</code></pre>
<p>将代码运行起来,可以看到插件代码得到执行。</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202508/214741-20250830121855353-2022459207.png" alt="" loading="lazy"></p>
<h3 id="2-组件已经插上了吗">2. 组件已经插上了吗</h3>
<p>plugin中的方法都已经执行了,那 <code>MyClassLibrary.dll</code> 自然就插上去了,接下来如何验证呢?可以使用 windbg 的 <code>!dumpdomain</code> 命令即可。</p>
<pre><code class="language-C#">
0:015&gt; !dumpdomain
--------------------------------------
System Domain:      00007ff8e9d4b150
LowFrequencyHeap:   00007FF8E9D4B628
HighFrequencyHeap:00007FF8E9D4B6B8
StubHeap:         00007FF8E9D4B748
Stage:            OPEN
Name:               None
--------------------------------------
Domain 1:         00000211d617dc80
LowFrequencyHeap:   00007FF8E9D4B628
HighFrequencyHeap:00007FF8E9D4B6B8
StubHeap:         00007FF8E9D4B748
Stage:            OPEN
Name:               clrhost
Assembly:         00000211d613e560
ClassLoader:      00000211D613E5F0
Module
00007ff889d54000    C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.16\System.Private.CoreLib.dll

...

Assembly:         000002118052b0d0
ClassLoader:      000002118052B160
Module
00007ff88a11c060    D:\sources\woodpecker\Test\MyClassLibrary\bin\Debug\net8.0\MyClassLibrary.dll

</code></pre>
<p>从卦中可以清晰的看到 <code>MyClassLibrary.dll</code> 已经成功的送入。</p>
<h3 id="3-组件如何卸载掉">3. 组件如何卸载掉</h3>
<p>能不能卸载掉,其实取决于你在 <code>new AssemblyLoadContext()</code> 时塞入的 isCollectible 字段决定的,如果为true就是一个可卸载的程序集,参考代码如下:</p>
<pre><code class="language-C#">
      public CustomAssemblyLoadContext(string name, string dependenciesPath)
            : base(name, isCollectible: true)
      {
            _dependenciesPath = dependenciesPath;
      }

</code></pre>
<p>其次要知道的是<code>卸载程序集</code>是一个异步操作,不要以为调用了 <code>UnLoad()</code> 就会立即卸载,它只是起到了一个标记删除的作用,只有程序集中的实例无引用根了,即垃圾对象的时候,再后续由 GC 来实现卸载。</p>
<p>这一块我们可以写段代码来验证下,我故意将逻辑包装到 DoWork() 方法中,然后处理完之后再次触发GC,修改后的代码如下:</p>
<pre><code class="language-C#">
    internal class Program
    {
      static void Main(string[] args)
      {
            DoWork();

            GC.Collect();
            GC.WaitForPendingFinalizers();

            Console.WriteLine("GC已触发,请再次观察 Assembly 是否被卸载...");

            Console.ReadLine();
      }

      static void DoWork()
      {
            Console.WriteLine("=== 插件系统启动 ===");

            // 设置插件目录
            string pluginsPath = @"D:\sources\woodpecker\Test\MyClassLibrary\bin\Debug\net8.0\";

            Console.WriteLine($"插件路径: {pluginsPath}");

            var dllFile = Directory.GetFiles(pluginsPath, "MyClassLibrary.dll").FirstOrDefault();

            var _loadContext = new CustomAssemblyLoadContext("MyPluginContext", pluginsPath);

            var assembly = _loadContext.LoadAssembly(dllFile);

            var type = assembly.GetType("MyClassLibrary.SamplePlugin");

            IPlugin plugin = (IPlugin)Activator.CreateInstance(type);

            Console.WriteLine($"- {plugin.Name} v{plugin.Version}");

            Console.WriteLine($"\n执行插件: {plugin.Name} v{plugin.Version}");
            plugin.Execute();
            string result = plugin.GetResult();
            Console.WriteLine($"插件返回: {result}");

            _loadContext.Unload();

            Console.WriteLine("程序集已标记为卸载... 请观察 Assembly 是否被卸载...");
            Console.ReadKey();
      }
    }

</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/214741/202508/214741-20250830121855365-571617532.png" alt="" loading="lazy"></p>
<p>从卦中可以看到确实已经不再有 <code>MyClassLibrary.dll</code> 程序集了,但托管堆上还有 CustomAssemblyLoadContext 死对象,当后续GC触发时再回收,用windbg验证如下:</p>
<pre><code class="language-C#">
0:014&gt; !dumpobj /d 238e9c464c8
Name:      Example_1_6.CustomAssemblyLoadContext
MethodTable: 00007ff88a06f098
EEClass:   00007ff88a079008
Tracked Type: false
Size:      88(0x58) bytes
File:      D:\sources\woodpecker\Test\Example_1_6\bin\Debug\net8.0\Example_1_6.dll
Fields:
            MT    Field   Offset               Type VT   Attr            Value Name
00007ff889e870a04001116       30      System.IntPtr1 instance 000002388042A8F0 _nativeAssemblyLoadContext
00007ff889dd5fa84001117      8      System.Object0 instance 00000238e9c46520 _unloadLock
00000000000000004001118       10                     0 instance 0000000000000000 _resolvingUnmanagedDll
00000000000000004001119       18                     0 instance 0000000000000000 _resolving
0000000000000000400111a       20                     0 instance 0000000000000000 _unloading
00007ff889e8ec08400111b       28      System.String0 instance 0000023880006a30 _name
00007ff889e3a5f0400111c       38         System.Int641 instance                0 _id
00007ff889f2f108400111d       40         System.Int321 instance                1 _state
00007ff889ddd070400111e       44       System.Boolean1 instance                1 _isCollectible
00007ff88a0ed1204001114      a00 ...Private.CoreLib]]0   static 00000238e9c46550 s_allContexts
00007ff889e3a5f04001115      bc0         System.Int641   static                1 s_nextId
0000000000000000400111f      a08 ...yLoadEventHandler0   static 0000000000000000 AssemblyLoad
00000000000000004001120      a10 ...solveEventHandler0   static 0000000000000000 TypeResolve
00000000000000004001121      a18 ...solveEventHandler0   static 0000000000000000 ResourceResolve
00000000000000004001122      a20 ...solveEventHandler0   static 0000000000000000 AssemblyResolve
00000000000000004001123      a28                     0   static 0000000000000000 s_asyncLocalCurrent
00007ff889e8ec084000001       48      System.String0 instance 0000023880006938 _dependenciesPath

0:014&gt; !gcroot 238e9c464c8
Caching GC roots, this may take a while.
Subsequent runs of this command will be faster.

Found 0 unique roots.

</code></pre>
<h2 id="三总结">三:总结</h2>
<p>有时候感叹 <code>知识无涯人有涯</code>,在 dump分析中不断的螺旋式提升,理论指导实践,实践反哺理论。<br>
<img src="https://images.cnblogs.com/cnblogs_com/huangxincheng/345039/o_210929020104最新消息优惠促销公众号关注二维码.jpg" width="700" height="300" alt="图片名称" align="center"></p><br><br>
来源:https://www.cnblogs.com/huangxincheng/p/19065688
頁: [1]
查看完整版本: 聊一聊 .NET 的 AssemblyLoadContext 可插拔程序集