cad.net dll动态加载和卸载
20240817补
acad卸载不太完美,我把卸载猜想写到下面链接中,大家自行实现.
主要是针对COM的GCHandle问题.
工程Loadx分支中呈现全部的旧代码.
https://gitee.com/inspirefunction/CadLabelBar/issues/IAI0ZZ
当时我搞卸载的时候怎么没看到下面链接,新版net出来微软就弄了?
什么JIT特化会进行内联之类的,我net3.5怎么阻止...
https://www.bookstack.cn/read/dotnet/18b062d94c6a58fa.md
需求
应用需求
1: 我们cad.net开发都会面临一个问题,
cad一直打开的状态下netload两次同名,但版本不一样的dll,它只会用第一次载入的.
也没法做到热插拔...
2: 制作一个拖拉dll到cad加载,
但是不想通过发送netload到命令栏以明文形式加载...
已有的资料 明经netloadx 似乎是不二之选...
但真正令我开始研究是因为若海提出的: 明经netloadx
在 a.dll 引用了 b.dll 时候,为什么不会成功调用...
我试图尝试直接 Assembly.Load(File.ReadAllBytes(path)) 在加载目录的每个文件,并没有报错,
然后出现了一个情况,能使用单独的命令,
却还是不能跨dll调用,也就是会有运行出错(runtime error).
注明: Assembly.Load(byte),转为byte是为了实现热插拔,Assembly.LoadForm()没有byte重载,也就无法拷贝到内存中去,故此不考虑.
南胜写了一篇文章回答了,但是他代码出现了几个问题:
- 他获取的路径是clr寻找路径之一,我需要改到加载路径上面的.
各位自行去看看clr的寻找未知dll的方式. - 以及他只支持一个引用的dll,而我需要知道引用的引用...的dll.
所以对他的代码修改一番.
工程开始
项目地址在这里,我博客只是带你慢慢看.
https://gitee.com/inspirefunction/CadLabelBar/tree/loadx/若海加载项目增加卸载版
项目结构
首先,共有四个项目.
- cad主插件项目:直接netload的项目.
- cad次插件:testa,testb [给a引用],testc [给b引用],后面还有套娃也可以...
cad子插件项目
// testa项目代码...
namespace testa {
public class MyCommands {
[CommandMethod(nameof(testa))]
public static void testa() {
var doc = Acap.DocumentManager.MdiActiveDocument;
if (doc == null) return;
doc.Editor.WriteMessage("\r\n自带函数" + mameof(testa));
}
[CommandMethod(nameof(gggg))]
public void gggg() {
var doc = Acap.DocumentManager.MdiActiveDocument;
if (doc == null) return;
doc.Editor.WriteMessage("\r\n **********" + nameof(gggg));
testb.MyCommands.TestBHello();
}
}
}
// testb项目代码...
namespace testb {
public class MyCommands {
public static void TestBHello() {
var doc = Acap.DocumentManager.MdiActiveDocument;
if (doc == null) return;
doc.Editor.WriteMessage("************" + nameof(TestBHello));
testc.MyCommands.TestcHello();
}
[CommandMethod(nameof(testb))]
public static void testb() {
var doc = Acap.DocumentManager.MdiActiveDocument;
if (doc == null) return;
doc.Editor.WriteMessage("\r\n自带函数"+nameof(testb));
}
}
}
// testc项目代码...
namespace testc {
public class MyCommands {
public static void TestCHello() {
var doc = Acap.DocumentManager.MdiActiveDocument;
if (doc == null) return;
doc.Editor.WriteMessage("************" + nameof(TestCHello));
}
[CommandMethod(nameof(testc))]
public static void testc() {
var doc = Acap.DocumentManager.MdiActiveDocument;
if (doc == null) return;
doc.Editor.WriteMessage("\r\n自带函数" + nameof(testc));
}
}
}
迭代版本号
必须更改版本号最后是*,否则无法重复加载(所有)
如果想加载时候动态修改dll的版本号,需要学习PE读写.(此文略)
net framework要直接编辑项目文件.csproj,启用由vs迭代版本号:
<PropertyGroup>
<Deterministic>False</Deterministic>
</PropertyGroup>
然后修改AssemblyInfo.cs
net standard只需要增加.csproj的这里,没有自己加一个:
<PropertyGroup>
<AssemblyVersion>1.0.0.*</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<Deterministic>False</Deterministic>
</PropertyGroup>
cad主插件项目
概念
先说一下测试环境和概念,
cad主插件
上面写了一个命令,这个命令调用了WinForm窗体
让它接受拖拽dll文件,拿到dll的路径,然后链式加载...
这个时候需要直接启动cad,然后调用netload命令加载cad主插件
的dll.
如果采用vs调试cad启动的话,那么我们本来也这么想的,但是会出错.
经过若海两天的Debug发现了: 不能在vs调试状态下运行cad!应该直接启动它!
猜想:这个时候令vs托管了cad的内存,令所有 Assembly.Load(byte)
都进入了托管内存上面,vs自动占用到 obj\Debug
文件夹下的dll.,不信你也可以试一下.
我开了个新文章写这个问题
启动cad之后,用命令调用出WinForm窗体
,再利用拖拽testa.dll
的方式,就可以链式加载到所有的dll了!
再修改testa.dll
重新编译,再拖拽到WinForm窗体
加载,
再修改testb.dll
重新编译,再拖拽到WinForm窗体
加载,
再修改testc.dll
重新编译,再拖拽到WinForm窗体
加载
.....如此如此,这般这般.....
WinForm窗体
拖拽这个函数网络搜一下基本能搞定,我就不贴代码了,接收拖拽之后就有个testa.dll
的路径,再调用传给加载函数就好了.
调用方法
AssemblyDependent ad = new();
List<LoadState> ls = new();
ad.Load(item, ls);
var msg = AssemblyDependent.PrintMessage(ls);
if (msg != null)
MessageBox.Show(msg, "加载完毕!");
else
MessageBox.Show("无任何信息", "加载出现问题!");
链式加载
#define HarmonyPatch
#define HarmonyPatch_1
namespace IFoxCAD.LoadEx;
/*
* 因为此处引用了 nuget的 Lib.Harmony
* 所以单独分一个工程出来作为cad工程的引用
* 免得污染了cad工程的纯洁
*/
#if HarmonyPatch_1
[HarmonyPatch("Autodesk.AutoCAD.ApplicationServices.ExtensionLoader", "OnAssemblyLoad")]
#endif
public class AssemblyDependent : IDisposable
{
#if HarmonyPatch
//这个是不能删除的,否则就不执行了
//HarmonyPatch hook method 返回 false 表示拦截原函数
public static bool Prefix() { return false; }
#endif
#region 字段和事件
/// <summary>
/// 当前域加载事件,运行时出错的话,就靠这个事件来解决
/// </summary>
public event ResolveEventHandler CurrentDomainAssemblyResolveEvent
{
add { AppDomain.CurrentDomain.AssemblyResolve += value; }
remove { AppDomain.CurrentDomain.AssemblyResolve -= value; }
}
/// <summary>
/// 拦截cad的Loader异常:默认是<paramref name="false"/>
/// </summary>
public bool PatchExtensionLoader = false;
#endregion
#region 构造
/// <summary>
/// 链式加载dll依赖
/// </summary>
public AssemblyDependent()
{
//初始化一次,反复load
CurrentDomainAssemblyResolveEvent += AssemblyHelper.DefaultAssemblyResolve;
}
#endregion
#region 获取加载链
/// <summary>
/// 加载程序集
/// </summary>
/// <param name="dllFullName">dll的文件位置</param>
/// <param name="loadStates">返回加载链</param>
/// <param name="byteLoad">true字节加载,false文件加载</param>
/// <returns> 参数 <paramref name="dllFullName"/> 加载成功标识
/// <code> 链条后面的不再理会,因为相同的dll引用辨识无意义 </code>
/// </returns>
public bool Load(string? dllFullName, List<LoadState> loadStates, bool byteLoad = true)
{
if (dllFullName == null)
throw new ArgumentNullException(nameof(dllFullName));
dllFullName = Path.GetFullPath(dllFullName);//相对路径要先转换
if (!File.Exists(dllFullName))
throw new ArgumentException("路径不存在");
//程序集数组要动态获取(每次Load的时候),
//否则会变成一个固定数组,造成加载了之后也不会出现成员
var cadAssembly = AppDomain.CurrentDomain.GetAssemblies();
var cadAssemblyRef = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();
List<string> allRefs = new();
GetAllRefPaths(cadAssembly, cadAssemblyRef, dllFullName, allRefs);
bool dllFullNameLoadOk = false;
//查询加载链逆向加载,确保前面不丢失
//这里有问题,从尾巴开始的,就一定是没有任何引用吗?
for (int i = allRefs.Count - 1; i >= 0; i--)
{
var allRef = allRefs[i];
//路径转程序集名
var an = AssemblyName.GetAssemblyName(allRef).FullName;
var assembly = cadAssembly.FirstOrDefault(a => a.FullName == an);
if (assembly != null)
{
loadStates.Add(new LoadState(allRef, false));//版本号没变不加载
continue;
}
//有一次true,就是true
if (allRef == dllFullName)
dllFullNameLoadOk = true;
try
{
var ass = GetPdbAssembly(allRef);
if (ass == null)
if (byteLoad)
ass = Assembly.Load(File.ReadAllBytes(allRef));
else
ass = Assembly.LoadFile(allRef);
loadStates.Add(new LoadState(allRef, true, ass));/*加载成功*/
}
catch { loadStates.Add(new LoadState(allRef, false));/*错误造成*/ }
}
return dllFullNameLoadOk;
}
/// <summary>
/// 在debug模式的时候才获取PBD调试信息
/// </summary>
/// <param name="path"></param>
/// <param name="byteLoad"></param>
/// <returns></returns>
Assembly? GetPdbAssembly(string? path)
{
#if DEBUG
//为了实现Debug时候出现断点,见链接,加依赖
// https://www.cnblogs.com/DasonKwok/p/10510218.html
// https://www.cnblogs.com/DasonKwok/p/10523279.html
var dir = Path.GetDirectoryName(path);
var pdbName = Path.GetFileNameWithoutExtension(path) + ".pdb";
var pdbFullName = Path.Combine(dir, pdbName);
if (File.Exists(pdbFullName))
return Assembly.Load(File.ReadAllBytes(path), File.ReadAllBytes(pdbFullName));
#endif
return null;
}
/// <summary>
/// 递归获取加载链
/// </summary>
/// <param name="cadAssembly">程序集_内存区</param>
/// <param name="cadAssemblyRef">程序集_映射区</param>
/// <param name="dllFullName">dll文件位置</param>
/// <param name="dllFullNamesOut">返回的集合</param>
/// <returns></returns>
void GetAllRefPaths(Assembly[] cadAssembly,
Assembly[] cadAssemblyRef,
string? dllFullName,
List<string> dllFullNamesOut)
{
if (dllFullName == null)
throw new ArgumentNullException(nameof(dllFullName));
if (dllFullNamesOut.Contains(dllFullName) || !File.Exists(dllFullName))
return;
dllFullNamesOut.Add(dllFullName);
var assemblyAsRef = GetAssembly(cadAssembly, cadAssemblyRef, dllFullName);
if (assemblyAsRef == null)
return;
var sb = new StringBuilder();
//dll拖拉加载路径-搜索路径(可以增加到这个dll下面的所有文件夹?)
sb.Append(Path.GetDirectoryName(dllFullName));
sb.Append("\\");
//遍历依赖,如果存在dll拖拉加载目录就加入dlls集合
var asse = assemblyAsRef.GetReferencedAssemblies();
for (int i = 0; i < asse.Length; i++)
{
var path = sb.ToString() + asse[i].Name;
var paths = new string[]
{
path + ".dll",
path + ".exe"
};
for (int j = 0; j < paths.Length; j++)
GetAllRefPaths(cadAssembly, cadAssemblyRef, paths[j], dllFullNamesOut);
}
}
/// <summary>
/// 在内存区和映射区找已经加载的程序集
/// </summary>
/// <param name="cadAssembly">程序集_内存区</param>
/// <param name="cadAssemblyRef">程序集_映射区</param>
/// <param name="dllFullName">dll文件位置</param>
/// <returns></returns>
Assembly? GetAssembly(Assembly[] cadAssembly,
Assembly[] cadAssemblyRef,
string? dllFullName)
{
//路径转程序集名
var assName = AssemblyName.GetAssemblyName(dllFullName).FullName;
//在当前程序域的 assemblyAs内存区 和 assemblyAsRef映射区 找这个程序集名
var assemblyAs = cadAssembly.FirstOrDefault(ass => ass.FullName == assName);
//内存区有表示加载过
//映射区有表示查找过,但没有加载(一般来说不存在.只是debug会注释掉 Assembly.Load 的时候用来测试)
if (assemblyAs != null)
return assemblyAs;
//映射区
var assemblyAsRef = cadAssemblyRef.FirstOrDefault(ass => ass.FullName == assName);
//内存区和映射区都没有的话就把dll加载到映射区,用来找依赖表
if (assemblyAsRef != null)
return assemblyAsRef;
var byteRef = File.ReadAllBytes(dllFullName);
if (PatchExtensionLoader)
{
#if HarmonyPatch_1
/* QQ1548253108:这里会报错,他提供了解决方案.
* 方案一:
* 在类上面加 [HarmonyPatch("Autodesk.AutoCAD.ApplicationServices.ExtensionLoader", "OnAssemblyLoad")]
*/
const string ext = "Autodesk.AutoCAD.ApplicationServices.ExtensionLoader";
Harmony hm = new(ext);
hm.PatchAll();
assemblyAsRef = Assembly.ReflectionOnlyLoad(byteRef);
hm.UnpatchAll(ext);
#endif
#if HarmonyPatch_2
//方案二:跟cad耦合了
const string ext = "Autodesk.AutoCAD.ApplicationServices.ExtensionLoader";
var docAss = typeof(Autodesk.AutoCAD.ApplicationServices.Document).Assembly;
var a = docAss.GetType(ext);
var b = a.GetMethod("OnAssemblyLoad");
Harmony hm = new(ext);
hm.Patch(b, new HarmonyMethod(GetType(), "Dummy"));
assemblyAsRef = Assembly.ReflectionOnlyLoad(byteRef);
hm.UnpatchAll(ext);
#endif
}
else
{
/*
* 0x01 此句没有依赖会直接报错
* assemblyAsRef = Assembly.ReflectionOnlyLoad(dllFullName);
* 0x02 重复加载无修改的同一个dll,会出现如下异常:
* System.IO.FileLoadException:
* “API 限制: 程序集“”已从其他位置加载。无法从同一个 Appdomain 中的另一新位置加载该程序集。”
* catch 兜不住的,仍然会在cad上面打印,原因是程序集数组要动态获取(已改)
*/
try
{
assemblyAsRef = Assembly.ReflectionOnlyLoad(byteRef);
}
catch (System.IO.FileLoadException)
{ }
}
return assemblyAsRef;
}
/// <summary>
/// 加载信息
/// </summary>
public static string? PrintMessage(List<LoadState> loadStates)
{
if (loadStates == null)
return null;
var sb = new StringBuilder();
var ok = loadStates.FindAll(a => a.State);
var no = loadStates.FindAll(a => !a.State);
if (ok.Count != 0)
{
sb.Append("** 这些文件加载成功!");
foreach (var item in ok)
{
sb.Append(Environment.NewLine);
sb.Append("++ ");
sb.Append(item.DllFullName);
}
sb.Append(Environment.NewLine);
sb.Append(Environment.NewLine);
}
if (no.Count != 0)
{
sb.Append("** 这些文件已被加载过,同时重复名称和版本号,跳过!");
foreach (var item in no)
{
sb.Append(Environment.NewLine);
sb.Append("-- ");
sb.Append(item.DllFullName);
}
}
return sb.ToString();
}
#endregion
#region 删除文件
/// <summary>
/// Debug的时候删除obj目录,防止占用
/// </summary>
/// <param name="dllFullName">dll文件位置</param>
public void DebugDelObjFiles(string dllFullName)
{
var filename = Path.GetFileNameWithoutExtension(dllFullName);
var path = Path.GetDirectoryName(dllFullName);
var pdb = path + "\\" + filename + ".pdb";
if (File.Exists(pdb))
File.Delete(pdb);
var list = path.Split('\\');
if (list[list.Length - 1] == "Debug" && list[list.Length - 2] == "bin")
{
var projobj = path.Substring(0, path.LastIndexOf("bin")) + "obj";
FileEx.DeleteFolder(projobj);
}
}
#endregion
#region 移动文件
/// <summary>
/// Debug的时候移动obj目录,防止占用
/// </summary>
public void DebugMoveObjFiles(string? dllFullName, Action action)
{
// 临时文件夹_pdb的,无论是否创建这里都应该进行删除
const string Temp = "Temp";
string? temp_Pdb_dest = null;
string? temp_Pdb_source = null;
string? temp_Obj_dest = null; ;
string? temp_Obj_source = null;
try
{
var filename = Path.GetFileNameWithoutExtension(dllFullName);
var path = Path.GetDirectoryName(dllFullName);
//新建文件夹_临时目录
temp_Pdb_dest = path + $"\\{Temp}\\";
//移动文件进去
temp_Pdb_source = path + "\\" + filename + ".pdb";
FileEx.MoveFolder(temp_Pdb_source, temp_Pdb_dest);
//检查是否存在obj文件夹,有就递归移动
var list = path.Split('\\');
if (list[list.Length - 1] == "Debug" && list[list.Length - 2] == "bin")
{
var proj = path.Substring(0, path.LastIndexOf("bin"));
temp_Obj_source = proj + "obj";
temp_Obj_dest = proj + $"{Temp}";
FileEx.MoveFolder(temp_Obj_source, temp_Obj_dest);
}
action.Invoke();
}
finally
{
// 还原移动
FileEx.MoveFolder(temp_Pdb_dest, temp_Pdb_source);
FileEx.DeleteFolder(temp_Pdb_dest);
FileEx.MoveFolder(temp_Obj_dest, temp_Obj_source);
FileEx.DeleteFolder(temp_Obj_dest);
}
}
#endregion
#region Dispose
public bool Disposed = false;
/// <summary>
/// 显式调用Dispose方法,继承IDisposable
/// </summary>
public void Dispose()
{
//由手动释放
Dispose(true);
//通知垃圾回收机制不再调用终结器(析构器)_跑了这里就不会跑析构函数了
GC.SuppressFinalize(this);
}
/// <summary>
/// 析构函数,以备忘记了显式调用Dispose方法
/// </summary>
~AssemblyDependent()
{
Dispose(false); //由系统释放
}
/// <summary>
/// 释放
/// </summary>
/// <param name="ing"></param>
protected virtual void Dispose(bool ing)
{
if (Disposed) return; //不重复释放
Disposed = true;//让类型知道自己已经被释放
CurrentDomainAssemblyResolveEvent -= AssemblyHelper.DefaultAssemblyResolve;
}
#endregion
}
/// <summary>
/// 加载程序集和加载状态
/// </summary>
public struct LoadState
{
public Assembly? Assembly;
public string DllFullName;
public bool State;
public LoadState(string dllFullName, bool state, Assembly? assembly = null)
{
DllFullName = dllFullName;
State = state;
Assembly = assembly;
}
}
public class FileEx
{
/// <summary>
/// 判断含有文件名和后缀
/// </summary>
/// <param name="pathOrFile">路径或者完整文件路径</param>
static bool ContainFileName(string? pathOrFile)
{
// 判断输入的是单文件,它可能不存在
var a = Path.GetDirectoryName(pathOrFile);
var b = Path.GetFileName(pathOrFile);
var c = Path.GetExtension(pathOrFile);
// 是文件
return a.Length > 0 && b.Length > 0 && c.Length > 0;
}
/// <summary>
/// 移动文件夹中的所有文件夹与文件到另一个文件夹
/// </summary>
/// <param name="sourcePathOrFile">源文件夹</param>
/// <param name="destPath">目标文件夹</param>
public static void MoveFolder(string? sourcePathOrFile, string? destPath)
{
if (sourcePathOrFile is null)
throw new ArgumentException(nameof(sourcePathOrFile));
if (destPath is null)
throw new ArgumentException(nameof(destPath));
if (ContainFileName(destPath))
destPath = Path.GetDirectoryName(destPath);
//目标目录不存在则创建
if (!Directory.Exists(destPath))
Directory.CreateDirectory(destPath);
if (ContainFileName(sourcePathOrFile))
{
// 如果是单个文件,就移动到目录就好了
if (File.Exists(sourcePathOrFile))
{
destPath += "\\" + Path.GetFileName(sourcePathOrFile);
File.Move(sourcePathOrFile, destPath);
return;
}
return;
}
// 如果是文件就改为路径
if (!Directory.Exists(sourcePathOrFile))
{
sourcePathOrFile = Path.GetDirectoryName(sourcePathOrFile);
if (!Directory.Exists(sourcePathOrFile))
throw new DirectoryNotFoundException("源目录不存在!");
}
MoveFolder2(sourcePathOrFile, destPath);
}
/// <summary>
/// 移动文件夹中的所有文件夹与文件到另一个文件夹
/// </summary>
/// <param name="sourcePath">源文件夹</param>
/// <param name="destPath">目标文件夹</param>
static void MoveFolder2(string sourcePath, string destPath)
{
//目标目录不存在则创建
if (!Directory.Exists(destPath))
Directory.CreateDirectory(destPath);
//获得源文件下所有文件
var files = new List<string>(Directory.GetFiles(sourcePath));
files.ForEach(c => {
string destFile = Path.Combine(destPath, Path.GetFileName(c));
//覆盖模式
if (File.Exists(destFile))
File.Delete(destFile);
File.Move(c, destFile);
});
//获得源文件下所有目录文件
List<string> folders = new(Directory.GetDirectories(sourcePath));
folders.ForEach(c => {
string destDir = Path.Combine(destPath, Path.GetFileName(c));
//Directory.Move必须要在同一个根目录下移动才有效,不能在不同卷中移动。
//Directory.Move(c, destDir);
//采用递归的方法实现
MoveFolder2(c, destDir);
});
}
/// <summary>
/// 递归删除文件夹目录及文件
/// </summary>
/// <param name="dir"></param>
/// <returns></returns>
public static void DeleteFolder(string? dir)
{
if (dir is null)
throw new ArgumentException(nameof(dir));
if (!Directory.Exists(dir)) //如果存在这个文件夹删除之
return;
foreach (string d in Directory.GetFileSystemEntries(dir))
{
if (File.Exists(d))
File.Delete(d); //直接删除其中的文件
else
DeleteFolder(d); //递归删除子文件夹
}
Directory.Delete(dir, true); //删除已空文件夹
}
}
运行域事件
而其中最重要的是这个事件,它会在运行的时候找已经载入内存上面的程序集.
AppDomain.CurrentDomain.AssemblyResolve += AssemblyHelper.DefaultAssemblyResolve;
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += AssemblyHelper.ReflectionOnlyAssemblyResolve;
封装
namespace IFoxCAD.LoadEx;
using System.Diagnostics;
public class AssemblyHelper {
public static Assembly? ReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs e)
{
var cadAss = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();
return Resolve(cadAss, sender, e);
}
/// <summary>
/// <code>
/// 程序域运行事件
/// 这相当于是dll注入的意思,只是动态加载的这个"dll"不存在实体,只是一段内存.
/// 它总是被 <seealso cref="AppDomain.CurrentDomain.AssemblyResolve"/>事件使用
/// 0x01 动态加载要注意所有的引用外的dll的加载顺序
/// 0x02 指定版本: Acad2008若没有这个事件,会使动态命令执行时候无法引用当前的程序集函数
/// 0x03 目录构成: 动态加载时,dll的地址会在系统的动态目录里,而它所处的程序集(运行域)是在动态目录里.
/// 0x04 命令构成: cad自带的netload会把所处的运行域给改到cad自己的,而动态加载不通过netload,所以要自己去改.
/// </code>
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <returns>程序集如果为空就不会调用</returns>
public static Assembly? DefaultAssemblyResolve(object sender, ResolveEventArgs e)
{
var cadAss = AppDomain.CurrentDomain.GetAssemblies();
return Resolve(cadAss, sender, e);
}
public static Assembly? Resolve(Assembly[] cadAss, object sender, ResolveEventArgs e)
{
// 名称和版本号都一致的,调用它
var result = cadAss.FirstOrDefault(ass => ass.GetName().FullName == e.Name);
if (result != null)
return result;
// 获取名称一致,但是版本号不同的,调用最后的可用版本
var ag = GetAssemblyName(e.Name);
// 获取最后一个符合条件的,
// 否则a.dll引用b.dll函数的时候,b.dll修改重生成之后,加载进去会调用第一个版本的b.dll,
// vs会迭代程序版本号的*,所以最后的可用就是循环到最后的.
for (int i = 0; i < cadAss.Length; i++)
if (GetAssemblyName(cadAss[i].GetName().FullName) == ag)
result = cadAss[i];
if (result == null)
{
// 惊惊: cad21+vs22 容易触发这个资源的问题
// https://stackoverflow.com/questions/4368201/
string[] fields = e.Name.Split(',');
string name = fields[0];
string culture = fields[2];
if (name.EndsWith(".resources") && !culture.EndsWith("neutral"))
return null;
}
if (result == null)
{
var sb = new StringBuilder();
sb.AppendLine($"{nameof(LoadEx)}------------------------------------------------------------");
sb.AppendLine(nameof(DefaultAssemblyResolve) + "出错,程序集无法找到它");
sb.AppendLine("++参数名:: " + GetAssemblyName(e.Name));
sb.AppendLine("++参数完整信息:: " + e.Name);
for (int i = 0; i < cadAss.Length; i++)
sb.AppendLine("-------匹配对象:: " + GetAssemblyName(cadAss[i].GetName().FullName));
sb.AppendLine($"程序集找不到,遇到无法处理的错误,杀死当前进程!");
sb.AppendLine($"{nameof(LoadEx)}------------------------------------------------------------");
Debug.WriteLine(sb.ToString());
Process.GetCurrentProcess().Kill();
//Debugger.Break();
}
return result;
}
static string GetAssemblyName(string argString)
{
return argString.Substring(0, argString.IndexOf(','));
}
}
调试
卸载DLL(20210430补充,同时更改了上面的链式加载)
卸载需要修改工程结构,并且最后发生了一些问题没能解决.
项目结构
- cad主插件工程,引用-->通讯类工程
- 通讯类工程(继承MarshalByRefObject接口的)
- 其他需要加载的子插件工程:cad子插件项目作为你测试加载的dll,里面有一个cad命令gggg,它将会用来验证我们是否成功卸载的关键.
和以往的工程都不一样的是,我们需要复制一份acad.exe目录的所有文件到一个非C盘目录,如下:
修改 主工程
,属性页
:
生成
,输出
: G:\AutoCAD 2008\ <--由于我的工程是.net standard,所以这里将会生成各类net文件夹
调试
,可执行文件
: G:\AutoCAD 2008\net35\acad.exe <--在net文件夹放acad.exe目录所有文件
为什么?因为通讯类.dll必须要在acad.exe旁边,否则无法通讯,会报错,似乎是权限问题,至于有没有其他方法,我不知道...
通讯结构图
程序创建的时候就会有一个主域,然后我们需要在主域上面创建: 新域
然后新域上创建通讯类,利用通讯类在新域进行链式加载,这样程序集都会在新域上面,
这样主域就能够调用新域程序集的方法了.
无法跨 AppDomain 传递 GCHandle
在新域上面创建通讯类--卸载成功,
但是关闭cad程序的时候弹出了报错:
System.ArgumentException:“无法跨 AppDomain 传递 GCHandle。”
我查询这个错误内容,
大部分是讲COM的,而我包内刚好使用了COM.
我将会在这里讨论它:
https://gitee.com/inspirefunction/CadLabelBar/issues/IAI0ZZ
做做实验,.实现一个没引用cad.dll的dll,我用了winform,
是可以卸载,且不会报GC错误.
代码
cad主程序dll工程
命令
using RemoteAccess;
using System.Diagnostics;
using Autodesk.AutoCAD.Runtime;
using Acap = Autodesk.AutoCAD.ApplicationServices.Application;
using Exception = System.Exception;
// 载入dll内的cad的命令
// 1 利用动态编译在主域下增加命令.
// 2 挂未知命令反应器,找不到就去域内找(比较简单,这个实验成功)
namespace JoinBox {
public class LoadAcadDll : IAutoGo {
public Sequence SequenceId() {
return Sequence.Last;
}
public void Terminate() {
// 不可以忽略,因为直接关闭cad的时候是通过这里进行析构,而且优先于析构函数.
// 而析构对象需要本类 _jAppDomain 提供域,否则拿不到.
JJUnLoadAppDomain();
}
/// <summary>
/// 用于和其他域通讯的中间类
/// </summary>
public static JAppDomain _jAppDomain = null;
/// <summary>
/// 命令卸载程序域
/// </summary>
[CommandMethod("JJUnLoadAppDomain")]
public void JJUnLoadAppDomain() {
if (_jAppDomain == null) return;
_jAppDomain.Dispose();
_jAppDomain = null;
}
[CommandMethod("测试无引用cad的dll的命令")]
public void 测试无引用cad的dll的命令() {
if (_jAppDomain == null) return;
// 这样调用是成功的,这个dll没有用到cad的东西,所以GC释放很成功
var aa = _jAppDomain.JRemoteLoader.Invoke("客户端.HelloWorld", "GetTime", new object[] { "我是皮卡丘" });
System.Windows.Forms.MessageBox.Show(aa.ToString());
}
/// <summary>
/// 未知命令就跑其他程序集找然后调用,测试我们卸载之后gggg命令是否仍然有用
/// </summary>
public void CmdUnknown() {
var dm = Acap.DocumentManager;
var md = dm.MdiActiveDocument;
// 反应器->未知命令
md.UnknownCommand += (sender, e) =>
{
if (_jAppDomain == null) return;
// 这里可能产生:不可序列化的错误
// 因为cad域需要和其他域沟通,那么cad域的变量都无法穿透过去
// 所以需要以参数封送到远程通讯类上,再发送到其他域
var globalCommandName = e.GlobalCommandName;
var jrl = _jAppDomain.JRemoteLoader;
jrl?.TraversideLoadAssemblys((jrl2, assembly, ars) => {
try
{
var cmd = ars[0].ToString().ToUpper();
var caddll = new LoadAcadCmds(assembly);
if (caddll.AcadDllInfos.ContainsKey(cmd))
{
var info = caddll.AcadDllInfos[cmd];
var sp = info.Namespace + "." + info.ClassName;
var returnValue = jrl2.Invoke(assembly, sp, cmd);
return true;//returnValue可能无返回值,但是这里仍然结束循环
}
}
catch (Exception e)
{
Debug.WriteLine(e.Message);
}
return null;
}, new object[] { globalCommandName });
};
}
public void Initialize()
{
// 此dll拥有引用的dll,引用的dll,引用的dll....
string dll = @"H:\解决方案\动态加载项目\若海加载项目增加卸载版\ClassLibrary1\bin\Debug\net35\ClassLibrary1.dll";//cad的类,会发生GC穿透
dll = @"H:\解决方案\动态加载项目\若海加载项目增加卸载版\客户端\bin\Debug\客户端.dll";//无调用cad的类,就可以卸载
try
{
_jAppDomain = new JAppDomain("MyJoinBoxAppDomain", dll);
var jrl = _jAppDomain.JRemoteLoader;
// 载入cad的命令之后是否可以在这个域内呢
jrl.LoadAssembly(dll);
//加载不成功就结束掉好了
if (!jrl.LoadOK)
{
Debug.WriteLine(jrl.LoadErrorMessage);
JJUnLoadAppDomain();
return;
}
// 调用方法
object retstr = jrl.Invoke("testa命名空间.MyCommands类", "gggg");
}
catch (System.Exception exception)
{
Debugger.Break();
Debug.WriteLine(exception.Message);
}
CmdUnknown();
}
}
}
反射导出cad命令
using System.Collections.Generic;
using System.Reflection;
using System;
using System.IO;
using Autodesk.AutoCAD.Runtime;
namespace JoinBox
{
[Serializable]
public class LoadAcadCmds
{
public Dictionary<string, LoadAcadCmdsInfo> AcadDllInfos { get; set; }
/// <summary>
/// 反射导出Acad的注册的命令
/// </summary>
/// <param name="dllFileNames">Acad注册的命令的Dll</param>
/// <returns></returns>
public LoadAcadCmds(Assembly ass)
{
AcadDllInfos = new();
var tyeps = new Type[] { };
try
{
//获取类型集合,反射时候还依赖其他的dll就会这个错误
tyeps = ass?.GetTypes();
}
catch (ReflectionTypeLoadException)//反射时候还依赖其他的dll就会这个错误
{ }
foreach (var type in tyeps)
{
if (!type.IsClass || !type.IsPublic)
continue;
foreach (MethodInfo method in type.GetMethods())
{
if (!(method.IsPublic && method.GetCustomAttributes(true).Length > 0))
continue;
CommandMethodAttribute cadAtt = null;
foreach (var att in method.GetCustomAttributes(true))
{
var name = att.GetType()?.Name;
if (name == typeof(CommandMethodAttribute).Name)
cadAtt = att as CommandMethodAttribute;
}
if (cadAtt == null)
continue;
var dllName = Path.GetFileNameWithoutExtension(
ass.ManifestModule.Name.Substring(0, ass.ManifestModule.Name.Length - 4));
var cmdup = cadAtt.GlobalName.ToUpper();
var info = new LoadAcadCmdsInfo
{
DllName = dllName,//不一定有文件名
Namespace = type.Namespace,
ClassName = type.Name,
CmdName = cmdup,
MethodName = method.Name,
};
//将cad命令作为key进行索引
if (!AcadDllInfos.ContainsKey(cmdup))
AcadDllInfos.Add(cmdup, info);
}
}
}
}
/// <summary>
/// 提取cad命令类信息
/// </summary>
[Serializable]
public class LoadAcadCmdsInfo
{
public string DllName;
public string Namespace;
public string ClassName;
public string CmdName;
public string MethodName;
}
}
通讯类dll工程
创建程序域和初始化通讯类JAppDomain
using System;
using System.IO;
using System.Reflection;
namespace RemoteAccess
{
public class JAppDomain : IDisposable
{
/// <summary>
/// 新域
/// </summary>
AppDomain _newAppDomain;
/// <summary>
/// 新域的通讯代理类(通过它和其他程序域沟通)
/// </summary>
public RemoteLoader JRemoteLoader { get; set; }
/// <summary>
/// 程序域的创建和释放
/// </summary>
/// <param name="newAppDomainName">新程序域名</param>
/// <param name="assemblyPlugs">子目录(相对形式)在AppDomainSetup中加入外部程序集的所在目录,多个目录用分号间隔</param>
public JAppDomain(string newAppDomainName, string assemblyPlugs = null)
{
// 如果是文件就转为路径
var ap = assemblyPlugs;
if (!string.IsNullOrEmpty(ap))
ap = Path.GetDirectoryName(ap);
var path = RemoteLoaderTool.GetAssemblyPath(true); //插件目录
path = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(ap) && ap != path)
ap += ";" + path;
// 创建App域
var ads = new AppDomainSetup
{
ApplicationName = newAppDomainName,
// 应用程序根目录
ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
// 子目录(相对形式)在AppDomainSetup中加入外部程序集的所在目录,多个目录用分号间隔
PrivateBinPath = ap ??= path,
};
//设置缓存目录
ads.CachePath = ads.ApplicationBase; //获取或设置指示影像复制是打开还是关闭
ads.ShadowCopyFiles = "true"; //获取或设置目录的名称,这些目录包含要影像复制的程序集
ads.ShadowCopyDirectories = ads.ApplicationBase;
ads.DisallowBindingRedirects = false;
ads.DisallowCodeDownload = true;
ads.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;
#if true4
//从安全策略证据新建程序域(应该是这句导致通讯类无法获取文件)
var adevidence = AppDomain.CurrentDomain.Evidence;
// 创建第二个应用程序域。
AppDomainFactory.JAppDomain = AppDomain.CreateDomain(newAppDomainName, adevidence, ads);
#endif
_newAppDomain = AppDomain.CreateDomain(newAppDomainName, null, ads);
// 遍历程序集,获取指定的程序集 RemoteLoader
string assemblyName = null;
foreach (Assembly ass in AppDomain.CurrentDomain.GetAssemblies())
{
if (ass == typeof(RemoteLoader).Assembly)
{
assemblyName = ass.FullName;
break;
}
}
if (assemblyName == null)
throw new ArgumentNullException(nameof(RemoteLoader) + "程序域不存在");
try
{
#if true2
var masterAppDomain = AppDomain.CurrentDomain;
// 如果通讯类是主域的话表示新域没用,加载进来的东西都在主域.
// 做了个寂寞,释放了空域
JRemoteLoader = masterAppDomain.CreateInstanceAndUnwrap(assemblyName, typeof(RemoteLoader).FullName) as RemoteLoader;
#else
// 在新域创建通讯类,会引发错误: System.ArgumentException:“无法跨 AppDomain 传递 GCHandle。”
// 因为使用了cad的dll,而它的dll用了非托管对象
JRemoteLoader = _newAppDomain.CreateInstanceAndUnwrap(assemblyName, typeof(RemoteLoader).FullName) as RemoteLoader;
#endif
}
catch (Exception)
{
AppDomain.Unload(_newAppDomain);
_newAppDomain = null;
JRemoteLoader = null;
throw new ArgumentNullException(
"需要将*通讯类.dll*扔到acad.exe,而c盘权限太高了," +
"所以直接复制一份acad.exe所有文件到你的主工程Debug目录," +
"调试都改到这个目录上面的acad.exe");
}
// 不用下面的字符串形式,否则改个名就报错了...
//try
//{
// JRemoteLoader = _newAppDomain.CreateInstance(
// RemoteLoaderTool.RemoteAccessNamespace,
// RemoteLoaderTool.RemoteAccessNamespace + ".RemoteLoader")//类名
// .Unwrap() as RemoteLoader;
//}
//catch (Exception e)//报错是否改了 RemoteLoader名称
//{
// throw e;
//}
}
#region Dispose
public bool Disposed = false;
/// <summary>
/// 显式调用Dispose方法,继承IDisposable
/// </summary>
public void Dispose()
{
//由手动释放
Dispose(true);
//通知垃圾回收机制不再调用终结器(析构器)_跑了这里就不会跑析构函数了
GC.SuppressFinalize(this);
}
/// <summary>
/// 析构函数,以备忘记了显式调用Dispose方法
/// </summary>
~JAppDomain()
{
Dispose(false);//由系统释放
}
/// <summary>
/// 释放
/// </summary>
/// <param name="ing"></param>
protected virtual void Dispose(bool ing)
{
if (Disposed) return; //不重复释放
Disposed = true;//让类型知道自己已经被释放
// 系统卸载出错,而手动卸载没出错,因为要留意JRemoteLoader对象在什么域的什么对象上.
if (_newAppDomain != null)
{
JRemoteLoader?.Dispose();
JRemoteLoader = null;
AppDomain.Unload(_newAppDomain);
}
_newAppDomain = null;
GC.Collect();
}
#endregion
}
}
通讯类RemoteLoader
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Remoting;
//参考文章1 https://www.cnblogs.com/zlgcool/archive/2008/10/12/1309616.html
//参考文章2 https://www.cnblogs.com/roucheng/p/csdongtai.html
namespace RemoteAccess
{
[Serializable]
public class AssemblyInfo
{
public Assembly Assembly;
public string Namespace;
public string Class;
public string Method;
public string TypeFullName;
}
/// <summary>
/// 通讯代理类
/// </summary>
[Serializable]
public class RemoteLoader : MarshalByRefObject, IDisposable
{
#region 成员
AssemblyDependent AssemblyDependent;
/// <summary>
/// 链条头的dll加载成功
/// </summary>
public bool LoadOK { get; set; }
/// <summary>
/// 加载的错误信息,可以获取到链条中段的错误
/// </summary>
public string LoadErrorMessage { get; set; }
#endregion
private const BindingFlags bfi = BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance;
public RemoteLoader() { }
/// <summary>
/// 域内进行链式加载dll
/// </summary>
/// <param name="file"></param>
public void LoadAssembly(string sFile)
{
AssemblyDependent = new AssemblyDependent(sFile);
AssemblyDependent.CurrentDomainAssemblyResolveEvent +=
RunTimeCurrentDomain.DefaultAssemblyResolve; //运行域事件保证跨dll的搜索
AssemblyDependent.Load();
LoadOK = AssemblyDependent.LoadOK;
LoadErrorMessage = AssemblyDependent.LoadErrorMessage;
}
/// <summary>
/// 加载cad的东西只能在外面做,
/// 而且远程通讯方法又必须在MarshalByRefObject接口下,
/// 所以这提供遍历加载的程序集们方法
/// </summary>
/// <param name="ac"><see cref="RemoteLoader"/>封送本类|
/// <see cref="Assembly"/>封送程序集|
/// <see cref="object[]"/>封送参数|
/// <see cref="object"/>封送返回值,<see cref="!null"/>结束循环.
/// </param>
/// <param name="ars">外参数传入封送接口</param>
public object TraversideLoadAssemblys(Func<RemoteLoader, Assembly, object[], object> ac, object[] ars = null)
{
object value = null;
if (AssemblyDependent == null)
return value;
foreach (var assembly in AssemblyDependent.MyLoadAssemblys)
{
value = ac.Invoke(this, assembly, ars);
if (value != null)
break;
}
return value;
}
/// <summary>
/// 调用载入的dll的方法
/// </summary>
/// <param name="spaceClass">命名空间+类名</param>
/// <param name="methodName">方法名</param>
/// <param name="args">方法需要的参数</param>
/// <returns></returns>
public object Invoke(string spaceClass, string methodName, params object[] args)
{
if (AssemblyDependent.MyLoadAssemblys.Count < 1)
throw new ArgumentNullException("没构造或加载失败");
var assemblyInfo = GetAssembly(spaceClass, methodName);
if (assemblyInfo == null)
throw new NotSupportedException("找不到指定的命名空间和类名:" + spaceClass);
return Invoke(assemblyInfo.Assembly, assemblyInfo.Namespace + "." + assemblyInfo.Class, assemblyInfo.Method, args);
}
/// <summary>
/// 遍历出"方法"在链条中什么dll上,返回程序集
/// </summary>
/// <param name="className">命名空间+类名</param>
/// <param name="methodName">方法名</param>
/// <param name="typeFullName">返回类型名</param>
/// <returns>程序集</returns>
AssemblyInfo GetAssembly(string className, string methodName)
{
var ta = this.TraversideLoadAssemblys((remoteLoaderFactory, assembly, ars) => {
//获取所有类型
Type[] types = assembly.GetTypes();
foreach (var type in types)
{
if (!type.IsClass || !type.IsPublic)
continue;
if (type.Namespace + "." + type.Name != className)
continue;
foreach (MethodInfo method in type.GetMethods())
{
if (!method.IsPublic)
continue;
// cad可以用这个,属性名称
//if (method.GetCustomAttributes(true).Length == 0)
//{
// continue;
//}
// 转大写匹配命令
if (method.Name.ToUpper() == methodName.ToUpper())
{
var asInfo = new AssemblyInfo
{
TypeFullName = type.FullName,
Assembly = assembly,
Namespace = type.Namespace,
Class = type.Name,
Method = method.Name,
};
return asInfo;
}
}
}
return null;
});
return (AssemblyInfo)ta;
}
public object Invoke(Assembly assembly, string spaceClass, string methodName, params object[] args)
{
if (assembly == null)
throw new ArgumentNullException("没程序域");
Type spaceClassType = assembly.GetType(spaceClass);
if (spaceClassType == null)
throw new ArgumentNullException("命名空间.类型出错");
// 转大写匹配命令(如果是方法的话,这里可能有重载)
List<MethodInfo> methodInfos = new();
foreach (var item in spaceClassType.GetMethods())
if (item.Name.ToUpper() == methodName.ToUpper())
methodInfos.Add(item);
if (methodInfos.Count == 0)
throw new ArgumentNullException("方法出错");
object spaceClassInstance = Activator.CreateInstance(spaceClassType);
object returnValue = null;
foreach (var methodInfo in methodInfos)
{
try
{
// 此句若出错表示运行域不在准确的域内,要去检查一下,此句也会导致GC释放出错
// 没有参数
returnValue = methodInfo.Invoke(spaceClassInstance, args);
// 参数1,重载
// returnValue = methodInfo.Invoke(spaceClassInstance, new object[] { "fsdfasfasf" });
}
catch
{ }
}
// 调用方式改变(但是这个方法需要 Assembly.LoadForm 否则无法查找影像文件)
// return Activator.CreateInstanceFrom(assembly.FullName, spaceClass + "." + methodName, false, bfi, null, args, null, null, null).Unwrap();
return returnValue;
}
#region Dispose
public bool Disposed = false;
/// <summary>
/// 显式调用Dispose方法,继承IDisposable
/// </summary>
public void Dispose()
{
//由手动释放
Dispose(true);
//通知垃圾回收机制不再调用终结器(析构器)_跑了这里就不会跑析构函数了
GC.SuppressFinalize(this);
}
/// <summary>
/// 析构函数,以备忘记了显式调用Dispose方法
/// </summary>
~RemoteLoader()
{
Dispose(false);//由系统释放
}
/// <summary>
/// 释放
/// </summary>
/// <param name="ing"></param>
protected virtual void Dispose(bool ing)
{
if (Disposed) return;//不重复释放
Disposed = true;//让类型知道自己已经被释放
if (AssemblyDependent == null) return;
AssemblyDependent.CurrentDomainAssemblyResolveEvent -=
RunTimeCurrentDomain.DefaultAssemblyResolve; //运行域事件保证跨dll的搜索
}
#endregion
}
}
通讯类RemoteLoader其余
- 本文的
cad主插件项目
-链式加载
- 本文的
cad主插件项目
-运行域事件
(完)