当前位置: 首页 > news >正文

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

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

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:        00000211D613E5F0Module00007ff889d54000    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:        000002118052B160Module00007ff88a11c060    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();}}

从卦中可以看到确实已经不再有 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 _dependenciesPath0:014> !gcroot 238e9c464c8
Caching GC roots, this may take a while.
Subsequent runs of this command will be faster.Found 0 unique roots.

三:总结

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

http://www.dtcms.com/a/358263.html

相关文章:

  • rhel-server-7.9-x86_64-dvd.iso
  • 机器学习中KNN算法介绍
  • 笔记共享平台|基于Java+vue的读书笔记共享平台系统(源码+数据库+文档)
  • 数据库原理及应用_数据库基础_第3章数据库编程_常用系统函数
  • 骑行商城怎么开发
  • 【金仓数据库产品体验官】KingbaseES-ORACLE兼容版快速体验
  • 国家统计局数据分析01——机器学习
  • GD32VW553-IOT 基于 vscode 的 bootloader 移植(基于Cmake)
  • 【DreamCamera2】相机应用修改成横屏后常见问题解决方案
  • 阿里云营业执照OCR接口的PHP实现与技术解析:从签名机制到企业级应用
  • LZ4 解压工具(WPF / .NET 8)说明书
  • Java Stream API并行流性能优化实践指南
  • 基于Kubernetes自定义调度器的资源隔离与性能优化实践指南
  • 从 0 到 1 构建零丢失 RabbitMQ 数据同步堡垒:第三方接口数据零丢失的终极方案
  • 人工智能学习:Python相关面试题
  • 人工智能学习:Linux相关面试题
  • 98、23种设计模式之代理模式(7/23)
  • spark.sparkContext.broadcast() 与 org.apache.spark.sql.functions.broadcast 的区别
  • 开源PPT生成智能体(Agent)全景透视:技术路线、代表项目与未来趋势
  • 鸿蒙ArkTS 核心篇-15-条件渲染(组件)
  • 三重积分的性质
  • [论文阅读] 人工智能 + 软件工程 | 从“法律条文”到“Gherkin脚本”:Claude与Llama谁更懂合规开发?
  • comfUI背后的技术——VAE
  • [创业之路-581]:如何驾驭不确定性和风险,并从中获利?
  • 什么是雪花算法
  • [Mysql数据库] 知识点总结7
  • 直播间整蛊玩法
  • 【一】Django框架版本介绍
  • 2025 批量下载hasmart所有知乎回答,文章和想法,导出txt,html和pdf
  • OSI与TCP/IP各层功能详解