需求
应用需求1
我们cad.net开发都会面临一个问题,加载了的dll无法实现覆盖操作,也就是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重载,也就无法拷贝到内存中去,故此不考虑.
如果手写过IOC容器的,应该对以上两个函数非常的熟悉才对.
我问了群里的大佬南胜写了一篇文章回答了我,但是我用他的代码出现了几个问题:
- 他获取的路径是clr寻找路径之一,我需要改到加载路径上面的...这里各位可以自行去看看clr的寻找未知dll的方式.
- 以及他只支持一个引用的dll,而我需要知道引用的引用的引用的引用的引用的引用的引用的引用的引用...的dll.
所以需要对他的代码修改一番.
工程开始
项目结构
首先,一共有四个项目,
- cad主插件项目:直接netload的项目.
- cad次插件:testa,testb [给a引用],testc [给b引用],后面还有套娃也可以...
cad子插件项目
testa项目代码
namespace testa
{
public class MyCommands
{
[CommandMethod("testa")]
public static void testa()
{
Document doc = Acap.DocumentManager.MdiActiveDocument;
Editor ed;
if (doc != null)
{
ed = doc.Editor;
ed.WriteMessage("\r\n自带函数testa.");
}
}
[CommandMethod("gggg")]
public void gggg()
{
Document doc = Acap.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
if (doc != null)
{
ed.WriteMessage("\r\n **********gggg");
testb.MyCommands.TestBHello();
}
}
}
}
testb项目代码
namespace testb
{
public class MyCommands
{
public static void TestBHello()
{
Document doc = Acap.DocumentManager.MdiActiveDocument;
Editor ed;
if (doc != null)
{
ed = doc.Editor;
ed.WriteMessage("************testb的Hello");
testc.MyCommands.TestcHello();
}
}
[CommandMethod("testb")]
public static void testb()
{
Document doc = Acap.DocumentManager.MdiActiveDocument;
Editor ed;
if (doc != null)
{
ed = doc.Editor;
ed.WriteMessage("\r\n自带函数testb.");
}
}
}
}
testc项目代码
namespace testc
{
public class MyCommands
{
public static void TestcHello()
{
Document doc = Acap.DocumentManager.MdiActiveDocument;
Editor ed;
if (doc != null)
{
ed = doc.Editor;
ed.WriteMessage("************testc的Hello");
}
}
[CommandMethod("testc")]
public static void testc()
{
Document doc = Acap.DocumentManager.MdiActiveDocument;
Editor ed;
if (doc != null)
{
ed = doc.Editor;
ed.WriteMessage("\r\n自带函数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的路径,再调用传给加载函数就好了.
调用方法
var ad = new AssemblyDependent(path);
var msg = ad.Load();
bool allyes = true;
foreach (var item in msg)
{
if (!item.LoadYes)
{
ed.WriteMessage(Environment.NewLine + "**" + item.Path +
Environment.NewLine + "**此文件已加载过,重复名称,重复版本号,本次不加载!" +
Environment.NewLine);
allyes = false;
}
}
if (allyes)
{
ed.WriteMessage(Environment.NewLine + "**链式加载成功!" + Environment.NewLine);
}
链式加载
飞诗帮我修改了其中一些bug,感谢.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
namespace RemoteAccess
{
[Serializable]
public class AssemblyDependent : IDisposable
{
string _dllFile;
/// <summary>
/// cad程序域依赖_内存区(不可以卸载)
/// </summary>
private Assembly[] _cadAs;
/// <summary>
/// cad程序域依赖_映射区(不可以卸载)
/// </summary>
private Assembly[] _cadAsRef;
/// <summary>
/// 加载DLL成功后获取到的程序集
/// </summary>
public List<Assembly> MyLoadAssemblys { get; private set; }
/// <summary>
/// 当前域加载事件,运行时出错的话,就靠这个事件来解决
/// </summary>
public event ResolveEventHandler CurrentDomainAssemblyResolveEvent
{
add
{
AppDomain.CurrentDomain.AssemblyResolve += value;
}
remove
{
AppDomain.CurrentDomain.AssemblyResolve -= value;
}
}
/// <summary>
/// 链式加载dll依赖
/// </summary>
/// <param name="dllFile"></param>
public AssemblyDependent(string dllFile)
{
_dllFile = Path.GetFullPath(dllFile);//相对路径要先转换 Path.GetFullPath(dllFile);
//cad程序集的依赖
_cadAs = AppDomain.CurrentDomain.GetAssemblies();
//映射区
_cadAsRef = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();
//被加载的都存放在这里
MyLoadAssemblys = new List<Assembly>();
}
/// <summary>
/// 返回的类型,描述加载的错误
/// </summary>
public class LoadDllMessage
{
public string Path;
public bool LoadYes;
public LoadDllMessage(string path, bool loadYes)
{
Path = path;
LoadYes = loadYes;
}
public override string ToString()
{
if (LoadYes)
{
return "加载成功:" + Path;
}
return "加载失败:" + Path;
}
}
/// <summary>
/// 加载信息集合
/// </summary>
List<LoadDllMessage> LoadYesList;
bool _byteLoad;
/// <summary>
/// 加载程序集
/// </summary>
/// <param name="byteLoad">true字节加载,false文件加载</param>
/// <returns>返回加载链的</returns>
public void Load(bool byteLoad = true)
{
_byteLoad = byteLoad;
if (!File.Exists(_dllFile))
{
throw new ArgumentNullException("路径不存在");
}
LoadYesList = new List<LoadDllMessage>();
//查询加载链之后再逆向加载,确保前面不丢失
var allRefs = GetAllRefPaths(_dllFile);
allRefs.Reverse();
foreach (var path in allRefs)
{
try
{
//路径转程序集名
string assName = AssemblyName.GetAssemblyName(path).FullName;
//路径转程序集名
var assembly = _cadAs.FirstOrDefault(a => a.FullName == assName);
if (assembly != null)
{
LoadYesList.Add(new LoadDllMessage(path, false));//版本号没变不加载
continue;
}
byte[] buffer = null;
bool flag = true;
//实现字节加载
if (path == _dllFile)
{
LoadOK = true;
}
#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) && _byteLoad)
{
var pdbbuffer = File.ReadAllBytes(pdbFullName);
buffer = File.ReadAllBytes(path);
var ass = Assembly.Load(buffer, pdbbuffer);
MyLoadAssemblys.Add(ass);
flag = false;
}
#endif
if (flag)
{
Assembly ass = null;
if (_byteLoad)
{
buffer = File.ReadAllBytes(path);
ass = Assembly.Load(buffer);
}
else
{
ass = Assembly.LoadFile(path);
}
MyLoadAssemblys.Add(ass);
}
LoadYesList.Add(new LoadDllMessage(path, true));//加载成功
}
catch
{
LoadYesList.Add(new LoadDllMessage(path, false));//错误造成
}
}
MyLoadAssemblys.Reverse();
}
//链条后面的不再理会,因为相同的dll引用辨识无意义
/// <summary>
/// 第一个dll加载是否成功
/// </summary>
public bool LoadOK { get; private set; }
/// <summary>
/// 加载出错信息
/// </summary>
public string LoadErrorMessage
{
get
{
var sb = new StringBuilder();
bool allyes = true;
foreach (var item in LoadYesList)
{
if (!item.LoadYes)
{
sb.Append(Environment.NewLine + "** 此文件已加载过,重复名称,重复版本号,本次不加载!");
sb.Append(Environment.NewLine + item.ToString());
sb.Append(Environment.NewLine);
allyes = false;
}
}
if (allyes)
{
sb.Append(Environment.NewLine + "** 链式加载成功!");
sb.Append(Environment.NewLine);
}
return sb.ToString();
}
}
/// <summary>
/// 获取加载链
/// </summary>
/// <param name="dll"></param>
/// <param name="dlls"></param>
/// <returns></returns>
List<string> GetAllRefPaths(string dll, List<string> dlls = null)
{
if (dlls == null)
{
dlls = new List<string>();
}
if (dlls.Contains(dll) || !File.Exists(dll))
{
return dlls;
}
dlls.Add(dll);
//路径转程序集名
string assName = AssemblyName.GetAssemblyName(dll).FullName;
//在当前程序域的assemblyAs内存区和assemblyAsRef映射区找这个程序集名
Assembly assemblyAs = _cadAs.FirstOrDefault(a => a.FullName == assName);
Assembly assemblyAsRef;
//内存区有表示加载过
//映射区有表示查找过但没有加载(一般来说不存在.只是debug会注释掉Assembly.Load的时候用来测试)
if (assemblyAs != null)
{
assemblyAsRef = assemblyAs;
}
else
{
assemblyAsRef = _cadAsRef.FirstOrDefault(a => a.FullName == assName);
//内存区和映射区都没有的话就把dll加载到映射区,用来找依赖表
if (assemblyAsRef == null)
{
// assemblyAsRef = Assembly.ReflectionOnlyLoad(dll); 没有依赖会直接报错
var byteRef = File.ReadAllBytes(dll);
assemblyAsRef = Assembly.ReflectionOnlyLoad(byteRef);
}
}
//遍历依赖,如果存在dll拖拉加载目录就加入dlls集合
foreach (var assemblyName in assemblyAsRef.GetReferencedAssemblies())
{
//dll拖拉加载路径-搜索路径(可以增加到这个dll下面的所有文件夹?)
string directoryName = Path.GetDirectoryName(dll);
var path = directoryName + "\\" + assemblyName.Name;
var paths = new string[]
{
path + ".dll",
path + ".exe"
};
foreach (var patha in paths)
{
GetAllRefPaths(patha, dlls);
}
}
return dlls;
}
/// <summary>
/// 递归删除文件夹目录及文件
/// </summary>
/// <param name="dir"></param>
/// <returns></returns>
static void DeleteFolder(string dir)
{
if (Directory.Exists(dir)) //如果存在这个文件夹删除之
{
foreach (string d in Directory.GetFileSystemEntries(dir))
{
if (File.Exists(d))
File.Delete(d); //直接删除其中的文件
else
DeleteFolder(d); //递归删除子文件夹
}
Directory.Delete(dir, true); //删除已空文件夹
}
}
/// <summary>
/// Debug的时候删除obj目录,防止占用
/// </summary>
public void DebugDelObjFiles()
{
try
{
var filename = Path.GetFileNameWithoutExtension(_dllFile);
var path = Path.GetDirectoryName(_dllFile);
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 bin = path.LastIndexOf("bin");
var proj = path.Substring(0, bin);
var obj = proj + "obj";
DeleteFolder(obj);
}
}
catch
{ }
}
#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;
GC.Collect();
}
#endregion
}
}
运行域事件
而其中最重要的是这个事件,它会在运行的时候找已经载入内存上面的程序集.
AppDomain.CurrentDomain.AssemblyResolve += RunTimeCurrentDomain.DefaultAssemblyResolve;
封装
using System;
using System.Linq;
using System.Reflection;
namespace JoinBoxCurrency
{
public static class RunTimeCurrentDomain
{
#region 程序域运行事件
// 动态编译要注意所有的引用外的dll的加载顺序
// cad2008若没有这个事件,会使动态命令执行时候无法引用当前的程序集函数
// 跨程序集反射
// 动态加载时,dll的地址会在系统的动态目录里,而它所处的程序集(运行域)是在动态目录里.
// netload会把所处的运行域给改到cad自己的,而动态编译不通过netload,所以要自己去改.
// 这相当于是dll注入的意思,只是动态编译的这个"dll"不存在实体,只是一段内存.
/// <summary>
/// 程序域运行事件
/// </summary>
public static Assembly DefaultAssemblyResolve(object sender, ResolveEventArgs args)
{
var cad = AppDomain.CurrentDomain.GetAssemblies();
/*获取名称和版本号都一致的,调用它*/
Assembly load = null;
load = cad.FirstOrDefault(a => a.GetName().FullName == args.Name);
if (load == null)
{
/*获取名称一致,但是版本号不同的,调用最后的可用版本*/
var ag = args.Name.Split(',')[0];
//获取 最后一个符合条件的,
//否则a.dll引用b.dll函数的时候,b.dll修改重生成之后,加载进去会调用第一个版本的b.dll
foreach (var item in cad)
{
if (item.GetName().FullName.Split(',')[0] == ag)
{
//为什么加载的程序版本号最后要是*
//因为vs会帮你迭代这个版本号,所以最后的可用就是循环到最后的.
load = item;
}
}
}
return load;
}
#endregion
}
}
关于动态加载和动态编译是有相通的部分的,而这个部分就是这个事件...
Acad2008若没有这个事件,会使动态编译的命令,在执行时候无法引用当前的程序集函数.
netload会把所处的运行域给改到cad自己的,而动态编译不通过 netload,所以要自己去改.
调试
卸载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旁边,否则无法通讯,会报错,至于有没有其他方法,我不知道...
通讯结构图
程序创建的时候就会有一个主域,然后我们需要在主域上面创建: 新域
然后新域上创建通讯类,利用通讯类在新域进行链式加载,这样程序集都会在新域上面,
这样主域就能够调用新域程序集的方法了.
GC无法穿透释放的BUG
在新域上面创建通讯类--卸载成功,但是关闭cad程序的时候弹出了报错:
System.ArgumentException:“无法跨 AppDomain 传递 GCHandle。”
猜测是因为cad的dll调用了非托管对象,所以有内存等待GC释放,从而导致此错误.
而这个GC是cad内部构建的,没有提供给二次开发操作(即使是Arx),自然无法干掉它了.
为了验证,实现一个没引用cad.dll的dll,是可以卸载,且不会报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 { get; set; }
public string Namespace { get; set; }
public string ClassName { get; set; }
public string CmdName { get; set; }
public string MethodName { get; set; }
}
}
通讯类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 { get; set; }
public string Namespace { get; set; }
public string Class { get; set; }
public string Method { get; set; }
public string TypeFullName { get; set; }
}
/// <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)
{
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主插件项目 - 运行域事件
(完)