是的,使用 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文件,管理混乱。 | 作为临时或辅助方案。 |
关键实施步骤
- 项目分离 :将你的CAD二次开发项目拆分为两个。
- 加载器项目 (Loader) :引用CAD API(如
acdbmgd.dll,acmgd.dll),包含上述DynamicAssemblyLoader逻辑和命令注册。此项目编译的DLL会被CAD锁定,但通常稳定无需频繁重编译。 - 业务逻辑项目 (BusinessLogic):包含核心功能代码。此项目的输出DLL将通过上述加载器动态加载,可以随时重新编译。
- 加载器项目 (Loader) :引用CAD API(如
- 配置加载器 :在加载器项目中,正确配置对CAD API的引用,并将"复制本地"设置为 False,以避免版本冲突。
- 部署与调试 :将加载器DLL通过
NETLOAD命令加载到CAD。调试时,只需修改业务逻辑项目代码并重新编译,然后在CAD中再次执行命令即可加载新逻辑,实现近乎热更新的效果。