.NET的AssemblyUnloadEventArgs类如何获取卸载信息?

来源:这里教程网 时间:2026-02-21 17:22:50 作者:

坦白说,如果你期望从

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<Assembly>
或者
List<string>
(记录程序集名称或路径)。当
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<AppDomain, List<string>> _loadedAssembliesMap = new Dictionary<AppDomain, List<string>>();
    public AppDomain CreateAndMonitorDomain(string domainName)
    {
        AppDomain newDomain = AppDomain.CreateDomain(domainName);
        newDomain.AssemblyLoad += NewDomain_AssemblyLoad; // 监听加载事件
        newDomain.AssemblyUnload += NewDomain_AssemblyUnload; // 监听卸载事件
        _loadedAssembliesMap[newDomain] = new List<string>(); // 初始化列表
        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($"
[Unload Event] AppDomain '{unloadedDomain.FriendlyName}' is about to unload.");
            if (_loadedAssembliesMap.TryGetValue(unloadedDomain, out List<string> 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("
--- Simulating some work ---
");
        System.Threading.Thread.Sleep(1000); // 模拟工作
        manager.UnloadDomain(domain1);
        System.Threading.Thread.Sleep(500); // 稍微等待
        manager.UnloadDomain(domain2);
        Console.WriteLine("
All domains unloaded. Press any key to exit.");
        Console.ReadKey();
    }
}

通过这种方式,你可以在

AppDomain
卸载时,获取到你之前加载到该
AppDomain
中的具体程序集信息。这虽然不是直接从
AssemblyUnloadEventArgs
中获取,但却是解决“需要知道具体哪些程序集被卸载”这一问题的有效且常用的策略。它要求你对
AppDomain
的生命周期管理有更主动的控制。

相关推荐