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重载,也就无法拷贝到内存中去,故此不考虑.

img

南胜写了一篇文章回答了,但是他代码出现了几个问题:

  • 他获取的路径是clr寻找路径之一,我需要改到加载路径上面的.
    各位自行去看看clr的寻找未知dll的方式.
  • 以及他只支持一个引用的dll,而我需要知道引用的引用...的dll.
    所以对他的代码修改一番.

工程开始

项目地址在这里,我博客只是带你慢慢看.
https://gitee.com/inspirefunction/CadLabelBar/tree/loadx/若海加载项目增加卸载版

项目结构

首先,共有四个项目.

  1. cad主插件项目:直接netload的项目.
  2. cad次插件:testa,testb [给a引用],testc [给b引用],后面还有套娃也可以...
graph TD netload命令加载-->cad主插件-->加载-->cad次插件 cad次插件testA-->引用-->testB-->testC-->test....

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
img

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(','));
    }
}

调试

另见 cad.net dll动态加载之后如何调试

卸载DLL(20210430补充,同时更改了上面的链式加载)

卸载需要修改工程结构,并且最后发生了一些问题没能解决.

项目结构

  1. cad主插件工程,引用-->通讯类工程
  2. 通讯类工程(继承MarshalByRefObject接口的)
  3. 其他需要加载的子插件工程: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旁边,否则无法通讯,会报错,似乎是权限问题,至于有没有其他方法,我不知道...

通讯结构图

graph TD cad主插件工程-->主域-->新域-->创建域-->创建通讯类-->通讯类 通讯类-->执行链式加载子插件工程-->新域程序集 通讯类-->新域程序集 新域-->卸载域-->破坏通讯类-->通讯类 主域-->跨域执行方法-->检查通讯类生命-->通讯类

程序创建的时候就会有一个主域,然后我们需要在主域上面创建: 新域
然后新域上创建通讯类,利用通讯类在新域进行链式加载,这样程序集都会在新域上面,
这样主域就能够调用新域程序集的方法了.

无法跨 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其余

  1. 本文的 cad主插件项目 - 链式加载
  2. 本文的 cad主插件项目 - 运行域事件
    (完)
posted @ 2020-10-18 03:58  惊惊  阅读(8653)  评论(1编辑  收藏  举报