购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

19.3 .NET应用程序域

19.3.1 基础知识

AppDomain即.NET应用程序域,是一个Windows操作系统设计的边界与隔离环境,专为加载与执行.NET应用程序中的程序集而设计。AppDomain可以被视为一个逻辑容器,内部封装了一组相互关联的程序集,提供了程序执行所需的独立空间。与基于C++编写的非托管程序直接运行于Windows进程中不同,.NET可执行文件在启动后,并非直接融入操作系统的进程环境,而是首先进入一个特定的AppDomain。

此外,相比整个Windows进程而言,AppDomain在资源消耗上展现出显著优势。由于它专注于管理.NET应用程序的执行环境,而非整个操作系统的资源,因此AppDomain在CPU和内存使用上更为轻量。这一特性使得公共语言运行时(CLR)能够更迅速地加载和卸载AppDomain,相较于传统进程管理,显著提升了应用程序的启动速度和资源利用效率。

在Windows环境下,一个单独的进程能够容纳多个独立的AppDomain,每个AppDomain均承载并隔离运行着一个或多个.NET应用程序。这种隔离机制确保了当一个应用程序域内发生异常或崩溃时,其影响被严格限制在该域内,不会波及其他并行运行的AppDomain,从而实现了安全高效的资源管理和错误隔离。图19-21表示应用程序域之间和进程的关系。

从图19-21可以看到,每个.NET应用程序域(AppDomain)都维护着一个加载器堆,该堆用于记录自该AppDomain创建以来所加载和访问过的所有类型信息。每个独特的类型在.NET环境中都拥有一个方法表(Method Table),这个方法表是类型的元数据核心部分,其中每一项记录都直接指向了通过即时编译器(Just-In-Time Compiler,JIT)编译生成的本地机器代码。

图19-21 应用程序域之间和进程的关系

值得注意的是,由于某些程序集包含.NET框架的核心功能,因此它们会被设计为跨多个AppDomain共享,比如MSCorLib.dll,该程序集包含了System.Object、System.Int32以及其他所有与.Net Framework密不可分的类型。当CLR初始化时,该程序集会自动加载,而且所有的AppDomain都共享该程序集的类型,因此CLR会为它们维护一个特殊的加载器堆。该加载器堆中所有的类型对象,以及为这些类型定义的方法JIT编译生成的所有本地代码,都会被进程中的所有AppDomain共享。这种设计不仅优化了资源使用,还确保了.NET运行时环境的一致性和稳定性。

接下来,我们查看AppDomain类的签名定义,以便更清晰地理解其结构和用途。具体代码如下所示。

从上述定义来看,AppDomain类继承了MarshalByRefObject类,这意味着AppDomain实例可以作为代理对象,允许在不同应用程序域之间安全地传递和引用对象,而无须直接跨域共享内存空间。通过这种方式,.NET运行时能够管理跨域对象访问的复杂性,包括远程过程调用(RPC)的封装、序列化等场景。此外,AppDomain类还提供了CreateDomain方法,该方法允许创建一个新的应用程序域,用于隔离加载和执行程序集。关于CreateDomain方法的具体使用场景及相关的隔离与通信知识,我们将在后续内容中详细介绍。

19.3.2 基本用法

1.默认应用程序域

在.NET环境中,当一个可执行文件(如控制台应用程序、Windows窗体应用程序等)启动时,CLR会初始化并加载到该宿主进程的默认应用程序域中。这个默认应用程序域是进程启动时自动创建的,并且可以作为加载和执行程序集的容器。通过AppDomain.CurrentDomain属性,我们可以访问和操作当前线程所在的应用程序域的信息。下面这段代码用于展示如何获取并打印默认应用程序域的相关信息。

上述代码还通过GetAssemblies方法获取了默认应用程序域中所加载的.NET程序集,该方法返回一个对象数组,运行结果如图19-22所示。

图19-22 AppDomain加载的程序集列表

需要说明的是,AppDomain类的ExecuteAssembly方法提供了一种在指定的应用程序域中加载和执行程序集的方式,该方法接受一个字符串参数,表示要执行的程序集的路径。例如在默认应用程序域下执行外部文件,具体代码如下所示。

此处指定加载绝对路径下的可执行文件Sharp4Startcalc.exe,该文件在Main方法中实现启动本地计算器,如图19-23所示。

图19-23 启动本地计算器

在深入分析ExecuteAssembly方法的过程中,发现内部通过调用Assembly.LoadFrom方法来动态加载外部的程序集,如图19-24所示。

图19-24 分析ExecuteAssembly方法

除了ExecuteAssembly方法外,AppDomain类还提供了ExecuteAssemblyByName方法,该方法提供了一种更为便捷的方式来执行程序集。与ExecuteAssembly或Assembly.LoadFrom方法不同,ExecuteAssemblyByName仅需要程序集的名称,而无须指定程序集文件的物理路径。例如,将Sharp4Startcalc.exe放入当前目录下运行,具体代码如下所示。

在运行时,该方法会按照特定的顺序在当前工作目录、环境变量指定的目录以及全局程序集缓存(GAC)中查找并加载该程序集,从而实现对程序集的高效管理和动态执行。

2.创建新的应用程序域

在.NET中,AppDomain.CreateDomain方法可以在当前进程中创建一个新的应用程序域,调用CreateDomain方法时,至少需要提供一个友好名称,但也可以指定其他参数来配置新创建的应用程序域。下面展示了如何使用AppDomain.CreateDomain方法创建一个新的应用程序域,具体代码如下所示。

运行以上代码后,新创建的NewAppDomain应用程序域自动地加载了当前应用程序目录下的Sharp4DLL.dll文件,如图19-25所示。

图19-25 CreateDomain创建新的应用程序域

值得注意的是,此处Load方法传递的参数如果是一个完全限定名称,则可以直接加载指定路径下的程序集,这对于实现插件化架构、动态扩展应用程序功能或实现热插拔的模块非常有用。下面这段代码演示了通过Load方法将加载的程序集加载到独立的应用程序域中。

以上代码通过domain.Load方法加载Sharp4DLL.dll程序集,而Sharp4DLL程序集中的Sharp4DLL方法用于启动本地计算器程序。我们可以利用返回的程序集对象,通过反射机制动态调用Sharp4DLL方法,从而启动计算器,如图19-26所示。

除此之外,AppDomain.CreateInstanceFrom方法也可以直接加载指定路径下的程序集,并且创建该程序集包含的对象,该方法返回System.Runtime.Remoting.ObjectHandle对象,这个对象在分布式应用程序和远程处理场景中尤为重要。同样,通过反射机制动态调用Sharp4DLL方法,从而启动计算器,具体实现如下所示。

这里的ObjectHandle类提供的Unwrap方法用于获取对被包装对象的直接引用。通过调用此方法,本地应用程序可以获取到远程或隔离环境中创建的对象实例,再通过反射触发Sharp4DLL方法。

图19-26 Load方法反射启动计算器

3.卸载应用程序域

CLR默认不支持直接卸载单独的.NET程序集,因为程序集的加载和卸载通常与应用程序域(AppDomain)的生命周期紧密相关。然而,CLR允许通过卸载整个AppDomain来间接地卸载该域中加载的所有程序集。当AppDomain被卸载时,CLR会触发DomainUnload事件,该事件为外部提供了一个在AppDomain被完全销毁之前执行卸载清理的机会。

以下是一个示例代码片段,展示了如何创建DomainUnload事件的处理方法,并在AppDomain卸载时显示卸载信息。

在卸载特定应用程序域的过程中,DomainUnload事件被成功触发,控制台输出如图19-27所示的结果,其中包括卸载的应用程序域的友好名称以及与卸载过程相关的状态信息。

4.跨应用程序域

在.NET框架中,为了维护应用程序的安全性和隔离性,默认情况下,两个或多个AppDomain之间的数据和执行环境是相对独立的。这意味着,一个AppDomain中的代码不能直接访问或修改另一个AppDomain中的内存或数据,除非通过特定的跨域通信机制。

当需要在不同的AppDomain中执行某个操作时,可以使用CrossAppDomainDelegate委托作为桥梁。这个委托允许将方法作为参数传递给另一个AppDomain,并在其中执行。为了使用这个机制,首先需要将要执行的方法与CrossAppDomainDelegate委托进行绑定,然后通过目标AppDomain的DoCallBack方法执行该委托。

图19-27 卸载应用程序域触发DomainUnload事件

下面是一个具体的例子,首先定义一个名为MyCallBack的方法,代码如下。

接着,建立新的应用程序域对象newAppDomain,再通过CrossAppDomainDelegate委托绑定MyCallBack方法,最后交由AppDomain.DoCallBack执行调用。具体实现代码如下所示。

运行后启动默认的应用程序域,然而MyCallBack执行的代码却在newAppDomain中,如图19-28所示。

另外,跨越AppDomain边界访问对象,一般通过两种方式,一是按值封送,二是按引用封送。除此之外,非封送类型进行跨越AppDomain访问时都会抛出异常。需要说明的是,AppDomain类提供了一个CreateInstanceAndUnwrap方法,当该方法封送的对象类型派生自MarshalByRefObject时,CLR就会跨越AppDomain边界按引用封送对象。

图19-28 跨应用程序域调用执行MyCallBack

为了说明如何通过继承MarshalByRefObject和实现接口来创建可跨应用程序域通信的对象,我们创建了一个程序集文件,具体实现代码如图19-29所示。

图19-29 程序集文件

在图19-29所示的代码中,首先,我们创建一个名为IService的接口,并在其中定义一个Setup方法,随后,我们定义一个实体类Service,它继承自MarshalByRefObject类以支持跨应用程序域的引用传递。同时,Service类实现IService接口,提供Setup方法的具体实现。这里的代码很明显是用于启动本地计算器的。

接下来,我们通过创建新的应用程序域Secondary AppDomain,调用CreateInstanceAndUnwrap方法获取对象引用,实现主应用程序域和其他域之间的通信,从而成功启动计算器进程,具体代码如下所示。

调试运行时,主应用程序域互通其他域,成功启动本地计算器进程,如图19-30所示。

图19-30 使用CreateInstanceAndUnwrap跨域互通访问

AppDomain.CurrentDomain.SetData方法是一种在.NET环境中用于在当前应用程序域内部存储特定键关联数据的方式。此方法允许设置一组键值对,这些数据仅对当前应用程序域内的代码可见和有效。

例如,可以通过System.Data.DataSetDefaultAllowedTypes指定允许被实例化的类,具体代码如下所示。

以上代码通过AppDomain.CurrentDomain.SetData设置允许DataSet可以实例化XamlReader、ObjectDataProvider、ExpandedWrapper对象。 56I58Xx7Jl8ZNtKsmR7z6B3+rJ2ok+PzE+gTi0Ppi3BqT7U1sw9gpafrhl1d+6E6

点击中间区域
呼出菜单
上一章
目录
下一章
×