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

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

    正在检查是否收录...

一:背景

1. 讲故事

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

二:Assembly 插拔分析

1. 一个简单的案例

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

  1. IPlugin 组件接口

这块比较简单,新建一个类库,里面主要就是组件需要实现的接口。

 namespace MyClassLibrary.Interfaces { public interface IPlugin { string Name { get; } string Version { get; } void Execute(); string GetResult(); } } 
  1. SamplePlugin 组件实现

新建一个组件,完成这些接口方法的实现。

 public class SamplePlugin : IPlugin { public string Name => "Sample Plugin"; public string Version => "1.0.0"; public void Execute() { Console.WriteLine("SamplePlugin is executing..."); } public string GetResult() { return "Hello from SamplePlugin!"; } } 
  1. 自定义的 CustomAssemblyLoadContext 上下文

最后就是在调用处自定义下 AssemblyLoadContext 以及简单调用,参考代码如下:

 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(); } } } 

将代码运行起来,可以看到插件代码得到执行。

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

2. 组件已经插上了吗

plugin中的方法都已经执行了,那 MyClassLibrary.dll 自然就插上去了,接下来如何验证呢?可以使用 windbg 的 !dumpdomain 命令即可。

 0:015> !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 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.16\System.Private.CoreLib.dll] ClassLoader: 00000211D613E5F0 Module 00007ff889d54000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.16\System.Private.CoreLib.dll ... Assembly: 000002118052b0d0 [D:\sources\woodpecker\Test\MyClassLibrary\bin\Debug\net8.0\MyClassLibrary.dll] ClassLoader: 000002118052B160 Module 00007ff88a11c060 D:\sources\woodpecker\Test\MyClassLibrary\bin\Debug\net8.0\MyClassLibrary.dll 

从卦中可以清晰的看到 MyClassLibrary.dll 已经成功的送入。

3. 组件如何卸载掉

能不能卸载掉,其实取决于你在 new AssemblyLoadContext() 时塞入的 isCollectible 字段决定的,如果为true就是一个可卸载的程序集,参考代码如下:

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

其次要知道的是卸载程序集是一个异步操作,不要以为调用了 UnLoad() 就会立即卸载,它只是起到了一个标记删除的作用,只有程序集中的实例无引用根了,即垃圾对象的时候,再后续由 GC 来实现卸载。

这一块我们可以写段代码来验证下,我故意将逻辑包装到 DoWork() 方法中,然后处理完之后再次触发GC,修改后的代码如下:

 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(); } } 

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

从卦中可以看到确实已经不再有 MyClassLibrary.dll 程序集了,但托管堆上还有 CustomAssemblyLoadContext 死对象,当后续GC触发时再回收,用windbg验证如下:

 0:014> !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 00007ff889e870a0 4001116 30 System.IntPtr 1 instance 000002388042A8F0 _nativeAssemblyLoadContext 00007ff889dd5fa8 4001117 8 System.Object 0 instance 00000238e9c46520 _unloadLock 0000000000000000 4001118 10 0 instance 0000000000000000 _resolvingUnmanagedDll 0000000000000000 4001119 18 0 instance 0000000000000000 _resolving 0000000000000000 400111a 20 0 instance 0000000000000000 _unloading 00007ff889e8ec08 400111b 28 System.String 0 instance 0000023880006a30 _name 00007ff889e3a5f0 400111c 38 System.Int64 1 instance 0 _id 00007ff889f2f108 400111d 40 System.Int32 1 instance 1 _state 00007ff889ddd070 400111e 44 System.Boolean 1 instance 1 _isCollectible 00007ff88a0ed120 4001114 a00 ...Private.CoreLib]] 0 static 00000238e9c46550 s_allContexts 00007ff889e3a5f0 4001115 bc0 System.Int64 1 static 1 s_nextId 0000000000000000 400111f a08 ...yLoadEventHandler 0 static 0000000000000000 AssemblyLoad 0000000000000000 4001120 a10 ...solveEventHandler 0 static 0000000000000000 TypeResolve 0000000000000000 4001121 a18 ...solveEventHandler 0 static 0000000000000000 ResourceResolve 0000000000000000 4001122 a20 ...solveEventHandler 0 static 0000000000000000 AssemblyResolve 0000000000000000 4001123 a28 0 static 0000000000000000 s_asyncLocalCurrent 00007ff889e8ec08 4000001 48 System.String 0 instance 0000023880006938 _dependenciesPath 0:014> !gcroot 238e9c464c8 Caching GC roots, this may take a while. Subsequent runs of this command will be faster. Found 0 unique roots. 

三:总结

有时候感叹 知识无涯人有涯,在 dump分析中不断的螺旋式提升,理论指导实践,实践反哺理论。
聊一聊 .NET 的 AssemblyLoadContext 可插拔程序集

  • 本文作者:WAP站长网
  • 本文链接: https://wapzz.net/post-27581.html
  • 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 4.0 许可协议。
本站部分内容来源于网络转载,仅供学习交流使用。如涉及版权问题,请及时联系我们,我们将第一时间处理。
文章很赞!支持一下吧 还没有人为TA充电
为TA充电
还没有人为TA充电
0
0
  • 支付宝打赏
    支付宝扫一扫
  • 微信打赏
    微信扫一扫
感谢支持
文章很赞!支持一下吧
关于作者
2.8W+
9
1
2
WAP站长官方

云闪付联合HarmonyOS SDK打造更便捷安全的支付体验

上一篇

美团王兴回应外卖业务受影响:反对内卷 坚持做正确的事

下一篇
评论区
内容为空

这一切,似未曾拥有

  • 复制图片
按住ctrl可打开默认菜单