C#.Net 如何动态加载与卸载程序集(.dll或者.exe)3---- 动态加载Assembly应用程序

时间:2024-04-18 21:37:43

下载 supergraphfiles.exe 示例文件。

应用程序体系结构

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

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

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

创建 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(),它需要执行以下操作:

1.       卸载 AppDomain。

2.       创建新的 AppDomain。

3.       在新的 AppDomain 中重新加载程序集。

4.       将图形线条挂钩到新创建的 AppDomain。

步骤 4 是必需的,因为 GraphLine 对象包含来自原 AppDomain 的 Function 对象。卸载 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);

}

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

状态

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