AppDomain 和动态加载

2023-11-15

应用程序体系结构

在我专攻代码之前,我想谈谈我尝试做的事。您可能记得,SuperGraph 让您从函数列表中进行选择。我希望能够在具体的目录中放置外接程序程序集,让 SuperGraph 检测它们,加载它们,并找到它们中包含的所有函数。

如果 SuperGraph 自己能完成此操作则不需要单独的 AppDomainAssembly.Load() 通常运行良好,但程序集无法独立卸载(只有 AppDomain 可以卸载)。这意味着如果您正在编写服务器,而且您希望用户无需启动和停止服务器即能更新他们的外接程序,那么您将无法使用默认的 AppDomain 实现此任务。

要实现此功能,我们将在一个独立的 AppDomain 中加载所有外接程序程序集。当添加或修改文件时,我们将卸载 AppDomain,创建新的 AppDomain,然后将当前文件加载到其中。这样,一切就都完美无缺了。

为了把这个讲得更明白一点,我创建了一个典型方案,如图 1 所示。

图 1:典型的 AppDomain 方案

在这个图表中,Loader 类创建一个名为 Functions 的新 AppDomain。创建 AppDomain 之后,Loader 在新的 AppDomain 中创建 RemoteLoader 的实例。

要加载程序集,请在 RemoteLoader 上调用加载函数。该函数打开新的程序集,找到程序集中的所有函数,将函数打包到 FunctionList 对象中,然后将该对象返回到 Loader。然后,就可以通过 Graph 函数使用此 FunctionList 中的 Function 对象。

创建 AppDomain

第一项任务是创建 AppDomain。要以正确的方式创建 AppDomain,我们需要向 AppDomain 传递一个 AppDomainSetup 对象。一旦您理解了这一切的工作原理,关于这些的文档就足够使用了,但是如果您正在试图理解其工作原理,那么这些文档的帮助并不大。当关于该主题的 Google 搜索将上个月的专栏作为较高的匹配之一返回时,我怀疑我可能有点麻烦了。

必须处理的基本问题是如何在运行时加载程序集。默认情况下,运行时将查看全局程序集缓存或当前应用程序目录树。而我们希望从完全不同的目录中加载我们的外接程序。

当您查看 AppDomainSetup 的文档时,您将发现可以把 ApplicationBase 属性设置为要搜索程序集的目录。然而,我们也需要参考原始的程序目录,因为那是 RemoteLoader 类存在的地方。

AppDomain 的创作者们理解这一点,因此他们已经提供了额外的位置,用于从中搜索程序集。我们将使用 ApplicationBase 引用外接程序目录,然后将 PrivateBinPath 设置为指向主应用程序目录。

下面是来自 Loader 类的代码,可实现此功能:

AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationBase = functionDirectory;
setup.PrivateBinPath = AppDomain.CurrentDomain.BaseDirectory;
setup.ApplicationName = "Graph";
appDomain = AppDomain.CreateDomain("Functions", null, setup);

remoteLoader = (RemoteLoader)
appDomain.CreateInstanceFromAndUnwrap("SuperGraph.exe",
"SuperGraphInterface.RemoteLoader");

创建 AppDomain 之后,使用 CreateInstanceFromAndUnwrap() 函数在新的应用程序域中创建 RemoteLoader 类的实例。请注意,需要使用类所在的程序集的文件名以及类的全名。

当执行此调用时,我们返回如同 RemoteLoader 一样的实例。实际上,它是一个小型代理类,将所有调用转发到其他 AppDomain 中的 RemoteLoader 实例中。这和 .NET Remoting 使用的是同一种结构。

程序集绑定日志查看器

当您编写代码实现此功能时,您会产生错误。 本文档对如何调试应用程序并未提供什么建议,但是如果您知道该向谁询问,他们将告诉您有关程序集绑定日志查看器(名为“fuslogvw.exe”,因为 加载子系统称为“fusion”)的信息。运行查看器时,您可以要求它记录故障,然后当您运行的应用程序出现加载程序集的问题时,您可以刷新查看器,获得 当前情况的详细信息。

例如,您会发现 Assembly.Load() 的文件名末尾不需要 .dll,这一点非常有用。您可以从日志中获知这一点,因为它将告诉您它曾试图加载 f.dll.dll

动态加载程序集

因此,既然我们已经创建了应用程序域,下一步应该搞清楚如何加载组件并从中提取函数。这需要两段相互独立的代码。第一段代码在目录中查找文件,然后加载找到的每个文件:

void LoadUserAssemblies()
{
availableFunctions = new FunctionList();
LoadBuiltInFunctions();

DirectoryInfo d = new DirectoryInfo(functionAssemblyDirectory);
foreach (FileInfo file in d.GetFiles("*.dll"))
{
string filename = file.Name.Replace(file.Extension, "");
FunctionList functionList = loader.LoadAssembly(filename);

availableFunctions.Merge(functionList);
}
}

Graph 类中的函数在外接程序目录中查找所有的 dll 文件,删除它们的扩展名,然后告诉加载程序加载它们。返回的函数列表将并入当前的函数列表。

第二段代码在 RemoteLoader 类中,它实际加载程序集并查找函数:

public FunctionList LoadAssembly(string filename)
{
FunctionList functionList = new FunctionList();
Assembly assembly = AppDomain.CurrentDomain.Load(filename);

foreach (Type t in assembly.GetTypes())
{
functionList.AddAllFromType(t);
}
return functionList;
}

这段代码只是对传入的文件名(实际是程序集名称)调用 Assembly.Load(),然后将所有有用的函数加载到 FunctionList 实例中返回给调用程序。

此时,应用程序可以启动,加载外接程序程序集,然后用户就可以引用它们。

重新加载程序集

下一项任务是能够按照需要重新加载这些程序集。最终,我们希望能够自动实现该任务,但是出于测试目的,我将 Reload 按钮添加到窗体中,以使程序集能够重新加载。该按钮的处理程序仅调用 Graph.Reload(),它需要执行以下操作:

    • 卸载 AppDomain
    • 创建新的 AppDomain
    • 在新的 AppDomain 中重新加载程序集。
    • 将图形线条挂钩到新创建的 AppDomain

步骤 4 是必需的,因为 GraphLine 对象包含来自原 AppDomainFunction 对象。卸载 AppDomain 后,函数对象无法再被使用。

为解决此问题,HookupFunctions() 修改了 GraphLine 对象,使它们从当前应用程序域指向正确的函数。

代码如下:

loader.Unload();
loader = new Loader(functionAssemblyDirectory);
LoadUserAssemblies();
HookupFunctions();
reloadCount++;

if (this.ReloadCountChanged != null)
ReloadCountChanged(this, new ReloadEventArgs(reloadCount));

只要执行重新加载操作,最后两行将引发一个事件。其作用是更新窗体上的重新加载计数器。

检测新的程序集

下一步是能够检测在外接程序目录中显示的新的或修改过的程序集。该框架提供 FileSystemWatcher 类来实现此功能。下面是我添加到 Graph 类构造函数中的代码:

watcher = new FileSystemWatcher(functionAssemblyDirectory, "*.dll");
watcher.EnableRaisingEvents = true;
watcher.Changed += new FileSystemEventHandler(FunctionFileChanged);
watcher.Created += new FileSystemEventHandler(FunctionFileChanged);
watcher.Deleted += new FileSystemEventHandler(FunctionFileChanged);

当创建 FileSystemWatcher 类时,我们告诉它要在什么目录中查找,要跟踪哪些文件。EnableRaisingEvents 属性表示当它检测到更改时,我们是否需要它发送事件。最后 3 行将事件挂钩到类中的某个函数。该函数仅仅调用 Reload() 以重新加载程序集。

这种方法有一些累赘的地方。在更新程序集时,我们必须卸载程序集才能够加载新的版本,但是添加或删除文件时不需要卸载程序集。在这种情况下,对所有更改执行此操作的成本并不是很高,而且它使代码更简单。

在构造此代码之后,我们运行该应用程序,然后尝试把新的程序集复制到外接程序目录中。正如我们所希望的那样,我们获得了文件更改事件,当重新加载完毕时,新的函数就可供使用。

然而,当我们试图更新现有的程序集时,我们遇到了一个问题。运行时已经锁定该文件,这意味着我们无法将新的程序集复制到外接程序目录中,我们收到一个错误。

AppDomain 类的设计人员意识到这是一个问题,因此他们提供一种不错的解决方法。当 ShadowCopyFiles 属性设置为 true(字符串 true,不是布尔常数 true。不要问我为什么……)时,运行时将把程序集复制到缓存目录中,然后打开该程序集。这样,原文件就不会被锁定,我们也就能更新正在使用的程序集。ASP.NET 使用了这种机制。

为了启用此功能,我在 Loader 类的构造函数中添加了以下行:

setup.ShadowCopyFiles = "true";

然后我重新生成了该应用程序,并得到相同的错误。我查看了 ShadowCopyDirectories 属性的文档,该文档明确指出 PrivateBinPath 指定的所有目录(包括 ApplicationBase 指定的目录)是阴影复制的(如果未设置此属性)。记得我是如何说该文档在这个方面不是很好的吗?

有关此属性的文档肯定是错了。我没有验证确切的表现方式,但是我可以告诉您 ApplicationBase 目录中的文件在默认情况下并不是阴影复制的。明确指定目录可以解决此问题:

setup.ShadowCopyDirectories = functionDirectory;

搞明白这一点至少花了我半个小时。

现在我们可以更新现有文件并将其正确地加载进去。可我刚把这个理顺,又遇到了另外一个小的问题。当我们从窗体的按钮上运行重新加载函数时,重新加载总是和绘制发生在同一个线程中,这意味着在重新加载过程中我们不可能尝试绘制直线。

既然我们已经切换到文件更改事件,那么在卸载 AppDomain 之后和加载新的 AppDomain 之前,有可能会进行绘制。如果发生这种情况,我们会得到一个异常。

这是传统的多线程编程问题,使用 C# lock 语句很容易处理。我在绘图函数和重新加载函数中添加了 lock 语句,这就确保了它们不会同时发生。这就解决了该问题,添加程序集的更新版本将使程序自动切换到函数的新版本。这相当不错。

还有一个奇怪的现象。原来用于检测文件更改的 Win32® 函数发送的更改数量很大,因此对文件做一次更新将发送五个更改事件,程序集也将被重新加载五次。解决方法是编写更智能的、可以将这些操作组合在一起的 FileSystemWatcher,但是此版本中没有提供这种解决方法。

拖放

将文件复制到目录中不是很方便,因此我决定在该应用程序中添加拖放功能。实现该任务的第一步是把窗体的 AllowDrop 属性设置为 true,这将打开拖放功能。下一步,我将一个例程挂钩到 DragEnter 事件。当光标在对象上移动进行拖放操作以确定当前对象是否接受拖放时,将调用该事件。

private void Form1_DragEnter(
object sender, System.Windows.Forms.DragEventArgs e)
{
object o = e.Data.GetData(DataFormats.FileDrop);
if (o != null)
{
e.Effect = DragDropEffects.Copy;
}
string[] formats = e.Data.GetFormats();
}

在此处理程序中,我查看是否有可用的 FileDrop 数据(也就是说,文件被拖放到窗口中)。如果有,我把效果设置为“复制”,这将相应地设置光标,并且如果用户释放鼠标按钮,将发送 DragDrop 事件。该函数中的最后一行完全是出于调试目的,用于查看操作中有哪些可用信息。

下一项任务是为 DragDrop 事件编写处理程序:

private void Form1_DragDrop(
object sender, System.Windows.Forms.DragEventArgs e)
{
string[] filenames = (string[]) e.Data.GetData(DataFormats.FileDrop);
graph.CopyFiles(filenames);
}

此例程获得与此操作关联的数据(文件名数组),将其传递到图形函数,然后图形函数把文件复制到外接程序目录中,触发文件更改事件以便重新加载它们。

状态

此时,您可以运行该应用程序,把新的程序集拖到程序上,程序将很快加载它们并保持运行。这相当不错。

 
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

AppDomain 和动态加载 的相关文章

随机推荐

  • 别不把自己当回事

    人活着要有尊严 哪怕现在的社会有多么的现实 没有尊严的人生是枯燥乏味的 连你自己都不把自己当回事 谁还会把你当回事 路是自己走的 事情是自己做的 不要别人的施舍 也会走到自己的成功 别什么事都依赖别人 别总是想从别人那里得到什么 因为别人的
  • Visio绘图中遇到的一些问题及相应解决方法(持续更新)

    1 为什么按下方向键后 选中的目标没有移动 而绘图界面却在移动 解决方法 检查键盘的SL Scroll Lock 键是否被按下 键盘上对应的指示灯是否被点亮 SL键被激活 按下方向键等同于鼠标拖动绘图区右侧或下方的滚动条 2 Visio在编
  • Lua :操作符很简单,算数、关系、逻辑和其他

    目录 1 算数运算符 2 关系运算符 3 逻辑运算符 4 其他运算符 Lua中操作符可以划分为三种 算数运算符 关系运算符 逻辑运算符和其他运算符 1 算数运算符 算数运算符 加法 减法 乘法 除法 取余 乘幂 负号 do local a
  • 根据字符串,对数据进行排序

    后端返回数据 const data mc 苹果 num 6 mc 香蕉 num 31 mc 樱桃 num 1 mc 橘子 num 22 mc 橙子 num 2 排序 const sortOrder 橙子 樱桃 苹果 香蕉 橘子 data s
  • Python字典的使用

    1 有如下学生的成绩信息 s1 姓名 乔峰 班级 1班 数学 88 语文 87 英语 90 s2 姓名 段誉 班级 2班 数学 98 语文 77 英语 95 s3 姓名 阿朱 班级 1班 数学 78 语文 83 英语 80 s4 姓名 阿紫
  • 二分查找(BinarySearch)

    尽管二分查找的基本思想相对简单 但细节可以令人难以招架 高德纳 文章目录 一 常见问题 整数溢出问题 区间选取问题 二 完整例子 一 常见问题 当年 乔恩 本特利将二分搜索问题布置给专业编程课的学生时 百分之90的学生在花费数小时后还是无法
  • yum install XX 失败: Error Downloading Packages

    yum clean all 清除缓存目录下的软件包及旧的headers yum list 重新列出所有已经安装和可以安装的软件包 然后在执行yum install xx 就行了
  • 关于Springboot引入Jython调用python的一个相对全面的解决方案

    最近在研究Jython调用python 总结一下可行方案 引入jar包
  • 西门子中压交流变频器——GL150介绍分享

    1 西门子中压交流变频器 GL150 GL150 LCI变频器 负载换向逆变器 可以让SINAMICS驱动器系统实现更高额定功率 GL150设计做可变转矩和恒定转矩特性的单电机应用的驱动器变频器 这种变频器有空冷和水冷设计型号 更高额定功率
  • Ajax在返回集合后,数据到复杂表格的应用

    通常 我们无论是用普通Ajax机制还是利用框架 在处理返回的问题上 都会遇到这样的问题 如 我们要将一个List
  • Cobbler 登录web界面提示报错“Internal Server Error”

    第一部分直接转载摘录JasonMingHao的博客 Cobbler 登录web界面提示报错 Internal Server Error 来说明问题哈 在访问cobbler web界面到时候出现以下提示 ssl的报错日志如下 root Cob
  • buck-boost电路计算

    以下内容来自唐老师讲电赛 各大公司的计算器 链接 https pan baidu com s 1dhKB G3no0AHLTt QwjtUg 提取码 5kah
  • 凸优化学习(七)——SVM“1-范数”的软边界

    注意 本文内容来自于吴恩达老师cs229课堂笔记的中文翻译项目 https github com Kivy CN Stanford CS 229 CN 中的凸优化部分的内容进行翻译学习 3 SVM L 1 L 1 L1 范数的软边界 为了看
  • AlertDialog,当点击按钮时,能够根据界面上输入的数据,弹出对话框,显示界面中输入的相关信息

    我的代码采用分离监听器的模式 即不在 setOnclickListener 中 new OnClickListener public class MainActivity extends Activity implements View O
  • 【Android】APT与JavaPoet学习与实战

    PS 本文讲解的APT全称为Annotation Processing Tool 而非是Android Performance Tuner 这两种工具简称皆为APT 前者是 注释处理工具 后者是 Android性能调试器 本文分别使用Jav
  • 使用--link实现容器互联,很简单

    大家好 今天分享docker 使用 link实现容器互联 运行镜像 root localhost docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES root lo
  • 基于STM32MP157调试MIPI-DSI屏幕

    平台 STM32MP157 屏幕 mipi dsi接口 1024x600 内核版本 linux5 4 本人是第一次调试mipi屏 在157这个平台上遇到的问题有一点多 接下来简单的描述下我的调试经验 一 先配置一下设备树DTB ltdc p
  • 学习java随堂练习-20220617

    目录 第1题 今天是学习Java的第十四天 1道练习题 第1题 题目 运行结果 代码如下 1 Student 描述学生类 属性 学号 姓名 性别 电话 方法 显示详细信息 public class Student private Strin
  • java代码 阿里云短信 手机接受验证码

    package com antifreeze server controller import com aliyuncs DefaultAcsClient import com aliyuncs IAcsClient import com
  • AppDomain 和动态加载

    应用程序体系结构 在我专攻代码之前 我想谈谈我尝试做的事 您可能记得 SuperGraph 让您从函数列表中进行选择 我希望能够在具体的目录中放置外接程序程序集 让 SuperGraph 检测它们 加载它们 并找到它们中包含的所有函数 如果