找回密码
 立即注册
首页 业界区 业界 聊一聊 .NET 的 AssemblyLoadContext 可插拔程序集 ...

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

后沛若 2025-9-24 11:38:26
一:背景

1. 讲故事

最近在分析一个崩溃dump时,发现祸首和AssemblyLoadContext有关,说实话这东西我也比较陌生,后来查了下大模型,它主要奔着替代 .NetFrameWork 时代的 AppDomain 的,都是用来做晚期加卸载,实现对宿主程序的可插拔,AppDomain.Create 是在AppDomain级别上,后者是在 Assembly 级别上。
二:Assembly 插拔分析

1. 一个简单的案例

简单来说这东西可以实现 Assembly 的可插拔,这个小案例有三个基本元素。

  • IPlugin 组件接口
这块比较简单,新建一个类库,里面主要就是组件需要实现的接口。
  1. namespace MyClassLibrary.Interfaces
  2. {
  3.     public interface IPlugin
  4.     {
  5.         string Name { get; }
  6.         string Version { get; }
  7.         void Execute();
  8.         string GetResult();
  9.     }
  10. }
复制代码

  • SamplePlugin 组件实现
新建一个组件,完成这些接口方法的实现。
  1.     public class SamplePlugin : IPlugin
  2.     {
  3.         public string Name => "Sample Plugin";
  4.         public string Version => "1.0.0";
  5.         public void Execute()
  6.         {
  7.             Console.WriteLine("SamplePlugin is executing...");
  8.         }
  9.         public string GetResult()
  10.         {
  11.             return "Hello from SamplePlugin!";
  12.         }
  13.     }
复制代码

  • 自定义的 CustomAssemblyLoadContext 上下文
最后就是在调用处自定义下 AssemblyLoadContext 以及简单调用,参考代码如下:
  1. namespace Example_1_6
  2. {
  3.     internal class Program
  4.     {
  5.         static void Main(string[] args)
  6.         {
  7.             Console.WriteLine("=== 插件系统启动 ===");
  8.             // 设置插件目录
  9.             string pluginsPath = @"D:\sources\woodpecker\Test\MyClassLibrary\bin\Debug\net8.0";
  10.             Console.WriteLine($"插件路径: {pluginsPath}");
  11.             var dllFile = Directory.GetFiles(pluginsPath, "MyClassLibrary.dll").FirstOrDefault();
  12.             var _loadContext = new CustomAssemblyLoadContext("MyPluginContext", pluginsPath);
  13.             var assembly = _loadContext.LoadAssembly(dllFile);
  14.             var type = assembly.GetType("MyClassLibrary.SamplePlugin");
  15.             IPlugin plugin = (IPlugin)Activator.CreateInstance(type);
  16.             Console.WriteLine($"- {plugin.Name} v{plugin.Version}");
  17.             Console.WriteLine($"\n执行插件: {plugin.Name} v{plugin.Version}");
  18.             plugin.Execute();
  19.             string result = plugin.GetResult();
  20.             Console.WriteLine($"插件返回: {result}");
  21.             Console.ReadKey();
  22.         }
  23.     }
  24.     public class CustomAssemblyLoadContext : AssemblyLoadContext
  25.     {
  26.         private readonly string _dependenciesPath;
  27.         public CustomAssemblyLoadContext(string name, string dependenciesPath)
  28.             : base(name, isCollectible: true)
  29.         {
  30.             _dependenciesPath = dependenciesPath;
  31.         }
  32.         public Assembly LoadAssembly(string assemblyPath)
  33.         {
  34.             return LoadFromAssemblyPath(assemblyPath);
  35.         }
  36.         public new void Unload()
  37.         {
  38.             base.Unload();
  39.         }
  40.     }
  41. }
复制代码
将代码运行起来,可以看到插件代码得到执行。
1.png

2. 组件已经插上了吗

plugin中的方法都已经执行了,那 MyClassLibrary.dll 自然就插上去了,接下来如何验证呢?可以使用 windbg 的 !dumpdomain 命令即可。
  1. 0:015> !dumpdomain
  2. --------------------------------------
  3. System Domain:      00007ff8e9d4b150
  4. LowFrequencyHeap:   00007FF8E9D4B628
  5. HighFrequencyHeap:  00007FF8E9D4B6B8
  6. StubHeap:           00007FF8E9D4B748
  7. Stage:              OPEN
  8. Name:               None
  9. --------------------------------------
  10. Domain 1:           00000211d617dc80
  11. LowFrequencyHeap:   00007FF8E9D4B628
  12. HighFrequencyHeap:  00007FF8E9D4B6B8
  13. StubHeap:           00007FF8E9D4B748
  14. Stage:              OPEN
  15. Name:               clrhost
  16. Assembly:           00000211d613e560 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.16\System.Private.CoreLib.dll]
  17. ClassLoader:        00000211D613E5F0
  18.   Module
  19.   00007ff889d54000    C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.16\System.Private.CoreLib.dll
  20. ...
  21. Assembly:           000002118052b0d0 [D:\sources\woodpecker\Test\MyClassLibrary\bin\Debug\net8.0\MyClassLibrary.dll]
  22. ClassLoader:        000002118052B160
  23.   Module
  24.   00007ff88a11c060    D:\sources\woodpecker\Test\MyClassLibrary\bin\Debug\net8.0\MyClassLibrary.dll
复制代码
从卦中可以清晰的看到 MyClassLibrary.dll 已经成功的送入。
3. 组件如何卸载掉

能不能卸载掉,其实取决于你在 new AssemblyLoadContext() 时塞入的 isCollectible 字段决定的,如果为true就是一个可卸载的程序集,参考代码如下:
  1.         public CustomAssemblyLoadContext(string name, string dependenciesPath)
  2.             : base(name, isCollectible: true)
  3.         {
  4.             _dependenciesPath = dependenciesPath;
  5.         }
复制代码
其次要知道的是卸载程序集是一个异步操作,不要以为调用了 UnLoad() 就会立即卸载,它只是起到了一个标记删除的作用,只有程序集中的实例无引用根了,即垃圾对象的时候,再后续由 GC 来实现卸载。
这一块我们可以写段代码来验证下,我故意将逻辑包装到 DoWork() 方法中,然后处理完之后再次触发GC,修改后的代码如下:
  1.     internal class Program
  2.     {
  3.         static void Main(string[] args)
  4.         {
  5.             DoWork();
  6.             GC.Collect();
  7.             GC.WaitForPendingFinalizers();
  8.             Console.WriteLine("GC已触发,请再次观察 Assembly 是否被卸载...");
  9.             Console.ReadLine();
  10.         }
  11.         static void DoWork()
  12.         {
  13.             Console.WriteLine("=== 插件系统启动 ===");
  14.             // 设置插件目录
  15.             string pluginsPath = @"D:\sources\woodpecker\Test\MyClassLibrary\bin\Debug\net8.0";
  16.             Console.WriteLine($"插件路径: {pluginsPath}");
  17.             var dllFile = Directory.GetFiles(pluginsPath, "MyClassLibrary.dll").FirstOrDefault();
  18.             var _loadContext = new CustomAssemblyLoadContext("MyPluginContext", pluginsPath);
  19.             var assembly = _loadContext.LoadAssembly(dllFile);
  20.             var type = assembly.GetType("MyClassLibrary.SamplePlugin");
  21.             IPlugin plugin = (IPlugin)Activator.CreateInstance(type);
  22.             Console.WriteLine($"- {plugin.Name} v{plugin.Version}");
  23.             Console.WriteLine($"\n执行插件: {plugin.Name} v{plugin.Version}");
  24.             plugin.Execute();
  25.             string result = plugin.GetResult();
  26.             Console.WriteLine($"插件返回: {result}");
  27.             _loadContext.Unload();
  28.             Console.WriteLine("程序集已标记为卸载... 请观察 Assembly 是否被卸载...");
  29.             Console.ReadKey();
  30.         }
  31.     }
复制代码
2.png

从卦中可以看到确实已经不再有 MyClassLibrary.dll 程序集了,但托管堆上还有 CustomAssemblyLoadContext 死对象,当后续GC触发时再回收,用windbg验证如下:
  1. 0:014> !dumpobj /d 238e9c464c8
  2. Name:        Example_1_6.CustomAssemblyLoadContext
  3. MethodTable: 00007ff88a06f098
  4. EEClass:     00007ff88a079008
  5. Tracked Type: false
  6. Size:        88(0x58) bytes
  7. File:        D:\sources\woodpecker\Test\Example_1_6\bin\Debug\net8.0\Example_1_6.dll
  8. Fields:
  9.               MT    Field   Offset                 Type VT     Attr            Value Name
  10. 00007ff889e870a0  4001116       30        System.IntPtr  1 instance 000002388042A8F0 _nativeAssemblyLoadContext
  11. 00007ff889dd5fa8  4001117        8        System.Object  0 instance 00000238e9c46520 _unloadLock
  12. 0000000000000000  4001118       10                       0 instance 0000000000000000 _resolvingUnmanagedDll
  13. 0000000000000000  4001119       18                       0 instance 0000000000000000 _resolving
  14. 0000000000000000  400111a       20                       0 instance 0000000000000000 _unloading
  15. 00007ff889e8ec08  400111b       28        System.String  0 instance 0000023880006a30 _name
  16. 00007ff889e3a5f0  400111c       38         System.Int64  1 instance                0 _id
  17. 00007ff889f2f108  400111d       40         System.Int32  1 instance                1 _state
  18. 00007ff889ddd070  400111e       44       System.Boolean  1 instance                1 _isCollectible
  19. 00007ff88a0ed120  4001114      a00 ...Private.CoreLib]]  0   static 00000238e9c46550 s_allContexts
  20. 00007ff889e3a5f0  4001115      bc0         System.Int64  1   static                1 s_nextId
  21. 0000000000000000  400111f      a08 ...yLoadEventHandler  0   static 0000000000000000 AssemblyLoad
  22. 0000000000000000  4001120      a10 ...solveEventHandler  0   static 0000000000000000 TypeResolve
  23. 0000000000000000  4001121      a18 ...solveEventHandler  0   static 0000000000000000 ResourceResolve
  24. 0000000000000000  4001122      a20 ...solveEventHandler  0   static 0000000000000000 AssemblyResolve
  25. 0000000000000000  4001123      a28                       0   static 0000000000000000 s_asyncLocalCurrent
  26. 00007ff889e8ec08  4000001       48        System.String  0 instance 0000023880006938 _dependenciesPath
  27. 0:014> !gcroot 238e9c464c8
  28. Caching GC roots, this may take a while.
  29. Subsequent runs of this command will be faster.
  30. Found 0 unique roots.
复制代码
三:总结

有时候感叹 知识无涯人有涯,在 dump分析中不断的螺旋式提升,理论指导实践,实践反哺理论。
3.jpeg


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册