一:背景
1. 講故事
最近在分析一個崩潰dump時,發現禍首和AssemblyLoadContext
有關,說實話這東西我也比較陌生,后來查了下大模型,它主要奔著替代 .NetFrameWork 時代的 AppDomain 的,都是用來做晚期加卸載,實現對宿主程序的可插拔,AppDomain.Create 是在AppDomain級別上,后者是在 Assembly 級別上。
二:Assembly 插拔分析
1. 一個簡單的案例
簡單來說這東西可以實現 Assembly 的可插拔,這個小案例有三個基本元素。
- IPlugin 組件接口
這塊比較簡單,新建一個類庫,里面主要就是組件需要實現的接口。
namespace MyClassLibrary.Interfaces
{public interface IPlugin{string Name { get; }string Version { get; }void Execute();string GetResult();}
}
- 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!";}}
- 自定義的 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分析中不斷的螺旋式提升,理論指導實踐,實踐反哺理論。