第二节:AppDomain
CLR COM服务器初始化时,会创建一个AppDomain。AppDomain是一组程序集的逻辑容器。CLR初始化时创建的第一个AppDomain称为默认的AppDomain,这个默认的AppDomain只有在Windonws进程终止时才能被撤销。
除了默认的AppDomain,正在使用非托管Com接口方法或托管类型方法的一个宿主还可指示CLR创建额外的AppDomain,AppDomain唯一的作用就是进程隔离。下面总结了AppDomain的具体功能。
1.1. 一个AppDomain中的代码创建的对象不能由另一个AppDomain中的代码直接访问。
一个AppDomain中的代码创建了一个对象后,该对象被AppDomain“拥有”。换言之,它的生存期不能比创建它的代码所在的AppDomain还要长。一个AppDomain中的代码为了访问另一个AppDomain中的对象,只能使用“按引用封送”或者”按值封送”的语义。这就加强了一个清晰的分隔和边界,因为一个AppDomain中的代码没有对另一个AppDomain中的代码所创建的对象的直接引用。这种隔离使AppDomain可以很容易地从一个进程卸载,不会影响其它应用程序正在运行的代码。
2. AppDomain可以卸载 CLR不支持从AppDomain中卸载一个程序集的能力。但是,可以告诉CLR卸载一个AppDomain,从而卸载当前包含在该AppDomain内的所有程序集。
3. AppDomain可以单独保护 AppDomain在创建之后,会应用一个权限集,它决定了向这个AppDomain中运行的程序集授予的最大权限。正是由于存在这些权限,所以当宿主加载一些代码之后,可以保证这些代码不会被破坏宿主本身使用的一些重要数据接口。
4. AppDomain可以单独实施配置 AppDomain在创建之后,会关联一组配置设置。这些配置主要影响CLR在AppDomain中加载程序集的方式。这些设置涉及搜索路径、版本绑定重定向、卷影赋值以及加载器优化。
重要提示:Windows的一个出色功能是让每个用用程序都在自己的进程地址空间中运行。这就保证了一个应用程序的代码不能访问另一个应用程序使用的代码或数据。进程隔离可防止安全漏洞、数据破换和其他不可预测的行为,确保了Windows系统以及在它上面运行的应用程序的健壮性。遗憾的是,在Windows中创建进程的开销很大。Win 32 CreateProcess函数的速度很慢,而且Window系统需要大量内存来虚拟换一个进程的地址空间。
但是,如果应用程序完全由托管代码构成(这些代码的安全性可以验证),同时这些代码没有调用非托管代码,那么在一个Window进程中运行多个托管代码是没有问题的。AppDomain提供了保护、配置和终结其中每一个应用程序所需的隔离性。
下图演示了一个Windows进程,其中运行着一个CLR Com 服务器。这个CLR当前管理这两个AppDomain。每个AppDomain都有自己的Loader堆,每个Loader堆都记录了自AppDomain创建以来已访问过哪些类型。Loader堆中的每个类型对象都有一个方法表,方法表中的每个记录项都指向JIT编译的本地代码。
除此之外,每个AppDomain都加载了一些程序集。AppDomain #1 (默认AppDomain)有三个程序集:MyApp.exe,TypeLib.dll和System.dll。AppDomain#2有两个程序集:Wintellect.dll和System.dll。
如图所示,System.dll程序集被加载到两个AppDomain中。如果这两个AppDomain都使用了来自System.dll的一个类型,那么在这两个AppDomain的Loader堆中,都会为同一个类型分配一个类型对象;类型对象的内存不会由两个AppDomain共享。另外,一个AppDomain中的代码调用一个类型定义的方法时,方法的IL代码会进行JIT编译,生成本地代码将与每个AppDomain相关联;方法的代码不由调用它的所有AppDomain共享。
不共享类型对象的内存或者本地代码,这当然是一种浪费。但是,AppDomain的全部目的就是隔离性;CLR要求在卸载某个AppDomain并释放它的所有资源的同时,不会对其它AppDomain产生负面影响。通过赋值CLR的数据结构,就可以保证这一点。除此之外,还能保证由多个AppDomain使用的一个类型在每个AppDomain中都有一组静态字段。
有的程序集本来就是要有多个AppDomain使用。最典型的例子就是MSCorLib.dll。该程序集包含了System.Object,System.Int32以及其他所有与.NET Farmework密不可分的类型。CLR初始化时,该程序集会自动加载,而且所有AppDomain都共享该程序集中的类型。为了减少资源消耗,MsCorLib.dll程序集以一种” AppDomain中立”的方式加载。也就是说,针对以“AppDomain中立”的方式加载的程序集,CLR会为他们维护一个特殊的Loader堆。该Loader堆中的所有类型对象,以及为这些类型定义的方法JIT编译生成的所有本地代码,都会被进程中的所有AppDomain共享。遗憾的是,共享这些资源所带来的收益并不是没有代价的。这个代价就是,以“AppDomain中立”的方式加载的所有程序集永远不能卸载。为了回收他们占用的资源,唯一的办法就是终止Windows进程,让Window去回收资源。
跨越AppDomain边界访问对象
一个AppDomain中的代码可以和另一个AppDomain中的类型和对象通信。但是,只允许通过良好定义的机制访问这些类型和对象。下面的Ch22-1-AppDomains示例程序演示了如何创建一个新的AppDomain,在其中加载一个程序集,然后构造那个程序集所定义的一个类型的实例。代码演示了构造以下三种类型时不同的行为:一个“按引用封送”的类型;一个“按值封送”的类型;一个完全不能封送的类型。代码还演示了这些已 封送的对象在创建他们的AppDomain卸载时的行为。Ch22-1AppDomains示例程序的代码实际很少,只是我添加了大量注释。在代码清单之后,我将逐一分析这些代码,解析CLR所做的事情。
namespace Ch22_1_AppDomains { class Program { static void Main(string[] args) { //Marshalling(); AppDomainResourceMonitoring(); } private static void Marshalling() { //获取AppDomain的一个引用("调用线程"当前正在该AppDomain中执行) AppDomain adCallingThreadDomain = Thread.GetDomain(); //获取这个AppDomain的友好字符串名称,并显示它 string callingDomainName = adCallingThreadDomain.FriendlyName; Console.WriteLine("Default AppDomain's friendly name={0}", callingDomainName); //获取显示我们的AppDomain中包含了Main方法的程序集 string exeAssembly = Assembly.GetEntryAssembly().FullName; Console.WriteLine("Main assembly={0}", exeAssembly); //定义一个局部变量来引用一个AppDomain AppDomain ad2 = null; //***Demo1:使用Marshal-by-Reference进行跨AppDomain通信..... Console.WriteLine("{0} Demo #1", Environment.NewLine); //新建一个AppDomain(安全性和配置匹配于当前AppDomain) ad2 = AppDomain.CreateDomain("AD #2", null, null); MarshalByRefType mbrt = null; //将我们的程序集加载到AppDomain中,构造一个对象,把他封送回我们的AppDomain(实际会的对一个代理的引用) mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "Ch22_1_AppDomains.MarshalByRefType"); //CLR在类型上撒谎了 Console.WriteLine("Type={0}", mbrt.GetType()); //证明得到的是对一个代理对象的引用 Console.WriteLine("Type={0}", RemotingServices.IsTransparentProxy(mbrt)); mbrt.SomeMethod(); //卸载新的AppDomain AppDomain.Unload(ad2); try { mbrt.SomeMethod(); Console.WriteLine("Successful call."); } catch (AppDomainUnloadedException) { Console.WriteLine("Failed call."); } //Demo 2: 使用Marshal-by-Value进行跨AppDomain通信.... Console.WriteLine("{0} Demo #2", Environment.NewLine); //新建一个AppDomain(安全和匹配与当前AppDomain) ad2 = AppDomain.CreateDomain("AD #2", null, null); mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "Ch22_1_AppDomains.MarshalByRefType"); MarshalByValType mbvt = mbrt.MethodWithReturn(); //证明我们得到的不是对一个代理对象的引用 Console.WriteLine("Is proxy={0}", RemotingServices.IsTransparentProxy(mbvt)); Console.WriteLine("Returned object created" + mbvt.ToString()); //卸载AppDomain AppDomain.Unload(ad2); try { Console.WriteLine("Returned object created " + mbvt.ToString()); Console.WriteLine("sucessful call"); } catch (AppDomainUnloadedException) { Console.WriteLine("Failed call."); } Console.WriteLine("{0} Demo #3", Environment.NewLine); ad2 = AppDomain.CreateDomain("AD #2", null, null); mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "Ch22_1_AppDomains.MarshalByRefType"); //对象的方法返回一个不可封送的对象;抛出异常 NonMarshalableType nmt = mbrt.MethodArgAndReturn(callingDomainName); } private static void AppDomainResourceMonitoring() { using (new AppDomainMonitorDelta(null)) { var list = new List<Object>(); for (Int32 x = 0; x < 1000; x++) list.Add(new Byte[10000]); for (Int32 x = 0; x < 2000; x++) new Byte[1000].GetType(); Int64 stop = Environment.TickCount + 5000; while (Environment.TickCount < stop) ; } } } //该实例可跨越AppDomain的边界"按引用封送" public sealed class MarshalByRefType : MarshalByRefObject { public MarshalByRefType() { Console.WriteLine("{0} actor running in {1}", this.GetType().ToString(), Thread.GetDomain().FriendlyName); } public void SomeMethod() { Console.WriteLine("Executing in " + Thread.GetDomain().FriendlyName); } public MarshalByValType MethodWithReturn() { Console.WriteLine("Executing in " + Thread.GetDomain().FriendlyName); MarshalByValType t = new MarshalByValType(); return t; } public NonMarshalableType MethodArgAndReturn(String callingDomainName) { Console.WriteLine("Calling from '{0}' to '{1}'", callingDomainName, Thread.GetDomain().FriendlyName); NonMarshalableType t = new NonMarshalableType(); return t; } } //该类型的实例可跨越AppDomain的边界"按值封送" [Serializable] public sealed class MarshalByValType : Object { private DateTime m_creatingTime = DateTime.Now;//注意DateTime是可以序列化的 public MarshalByValType() { Console.WriteLine("{0} actor running in {1},Created on {2:D}", this.GetType().ToString(), Thread.GetDomain().FriendlyName, m_creatingTime); } public override string ToString() { return m_creatingTime.ToLongDateString(); } } //该类的实例不能夸AppDomain边界进行封送 public sealed class NonMarshalableType : Object { public NonMarshalableType() { Console.WriteLine("Executing in" + Thread.GetDomain().FriendlyName); } } sealed class AppDomainMonitorDelta : IDisposable { private AppDomain m_appDomain; private TimeSpan m_thisADCpu; private Int64 m_thisADMemoryInUse; private Int64 m_thisADMemoryAllocated; static AppDomainMonitorDelta() { //确定以打开AppDomain监视 AppDomain.MonitoringIsEnabled = true; } public AppDomainMonitorDelta(AppDomain ad) { m_appDomain = ad ?? AppDomain.CurrentDomain; // m_thisADCpu = m_appDomain.MonitoringTotalProcessorTime; //返回由当前CLR实例控制的所有AppDomain正在使用的字节数 m_thisADMemoryInUse = m_appDomain.MonitoringSurvivedMemorySize; //返回一个特定的AppDomain已分配的字节数 m_thisADMemoryAllocated = m_appDomain.MonitoringTotalAllocatedMemorySize; } public void Dispose() { GC.Collect(); Console.WriteLine("FriendlyName={0},CPU={1}ms", m_appDomain.FriendlyName, (m_appDomain.MonitoringTotalProcessorTime - m_thisADCpu).TotalMilliseconds); Console.WriteLine("Allocated {0:N0} bytes of which {1:N0} survived GCs", m_appDomain.MonitoringTotalAllocatedMemorySize - m_thisADMemoryAllocated, m_appDomain.MonitoringSurvivedMemorySize - m_thisADMemoryInUse); } } }
我们现在针对上面的例子来进行讲解:在Marshalling方法中,首先获得一个AppDomain对象的引用,当前调用线程正在这个AppDomain中执行。在Windows中,线程总是在一个进程的上下文中创建,而且线程的整个生存期在该进程的生存期内。但是线程和AppDomain之间没有一对一的关系。AppDomain是一个CLR功能:Windows对AppDomain一无所知。由于多个AppDomain可以在一个Windows进程中,所以线程能执行一个AppDomain中的代码,再执行另一个AppDomain中的代码。从CLR角度看,线程一次只执行一个AppDomain中的代码,线程可调用Thread的静态方法GetDomain向CLR询问它正在那个AppDomain中执行,线程还可查询AppDomain的静态只读属性CurrentDomain来获得同样的信息。
AppDomain创建之后,可以赋予它一个友好的名称,这个友好的名称只是一个String,我们可以利用它来标识一个AppDomain。友好名称一般在调试过程中比较有用。由于CLR在我们的任何代码运行之前就创建默认AppDomain,所以CLR使用可执行文件的文件名来作为默认的AppDomain友好的名称。在Marshalling方法中,是使用了AppDomain的只读属性FriendlyName来查询默认的AppDomain的友好名称。
接着,Marshalling方法查询默认的AppDomain中加载的程序集的强命名标识,这个程序集定义了入口方法Main。这个程序集定义了几个类型:Program, MarshalByRefType, MarshalByValType, NonMarshalableType。
演示1:使用“按引用封送”的跨AppDomain通信
在演示1中,我调用AppDomain.CreateDomain()方法,告诉CLR在同一个Windows进程中创建一个新的AppDomain。AppDomain类型实际提供了CreateDomain方法的几个重载版本。你可以仔细研究一下,并在新建AppDomain时选择一个最合适的一个版本。本例使用的CreateDomain版本接受一下三个参数。
- 一个String,它标识了想分配给新AppDomain的友好名称。本例传递的是“AD #2”.
- 一个System.Security.Policy.Evidence,它标识了CLR用来计算AppDomain的权限集的证据。本例为该参数专递了一个null,使新增AppDomain从创建它的AppDomain继承权限集。通常,如果希望围绕AppDomain中的代码创建一个安全边界,就可构造一个System.Security.PermissionSet对象,将期望的权限对象添加到其中(实现了IPermission接口的类型的实例),最后所得到的PermissionSet对象引用传给接受一个PermissionSet的那个重载版本的CreateDomain。
- 一个System.AppDomainSetup,它标识了CLR为新的AppDomain使用的配置设置。
同样,本例为该参数传递一个null,使新的AppDomain从创建它的AppDomain继承配置设置。如果希望新的AppDomain有一个特殊的配置,可以构造一个AppDomainSetup对象,将它的各种属性设为你希望的值,然后将得到的AppDomainSetup对象引用传递给CreateDomain方法。
在内部,CreateDomain方法会创建一个新AppDomain,该AppDomain将被赋予指定的友好名称、安全性和配置设置。新的AppDomain有它自己的Loader堆,这个堆目前是空的,因为此时还没有程序集加载到新AppDomain中,创建AppDomain时,CLR不在这个AppDomain中创建任何线程:AppDomain也没有代码运行,除非你显示的让一个线程调用AppDomain中的代码。
现在,为了在新AppDomain中创建一个类型的实例,首先必须将一个程序集加载到这个新AppDomain中,然后构造这个程序集中定义的一个类型的实例。这就是AppDomain的公共实例方法CreateInstanceAndUnwrap所做的事情。调用这个CreateInstanceAndUnwrap方法时,我传递了两个String参数;第一个string标识了想在新AppDomain中加载的程序集;第二个参数标识了想构造其实例的那个类型的名称。在内部,CreateInstanceAndUnwrap方法导致调用线程从当前AppDomain转至到新AppDomain。现在,线程将指定的程序集加载到新的AppDomain中,并扫描程序集的类型定义元数据表,查找制定类型。找到类型后,线程调用MarshalByRefType的无参构造器。现在,线程又返回默认的AppDomain,使CreateInstanceAndUnwrap能返回对新的MarshalByRefType对象的引用。
所有这些听起来都很好,但还存在一个问题:CLR不允许一个AppDomain中的变量(根)引用另一个AppDomain中创建的对象。如果CreateInstanceAndUnwrap只直接返回对象引用,AppDomain提供的隔离性就会被打破,而隔离是AppDomain的全部目的!因此,在CreateInstanceAndUnwrap返回对象引用之前,它还要执行一些额外的逻辑。
MarshalByRefType类型时从一个非常特殊的基类MarshalByRefObject派生的。当CreateInstanceAndUnwrap发现它封送的一个对象的基类派生自MarshalByRefObject时,CLR就会跨AppDomain边界按引用封送对象。下面讲诉按引用将一个对象从一个AppDomain(源AppDomain,这里是真正创建对象的地方)封送到另一个AppDomain(目标AppDomain,这里调用CreateInstanceAndUnwrap的地方)的具体含义。
源AppDomain想向目标AppDomain发送或返回一个对象引用时,CLR会在目标的AppDomain的Loader堆中定义一个代理类型。这个代理类型使用原始类型的元数据定义。因此,它看起来和原始类型完全一样;有完全一样的实例成员。但是,实例字段不会成为代理类型的一部分,然后会更多的讨论实例字段的问题。这个代理类型中确实定义了几个(自己的)实例字段,但这些字段和原始类型的不一致。相反,这些字段只是用于指定哪个AppDomain”拥有”真实的对象,以及如何在拥有(对象)的AppDomain中找到真实的对象。
这个代理类型在目标AppDomain中定义好之后,CreateInstanceAndUnwrap方法就会创建这个代理类型的一个实例,初始化它的字段来标识源AppDomain和真实对象,然后将对这个代理对象的引用返回目标AppDomain。在Ch22-1-AppDomains应用程序中,mbrt变量被设为引用这个代理。注意,从CreateInstanceAndUnwrap方法返回的对象实际不是MarshalByRefType类型的一个实例。CLR一般不允许将一个类型的对象转换成一个不兼容的类型。但在当前这种情况下,CLR允许进行转型,因为新类型和原始类型具有一样的实例成员。实际上,用代理对象调用GetType,它会想你说谎,说自己是一个MarshalByRefType对象。
然而,可以证明从CreateInstanceAndUnwrap返回的对象实际是对一个代理对象的引用,为此,Ch22-1-AppDomains应用程序中调用了RemotingServices.IsTransparentProxy方法,并传递CreateInstanceAndUnwrap方法返回的引用。从输出结果可知,IsTransparentProxy方法返回true,证明返回时一个代理。
接着,应用程序使用代理调用SomeMethod方法。由于mbrt变量引用一个代理对象,所以会调用由代理实现的SomeMethod。在代理的实现中,利用了代理对象中的信息字段,将调用线程从默认的AppDomain切换至新的AppDomain。现在,该线程的任何行动都在新AppDomain的安全策略和配置设置下运行。然后,线程使用代理对象的GCHandle字段查找AppDomain中的真实对象,并用真实对象调用真实的SomeThod方法。
有两个办法可证明调用线程从默认AppDomain切换至新AppDomain。在SomeMethod方法中,我调用了Thread.GetDomain().FriendlyName。这将返回“AD #2”,这是因为线程现在正在新的AppDomain中 运行,而这个新的AppDomain是通过调用AppDomain.CreateDomain方法,并传递”AD #2”作为友好名称参数来创建的。其实,如果在一个调试器中调试代码,并打开了”调用堆栈”窗口,那么”[外部代码]”行会标注一个线程在什么位置跨越AppDomain边界。
真实的SomeMethod方法返回后,会返回至代理的SomeMethod方法,然后将线程切换至默认的AppDomain。接着,线程执行默认AppDomain中的代码。
注意: 一个AppDomain中的线程调用另一个AppDomain中的方法时,线程会在这两个AppDomain之间切换。这意味着跨AppDomain边界的方法调用时同步执行的。但是,在任意时刻,一个线程只能在一个AppDomain中,而且要用那个AppDomain的安全和配置设置来执行代码。如果希望多个AppDomain中的代码并发执行,应创建额外的线程,让这些线程在你希望的AppDomain中执行你希望的代码。
CH22-1-AppDomains应用程序接下来做的事情是调用AppDomain类的公共静态方法Unload,这回强制CLR卸载指定的AppDomain(包括加载到其中的所有程序集),并强制执行一次垃圾回收,以释放由卸载AppDomain中的代码创建的所有对象。这是,默认的AppDomain的mbrt变量仍然引用一个有效的代理对象。但是,代理对象以不再引用一个有效的AppDomain。
当然默认的AppDomain试图使用代理对象调用SomeMethod方法时,调用的是该方法在代理中的实现。代理的实现发现包含真实对象的AppDomain已经卸载。所以,代理的SomeMethod方法会抛出一个AppDomainUnloadedException异常,告诉调用者操作无法完成。
显然,Microsoft的CLR团队不得不做大量的工作来确保AppDomain的正确隔离,但这是他们必须做的跨AppDomain访问对象的功能正在被大量使用,开发人员对这个功能的依懒性正在日益增长。不过,使用”按引用封送”的语义进行跨AppDomain边界的对象访问,会产生一些性能上的开销。所以,一般尽量少用这个功能。
前面我曾许诺过更多讨论实例字段。从MarshalByRefObject派生的类型可定义实例字段。但是,这些实例字段不会成为代理类型的一部分,也不会包含在一个代理对象中。
当你写代码对派生自MarshalByRefObject的一个类型的实例字段进行读写时,JIT编译器会自动生成代码,调用System.Ojbect的FieldGetter方法或FieldSetter方法来使用代理对象。这些方法是私有的,而且没有在.NET FrameWork SDK文档中记录。简单的说,这些方法利用反射机制来获取或设置一个字段中的值。因此,虽然能访问派生自MarshalByRefObject的一个类型中的字段,但性能很差,因为CLR最终要调用方法来执行字段访问。事实上,即使你要访问的字段在你自己的AppDomain中,性能也好不到那里去。
从好不好用的角度说,派生自MarshalByRefObject的类型真的应该避免定义任何静态成员。这是因为静态成员总是在调用AppDomain的上下文中访问。要切换到哪个AppDomain的信息时包含在代理对象中的,但调用静态成员时没有代理对象,所以不会发生AppDomain的切换。让一个类型的静态成员在一个AppDomain中执行,让实例成员在另一个AppDomain中执行,这样的编程未免太丑了。
由于第二个AppDomain中没有根的,所以代理引用的原始对象可以被垃圾回收。这当然不理想。但另一个方面,假如将原始对象不确定的留在内存中,代理可能不在引用它,而原始对象依然存活;这同样不理想。CLR解决这个问题的方法时使用了一个“租约管理器”。一个对象的代理创建好之后,CLR保持对象存活5分钟。如果5分钟只能没有通过代理发出调用,对象就会失败,下次垃圾回收会释放它的对象。每发出一次对象的调用,“租约管理器”都会续订对象的租期,保证它在接下去的2分钟内在内存中保持存活。如果在对象过期之后试图通过一个代理调用它,CLR会抛出一个RemotingException。
默认的5分钟和2分钟的租期设定可以修改的,你值需要重写MarshalByRefObject的虚方法InitializeLifetimeServices即可。
演示2:使用”按值封送”的跨AppDomain通信
演示2与演示2非常相识。和演示1一样,演示2页创建了一个新AppDomain。然后,调用CreateInstanceAndUnwrap方法将同一个程序集加载到新建AppDomain中,并在这个新AppDomain中创建MarshalByRefType类型的一个实例。接着,CLR为这个对象创建一个代理,mbrt变量被初始化成引用这个代理。现在,使用这个代理,我调用MethodWithReturn。这个方法时无参,它将在新AppDomain中执行,从而创建MarshalByValType类型的一个实例,并将一个引用返回给默认的AppDomain。
MarshalByValType不是从MarshalByRefObject派生的,所以CLR不能定义一个代理类型,并创建代理类型的一个实例;对象不能按引用跨AppDomain边界进行封送。
但是,由于MarshalByValType标记了[Serializable]这个自定义attribute,所以CreateInstanceAndUnwrap方法能够按值封送对象。下面具体描素了将一个对象按值从一个AppDomain(源 AppDomain)封送到另一个AppDomain(目标AppDomain)的含义。
源AppDomain想向目标AppDomain发送或返回一个对象引用时,CLR将对象的实例字段序列化成一个字节数组。这个字节数组从源AppDomain复制到目标AppDomain。然后,CLR在目标AppDomain中反序列化字节数组,这会强制CLR将定义了”被反序列化的类型”的程序集加载到目标AppDomain中(如果尚未加载的话)。接着,CLR创建类型的一个实例,并利用字节数组中的值初始化对象的字段,使之与源对象中的值相同。换言之,CLR在目标AppDomain中准确的复制了源对象。然后CreateInstanceAndUnwrap方法返回对这个副本的引用;这样一来,对象就跨AppDomain的边界按值封送了。
重要提示:加载程序集时,CLR使用目标AppDomain的策略和配置设置(例如,AppDomain可能有一个不同Appbase目录或者不同版本绑定重定向)。这些策略上的差异可能阻碍CLR定为程序集。如果程序集无法记载,会抛出异常,目标AppDomain不会接收到对象引用。
至此,源AppDomain中的对象和目标AppDomain中的对象就有了独立的生存期,他们的状态也可以独立的更改。如果源AppDomain中没有根保持源对象的存活,元对象的内存就会在下一次垃圾回收时被回收。
为了证明从MethodWithReturn方法返回的对象不是对代理对象的一个引用,Ch22-1-AppDomains应用程序调用了RemotingService的公共静态方法IsTransparentProxy,并将MethodWidthReturn方法返回的引用作为参数传给它。如果返回false,表明对象是一个真实的对象,而非代理对象。
现在,程序使用真实的对象调用ToString方法。由于mbrt变量引用一个真实的对象,所以会调用这个方法的真实实现,线程不会再AppDomain之间切换。为了证明这点,可以查看调试器的”调用堆栈”窗口,并没有显示一个[AppDomain Transition]行。
为了进一步证明没有涉及代理,Ch22-1-AppDomains应用程序卸载了AppDomain。然后尝试再次调用ToString。这次调用会成功。因为卸载的AppDomain堆默认的AppDomain又有的对象没有影响。在这些对象中,当然也包括按值封送的对象。
演示3:使用不可封送的类型跨AppDomain通信
这个演示和前两个演示非常相似,都是创建一个新AppDomain。然后,调用CreateInstanceAndUnwrap方法将同一个程序集加载到新AppDomain中,在这个新的AppDomain中创建一个MarshalByValType对象,并让mbrt引用这个对象的一个代理。
然后,我使用代理调用MethodArgAndReturn,它接受一个实参。同样的,CLR必须保持AppDomain的隔离,所以不能直接将对实参的引用传给新的APPDomain。如果对象的类型派生自MarshalByRefObject,CLR会为它创建一个代理,并按引用封送。如果对象的类型用[Serializable]进行了标记,CLR会将对象序列化成一个字节数组,将字节数组封送到新的AppDomain中,再将字节数组反序列化一个对象图,将对象图的根传给MethodArgAndReturn方法。
在这个特定的例子中,我跨越AppDomain边界传递一个String对象。String类型不是从MarshalByRefObject派生的,所以CLR不能创建一个代理。幸好,String被标记为[Serializable],所以CLR能按值封送它,允许代码正常工作。注意,对于string对象,CLR会采取一个特殊的优化措施,跨越AppDomain边界封送一个String对象时,CLR只是跨越边界传递对String对象的引用;不会真的生成String对象的一个副本。CLR之所以能提供这个优化措施,时因为String对象时不可变的;所以,一个AppDomain中的代码不可能破坏String对象的字段。
在MethodArgAndReturn内部,我显示传给它字符串,证明字符串跨越了AppDomain边界。然后,我创建NonMarshalableType类型的一个实例,并将对这个对象的一个引用返回至默认AppDomain。由于NonMarshalableType不是从MarshalByRefObject派生,而且没有应用[Serializable]这个标记,所以不允许按引用和按值封送对象-对象完全不能跨越AppDomain边界进行封送!为了报告这个问题,MethodArgAndReturn在默认AppDomain中抛出一个SerializationException。