动态加载DLL解除锁定

是的,使用 Assembly.LoadFrom 加载的 DLL 同样会被 CAD 进程锁定,导致无法直接覆盖编译。核心解决方案是采用动态加载与卸载机制,而非依赖"提前卸载"。

核心原理与解决方案

问题的根源在于,一旦程序集被加载到应用程序域(AppDomain)中,其对应的 DLL 文件就会被操作系统锁定,直到该应用程序域被卸载。CAD 进程(如 AutoCAD、中望CAD)默认在单一应用程序域中运行,因此直接加载的 DLL 会一直被锁定。

根本解决方法是:创建一个独立的、可卸载的应用程序域来加载业务 DLL。 当需要更新 DLL 时,卸载这个独立的应用程序域即可释放文件锁,然后重新编译和加载新版本。

实现方案:使用 AppDomain 进行动态加载与卸载

以下是一个简化的加载器框架,演示如何创建和卸载独立的 AppDomain 来加载目标 DLL。

csharp 复制代码
using System;
using System.Reflection;

public class DynamicAssemblyLoader
{
    private AppDomain _pluginDomain;
    private string _assemblyPath;
    private string _className;
    private string _methodName;

    /// <summary>
    /// 初始化加载器
    /// </summary>
    /// <param name="assemblyPath">目标业务DLL的完整路径</param>
    /// <param name="className">要执行的类的完全限定名</param>
    /// <param name="methodName">要执行的静态方法名</param>
    public DynamicAssemblyLoader(string assemblyPath, string className, string methodName)
    {
        _assemblyPath = assemblyPath;
        _className = className;
        _methodName = methodName;
    }

    /// <summary>
    /// 在独立的AppDomain中加载并执行目标方法
    /// </summary>
    public void ExecuteInIsolatedDomain()
    {
        // 1. 创建独立的应用程序域设置
        AppDomainSetup domainSetup = new AppDomainSetup        {
            ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
            PrivateBinPath = System.IO.Path.GetDirectoryName(_assemblyPath)
        };

        // 2. 创建新的应用程序域 _pluginDomain = AppDomain.CreateDomain("PluginDomain", null, domainSetup);

        // 3. 在新域中创建代理并执行
        try {
            // 加载代理类型(需继承MarshalByRefObject)
            Type proxyType = typeof(PluginProxy);
            PluginProxy proxy = (PluginProxy)_pluginDomain.CreateInstanceAndUnwrap(
                proxyType.Assembly.FullName,
                proxyType.FullName);

            // 4. 通过代理在新域中加载目标DLL并执行方法
            proxy.ExecuteMethod(_assemblyPath, _className, _methodName);
        }
        catch (Exception ex)
        {
            // 处理异常 System.Diagnostics.Debug.WriteLine($"执行失败: {ex.Message}");
 UnloadDomain(); // 执行失败也卸载域
        }
    }

    /// <summary>
    /// 卸载独立的应用程序域,释放DLL文件锁
    /// </summary>
    public void UnloadDomain()
    {
        if (_pluginDomain != null)
        {
            AppDomain.Unload(_pluginDomain);
            _pluginDomain = null;
            System.Diagnostics.Debug.WriteLine("应用程序域已卸载,DLL文件锁已释放。");
        }
    }

    /// <summary>
    /// 代理类,必须继承MarshalByRefObject以跨域通信
    /// </summary>
    private class PluginProxy : MarshalByRefObject
    {
        public void ExecuteMethod(string assemblyPath, string className, string methodName)
        {
            // 在新的AppDomain中加载程序集
            Assembly pluginAssembly = Assembly.LoadFrom(assemblyPath);
            Type targetType = pluginAssembly.GetType(className);
            if (targetType == null)
            {
                throw new ArgumentException($"未找到类: {className}");
            }

            // 获取并执行静态方法
            MethodInfo targetMethod = targetType.GetMethod(methodName,
                BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);
            if (targetMethod == null)
            {
                throw new ArgumentException($"未找到方法: {methodName}");
            }

            // 如果是静态方法,传入null作为实例
            object instance = targetMethod.IsStatic ? null : Activator.CreateInstance(targetType);
            targetMethod.Invoke(instance, null);
        }
    }
}

在CAD命令中的使用流程

在CAD二次开发中,你通常会通过一个"加载器DLL"来管理上述逻辑。这个加载器DLL被CAD永久加载并锁定,但它负责动态加载和卸载真正的业务DLL。

csharp 复制代码
// 在CAD的IExtensionApplication中初始化或通过NETLOAD命令触发的类
public class CadLoader : IExtensionApplication
{
    private DynamicAssemblyLoader _loader;

    public void Initialize()
    {
        // 初始化代码,例如注册命令 AddCommand();
    }

    public void Terminate()
    {
        // 清理代码 }

    private void AddCommand()
    {
        // 注册一个CAD命令,例如"MYCOMMAND"
        var cmdClass = new MyCommands();
    }

    // CAD命令类 public class MyCommands {
        [CommandMethod("MYCOMMAND")]
        public void MyCommand()
        {
            string businessDllPath = @"C:\MyCadPlugins\BusinessLogic.dll";
            string className = "BusinessLogic.MyCadTools";
            string methodName = "Run";

            _loader = new DynamicAssemblyLoader(businessDllPath, className, methodName);

            try {
                _loader.ExecuteInIsolatedDomain();
            }
            finally
            {
                // 执行完毕后立即卸载域,释放文件锁,以便后续编译 _loader.UnloadDomain();
            }
        }
    }
}

方案对比与选择

方案 原理 优点 缺点 适用场景
动态加载/卸载 AppDomain 创建独立、可卸载的应用程序域加载业务DLL。 无需关闭CAD,文件锁可即时释放,支持热编译和调试。 实现较复杂,需处理跨域通信和序列化。 推荐。需要频繁修改代码、进行迭代调试的开发阶段。
关闭CAD后重新编译 物理关闭CAD进程以释放所有DLL锁。 简单粗暴,无需修改代码。 开发效率极低,每次编译都需重启CAD。 代码稳定,极少修改的后期维护阶段。
修改输出路径/重命名DLL 通过生成后事件,每次编译输出到不同文件名的DLL。 实现相对简单,避免文件冲突。 磁盘会残留大量历史DLL文件,管理混乱。 作为临时或辅助方案。

关键实施步骤

  1. 项目分离 :将你的CAD二次开发项目拆分为两个。
    • 加载器项目 (Loader) :引用CAD API(如 acdbmgd.dll, acmgd.dll),包含上述 DynamicAssemblyLoader 逻辑和命令注册。此项目编译的DLL会被CAD锁定,但通常稳定无需频繁重编译。
    • 业务逻辑项目 (BusinessLogic):包含核心功能代码。此项目的输出DLL将通过上述加载器动态加载,可以随时重新编译。
  2. 配置加载器 :在加载器项目中,正确配置对CAD API的引用,并将"复制本地"设置为 False,以避免版本冲突。
  3. 部署与调试 :将加载器DLL通过 NETLOAD 命令加载到CAD。调试时,只需修改业务逻辑项目代码并重新编译,然后在CAD中再次执行命令即可加载新逻辑,实现近乎热更新的效果。

参考来源