一、概述
使用.NET建立的可执行程序 *.exe,并没有直接承载到进程当中,而是承载到应用程序域(AppDomain)当中。在一个进程中可以包含多个应用程序域,一个应用程序域可以装载一个可执行程序(*.exe)或者多个程序集(*.dll),这样可以使应用程序域之间实现深度隔离,即使进程中的某个应用程序域出现错误,也不会影响其他应用程序域的正常运作。处理asp.net所涉及的类大多数定义在System.Web程序集中。
- 当exe程序集加载完毕,.Net会在当前进程中创建一个默认应用程序域,这个应用程序域的名称与程序集名称相同。默认应用程序域不能被卸载,并且与其所在的进程同生共灭。
- 应用程序域只是允许它所加载的程序集访问由.Net Runtime所提供的服务。这些服务包括托管堆(Managed Heap),垃圾回收器(Garbage collector),JIT 编译器等.Net底层机制.
- 一个应用程序域中如果出现了致命错误导致崩溃,只会影响其本身,而不会影响到其他应用程序域。实现了错误隔离。
应用程序域是进程中承载程序集的逻辑分区,在应用程序域中存在更细粒度的用于承载.NET对象的实体,即.NET上下文Context。
所有的.NET对象都存在于.NET上下文当中,每个AppDomain当中至少存在一个默认上下文(context0),:
优点
托管程序为什么要使用应用程序域呢?概括其优点如下:
- 在一个应用程序中出现的错误不会影响其他应用程序。
- 因为类型安全的代码不会导致内存错误,所以使用应用程序域可以确保在一个域中运行的代码不会影响进程中的其他应用程序。
- 能够在不停止整个进程的情况下停止单个应用程序。
- 使用应用程序域时,可以卸载在单个应用程序中运行的代码。
- 在一个应用程序中运行的代码不能直接访问其他应用程序中的代码或资源。
属性
- ActivationContext 获取当前应用程序域的激活上下文。
- ApplicationIdentity 获得应用程序域中的应用程序标识。
- BaseDirectory 获取基目录,它由程序集冲突解决程序用来探测程序集。
- CurrentDomain 获取当前 Thread 的当前应用程序域。
- FriendlyName 获取此应用程序域的友好名称。
- Id 获得一个整数,该整数唯一标识进程中的应用程序域。
- SetupInformation 获取此实例的应用程序域配置信息。
方法
- CreateDomain(String) 使用指定的名称新建应用程序域。
- CreateInstance(String, String) 创建在指定程序集中定义的指定类型的新实例。
- CreateInstanceAndUnwrap(String, String) 创建指定类型的新实例。 形参指定定义类型的程序集以及类型的名称。
- CreateInstanceFrom(String, String) 创建在指定程序集文件中定义的指定类型的新实例。
- DoCallBack 在另一个应用程序域中执行代码,该应用程序域由指定的委托标识。
- ExecuteAssembly(String) 执行指定文件中包含的程序集。
- ExecuteAssemblyByName(String) 在给定其显示名称的情况下执行程序集。
- GetAssemblies 获取已加载到此应用程序域的执行上下文中的程序集。
- GetData 为指定名称获取存储在当前应用程序域中的值。
- IsDefaultAppDomain 返回一个值,指示应用程序域是否是进程的默认应用程序域。
- Load(AssemblyName) 在给定 AssemblyName 的情况下加载 Assembly。
- SetData(String, Object) 为指定的应用程序域属性分配指定值。
- Unload 卸载指定的应用程序域。
程序集
程序集是包含一个或多个类型定义文件和资源文件的集合。它允许我们分离可重用类型的逻辑表示和物理表示。程序集可以是静态的或动态的。静态程序集可以包括 .NET Framework 类型(接口和类),以及该程序集的资源(位图、JPEG 文件、资源文件等)。静态程序集存储在磁盘上的可移植可执行 (PE) 文件中。您还可以使用 .NET Framework 来创建动态程序集,动态程序集直接从内存运行并且在执行前不存储到磁盘上。您可以在执行动态程序集后将它们保存在磁盘上。
全局程序集缓存:安装有公共语言运行时的每台计算机都具有称为全局程序集缓存的计算机范围内的代码缓存。 全局程序集缓存中存储了专门指定给由计算机中若干应用程序共享的程序集。
二、基本操作
在.Net 中,将应用程序域封装为了AppDomain类,这个类提供了应用程序域的各种操作,包含 加载程序集、创建对象、创建应用程序域 等。通常的编程情况下下,我们几乎从不需要对AppDomain进行操作。
1、获取当前运行的代码所在的应用程序域。
可以使用AppDomain类的静态属性CurrentDoamin,获取当前代码所在的应用程序域;或者使用Thread类的静态方法GetDomain(),得到当前线程所在的应用程序域:
AppDomain currentDomain = AppDomain.CurrentDomain;
AppDomain currentDomain = Thread.GetDomain();
NOTE:一个线程可以访问进程中所包含的所有应用程序域,因为虽然应用程序域是彼此隔离的,但是它们共享一个托管堆(Managed Heap)。
2、获取应用程序域的名称。
使用AppDomain的实例只读属性,FriendlyName:
string name = AppDomain.CurrentDomain.FriendlyName;
3、从当前应用程序域中创建新应用程序域。
可以使用CreateDomain()静态方法,并传入一个字符串,作为新应用程序域的名称(亦即设置FriendlyName属性):
AppDomain newDomain = AppDomain.CreateDomain("New Domain");
4、配置应用程序域(在创建前):
AppDomainSetup domainInfo = new AppDomainSetup(); domainInfo.ApplicationBase = "e:\AttributeTest"; domainInfo.ConfigurationFile = "e:\AttributeTest\1.exe.config"; AppDomain domain = AppDomain.CreateDomain("MyDomain", null, domainInfo);
5、判断是否为默认应用程序域:
newDomain.IsDefaultAppDomain()
6、卸载应用程序集
AppDomain.Unload(newDomain);
三、在新的应用程序域中执行程序集
加载可执行程序:
AppDomain newAppDomain = AppDomain.CreateDomain("newAppDomain"); newAppDomain.ExecuteAssembly("Example.exe");//或使用ExecuteAssemblyByName AppDomain.Unload(newAppDomain);
四、在新的应用程序域中创建对象
将程序集加载到应用程序域中的方式:
- a.Assembly.Load(assemblyName);使用程序集名来加载到应用程序域中,为常用方法。
- b.Assembly.LoadFrom():加载给定其文件位置的程序集,通过此方法加载程序集将使用不同的加载上下文。
- c.Assembly.ReflectionOnlyLoad和 Assembly.ReflectionOnlyLoadFrom:将程序集加载到仅反射上下文中,可检查(但不能执行)加载到该上下文中的程序集,同时允许检查以其他平台为目标的程序集。
- e.Type.GetType()加载程序集
- f.AppDomain的Load方法加载程序集。主要用于实现COM互操作性,不应该使用该方法将程序集加载到除从其调用该方法的应用程序域以外的其他应用程序域。
- d.AppDomain的CreateInstance和CreateInstanceAndUnwrap方法。
具体见反射基础:https://www.cnblogs.com/springsnow/p/9433924.html
在新的应用程序域中创建对象,可以使用AppDomain的实例方法CreateInstanceAndUnWrap()或者CreateInstance()方法。方法包含两个参数,第一个参数为类型所在的程序集,第二个参数为类型全称(这两个方法后面会详述):
DemoClass obj = (DemoClass)newDomain.CreateInstanceAndUnWrap("ClassLib", "ClassLib.DemoClass"); //或者 ObjectHandle objHandle = newDomain.CreateInstance("ClassLib", "ClassLib.DemoClass"); DemoClass obj = (DemoClass)objHandle.UnWrap();
//或者
DemoClass obj = (DemoClass)newDomain.CreateInstanceFromAndUnWrap("c:\ClassLib.dll", "ClassLib.DemoClass");
注意:obj是在当前的默认应用程序域声明的;而类型的实例(对象本身)却在新创建的应用程序域NewDomain中创建的。这样就出现了一种尴尬的情况:而默认情况下AppDomain是彼此隔离的,我们不能直接在一个应用程序中引用另一个应用程序域中的对象,所以这里便会引发异常。
跨AppDomain运行代码:
void Main() { //建立新的应用程序域对象 AppDomain newAppDomain = AppDomain.CreateDomain("newAppDomain"); CrossAppDomainDelegate crossAppDomainDelegate = new CrossAppDomainDelegate(MyCallBack); newAppDomain.DoCallBack(crossAppDomainDelegate); newAppDomain.DomainUnload += (obj, e) => { Console.WriteLine("New Domain unload!"); }; AppDomain.Unload(newAppDomain); } static public void MyCallBack() { string name = AppDomain.CurrentDomain.FriendlyName; for (int n = 0; n < 4; n++) Console.WriteLine(string.Format(" Do work in {0}........", name)); }
五、封值传送,Serializable
我们可以将对象标记为可序列化,将对象本身由另一应用程序域(远程)传递到本地应用程序域中。
原理: 先在远程创建对象,接着将对象序列化,然后传递对象,在本地进行反序列化,最后还原对象。
当位于ConsoleApp.exe的obj引用NewDomain中创建的对象时,.Net将NewDomain中对象的状态进行复制、序列化,然后在ConsoleApp.exe中重新创建对象,还原状态,并通过代理来对对象进行访问。这种跨应用程序域的访问方式叫做 传值封送(Marshal by value)
[Serializable] public class DemoClass { /*略*/ }
六、传引用封送,MarshalByRefObject
让对象依然保留在远程(本例为NewDomain中),而在客户端仅创建代理,上面已经说了代理的接口和远程对象完全相同,所以客户端以为仍然访问的是远程对象,当客户端调用代理上的方法时,由代理将对方法的请求发送给远程对象,远程对象执行方法请求,最后再将结果传回。这种方式叫做 传引用封送(Marshal by reference)。
对象或者对象引用在传递的过程中,是以一种包装过的状态(warpper state)进行传递。所以在创建对象时,要解包装,因此在CreateInstanceAndUnWrap()方法后多了一个AndUnWrap后缀,实际上UnWrap还包含一个创建代理的过程。
让对象继承自MarshalByRefObject基类,实现传引用封送。
public class DemoClass:MarshalByRefObject {/*略*/}
上下文绑定对象:ContextBoundObject
当系统需要对象使用消息接收器机制的时候,即可使用ContextBoundObject类。ContextBoundObject继承了MarshalByRefObject类,保证了它的子类都会通过透明代理被访问。
一般类所建立的对象为上下文灵活对象(context-agile),它们都由CLR自动管理,可存在于任意的上下文当中。而 ContextBoundObject 的子类所建立的对象(称作上下文绑定对象)只能在建立它的对应上下文中正常运行,此状态被称为上下文绑定。其他对象想要访问ContextBoundObject 的子类对象时,都只能通过代透明理来操作。ContextBound还有一个Synchronization特性,此特性会保证ContextBound对象被加载到一个线程安全的上下文当中运行。
七、应用程序域、进程与线程关系。
概述:
- 进程之间彼此是完全隔绝的,而线程与运行在相同进程的其他线程共享堆heap内存。
- 在应用程序域和线程之间没有一对一的关联。
进程(Process)、线程(Thread)、应用程序域(AppDomain)、上下文(Context)的关系如图5.0。
一个进程内可以包括多个应用程序域,也有包括多个线程,线程也可以穿梭于多个应用程序域当中。但在同一个时刻,线程只会处于一个应用程序域内。线程也能穿梭于多个上下文当中,进行对象的调用。