AssemblyUnloadEventArgs本身不包含卸载的程序集信息,仅作为AppDomain卸载的通知信号,其设计目的是提供一个清理资源的时机而非传递数据;具体被卸载的程序集需通过自定义管理器在加载时记录,并在事件触发时通过sender参数获取对应AppDomain的上下文来查询。

坦白说,如果你期望从
AssemblyUnloadEventArgs类中直接获取到具体的卸载信息,比如哪些程序集被卸载了,那你可能会有些失望。因为它是一个空的事件参数类,并没有携带任何额外的数据。它的存在,更多是作为一个信号,通知你一个
AppDomain即将被卸载,而不是提供具体的卸载内容列表。
这个类存在的意义,更多是作为一个信号(signal),而非一个数据载体(data carrier)。当
AppDomain即将被卸载时,它会触发
AppDomain.AssemblyUnload事件,而
AssemblyUnloadEventArgs就是这个事件的参数,告诉你“嘿,有事情要发生了,做好准备!”。它本身不包含卸载的程序集列表,因为这个事件的触发点是整个
AppDomain的生命周期结束,而不是单个程序集的卸载。换句话说,当这个事件被触发时,整个应用域都在走向终结,所有加载到其中的程序集都将随之而去。你真正能从事件处理函数中获取的“信息”,其实是
sender对象,它代表了即将被卸载的那个
AppDomain实例。
为什么AssemblyUnloadEventArgs是空的,它有什么用?
在我看来,
AssemblyUnloadEventArgs的设计哲学,更偏向于一种通知机制,而非详细的事件报告。它就像一个响铃,告诉你“散场了”,但不会告诉你谁走了,或者带走了什么行李。这种设计其实是有其道理的:当一个
AppDomain被卸载时,其内部的所有程序集都会被卸载。所以,事件本身不需要再额外携带一个程序集列表来重复这个事实。
它的主要用途是提供一个钩子(hook),让开发者有机会在
AppDomain完全卸载之前执行一些清理工作。比如,释放非托管资源、关闭文件句柄、保存状态、或者记录日志。这是一个关键的“最后机会”,在托管代码环境被彻底销毁前,完成一些必要的收尾。如果它需要携带大量数据,反而可能增加开销,甚至引发一些复杂性,毕竟在
AppDomain即将死亡的边缘,系统资源和状态都可能变得不稳定。
如何正确监听AppDomain的卸载事件并获取相关上下文?
要监听
AppDomain的卸载事件,我们通常会订阅
AppDomain.CurrentDomain.AssemblyUnload事件。虽然事件参数
AssemblyUnloadEventArgs是空的,但事件处理函数的
sender参数却非常有用。它会传入即将被卸载的
AppDomain实例本身。
using System;
using System.Reflection;
public class AppDomainMonitor
{
public static void Main(string[] args)
{
// 创建一个新的AppDomain
AppDomain newDomain = AppDomain.CreateDomain("MyTestDomain");
// 在新域中加载一个程序集(例如,加载当前执行的程序集)
// 实际应用中,你可能会通过Assembly.LoadFrom加载一个独立的DLL
string assemblyPath = Assembly.GetExecutingAssembly().Location;
newDomain.Load(AssemblyName.GetAssemblyName(assemblyPath));
// 订阅新域的AssemblyUnload事件
newDomain.AssemblyUnload += NewDomain_AssemblyUnload;
Console.WriteLine($"AppDomain '{newDomain.FriendlyName}' created and event subscribed.");
// 执行一些操作...
Console.WriteLine("Performing some operations in the new domain...");
// 卸载AppDomain
Console.WriteLine($"Unloading AppDomain '{newDomain.FriendlyName}'...");
AppDomain.Unload(newDomain);
Console.WriteLine("AppDomain unload initiated. Press any key to exit.");
Console.ReadKey();
}
private static void NewDomain_AssemblyUnload(object sender, AssemblyUnloadEventArgs e)
{
// 这里的 sender 就是即将被卸载的 AppDomain 实例
AppDomain unloadedDomain = sender as AppDomain;
if (unloadedDomain != null)
{
Console.WriteLine($"[Event Handler] AppDomain '{unloadedDomain.FriendlyName}' is about to unload.");
// 在这里执行清理逻辑
Console.WriteLine("[Event Handler] Performing cleanup operations...");
// 此时尝试访问 unloadedDomain.GetAssemblies() 可能会失败或返回不完整的结果
// 因为 AppDomain 已经处于卸载过程中
// Console.WriteLine($"[Event Handler] Assemblies in '{unloadedDomain.FriendlyName}':");
// foreach (var assembly in unloadedDomain.GetAssemblies())
// {
// Console.WriteLine($" - {assembly.FullName}");
// }
}
}
}通过
sender,你可以识别出是哪个
AppDomain正在被卸载。这对于多
AppDomain场景下的日志记录、资源管理或特定于
AppDomain的清理操作至关重要。比如,如果你为每个插件创建了一个独立的
AppDomain,那么在卸载事件中,你可以根据
sender来确定是哪个插件的
AppDomain要被销毁,进而执行该插件特有的清理逻辑。
如果我需要知道具体哪些程序集被卸载了,应该怎么做?
这是一个非常实际的需求,但也是
AssemblyUnloadEventArgs本身无法满足的。它不会告诉你具体哪些程序集,因为它的作用域是整个
AppDomain的卸载。如果你真的需要这个信息,那么你需要在
AppDomain的生命周期内,自己维护一个已加载程序集的列表。
这通常意味着,在你加载程序集的时候,就应该把它们记录下来。比如,在一个自定义的
AppDomain管理器中,维护一个
List或者
List(记录程序集名称或路径)。当
AssemblyUnload事件触发时,你可以利用
sender(即即将被卸载的
AppDomain实例)来索引到你之前为该
AppDomain维护的程序集列表。
实现思路:
-
创建自定义
AppDomain
管理器: 封装AppDomain
的创建、加载程序集和卸载逻辑。 -
追踪加载的程序集: 在每次通过
AppDomain.Load()
或AppDomain.ExecuteAssembly()
加载程序集时,将程序集的完整名称或路径记录到一个与该AppDomain
实例关联的集合中。 -
在卸载事件中查询: 当
AppDomain.AssemblyUnload
事件触发时,通过sender
获取到对应的AppDomain
,然后从你的管理器中查询该AppDomain
之前记录的程序集列表。
using System;
using System.Collections.Generic;
using System.Reflection;
public class CustomAppDomainManager
{
private static Dictionary> _loadedAssembliesMap = new Dictionary>();
public AppDomain CreateAndMonitorDomain(string domainName)
{
AppDomain newDomain = AppDomain.CreateDomain(domainName);
newDomain.AssemblyLoad += NewDomain_AssemblyLoad; // 监听加载事件
newDomain.AssemblyUnload += NewDomain_AssemblyUnload; // 监听卸载事件
_loadedAssembliesMap[newDomain] = new List(); // 初始化列表
Console.WriteLine($"Custom AppDomain '{domainName}' created.");
return newDomain;
}
public void LoadAssemblyIntoDomain(AppDomain domain, string assemblyPath)
{
// 假设这里是实际的加载逻辑,例如通过反射在远程域中执行
// 为了简化示例,我们只是模拟加载并记录
if (System.IO.File.Exists(assemblyPath))
{
// 在实际的跨域加载中,你需要使用 domain.Load() 或 domain.ExecuteAssembly()
// 这里我们只是记录路径
_loadedAssembliesMap[domain].Add(assemblyPath);
Console.WriteLine($" - Assembly '{assemblyPath}' simulated loaded into '{domain.FriendlyName}'.");
}
else
{
Console.WriteLine($" - Assembly path '{assemblyPath}' not found.");
}
}
public void UnloadDomain(AppDomain domain)
{
Console.WriteLine($"Initiating unload for AppDomain '{domain.FriendlyName}'...");
AppDomain.Unload(domain);
}
private static void NewDomain_AssemblyLoad(object sender, AssemblyLoadEventArgs args)
{
// 理论上,这里也可以记录,但通常我们更关心我们主动加载的
// 如果需要,可以在这里将 args.LoadedAssembly.FullName 加入 _loadedAssembliesMap[sender as AppDomain]
// 但通常我们只追踪我们主动加载的,避免系统程序集
// Console.WriteLine($"[Load Event] Assembly '{args.LoadedAssembly.FullName}' loaded into '{((AppDomain)sender).FriendlyName}'.");
}
private static void NewDomain_AssemblyUnload(object sender, AssemblyUnloadEventArgs e)
{
AppDomain unloadedDomain = sender as AppDomain;
if (unloadedDomain != null)
{
Console.WriteLine($"\n[Unload Event] AppDomain '{unloadedDomain.FriendlyName}' is about to unload.");
if (_loadedAssembliesMap.TryGetValue(unloadedDomain, out List assemblies))
{
Console.WriteLine($" Previously tracked assemblies in '{unloadedDomain.FriendlyName}':");
foreach (var assemblyPath in assemblies)
{
Console.WriteLine($" - {assemblyPath}");
}
_loadedAssembliesMap.Remove(unloadedDomain); // 清理追踪数据
}
else
{
Console.WriteLine($" No tracked assemblies found for '{unloadedDomain.FriendlyName}'.");
}
Console.WriteLine($"[Unload Event] Cleanup complete for '{unloadedDomain.FriendlyName}'.");
}
}
public static void Main(string[] args)
{
CustomAppDomainManager manager = new CustomAppDomainManager();
AppDomain domain1 = manager.CreateAndMonitorDomain("PluginDomain1");
manager.LoadAssemblyIntoDomain(domain1, "C:\\Plugins\\MyPluginA.dll");
manager.LoadAssemblyIntoDomain(domain1, "C:\\Plugins\\MyPluginB.dll");
AppDomain domain2 = manager.CreateAndMonitorDomain("PluginDomain2");
manager.LoadAssemblyIntoDomain(domain2, "C:\\Plugins\\MyPluginC.dll");
Console.WriteLine("\n--- Simulating some work ---\n");
System.Threading.Thread.Sleep(1000); // 模拟工作
manager.UnloadDomain(domain1);
System.Threading.Thread.Sleep(500); // 稍微等待
manager.UnloadDomain(domain2);
Console.WriteLine("\nAll domains unloaded. Press any key to exit.");
Console.ReadKey();
}
} 通过这种方式,你可以在
AppDomain卸载时,获取到你之前加载到该
AppDomain中的具体程序集信息。这虽然不是直接从
AssemblyUnloadEventArgs中获取,但却是解决“需要知道具体哪些程序集被卸载”这一问题的有效且常用的策略。它要求你对
AppDomain的生命周期管理有更主动的控制。










