需求
應用需求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主插件項目 - 運行域事件
(完)