標籤:
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系統需要大量記憶體來虛擬換一個進程的地址空間。
但是,如果應用程式完全由Managed 程式碼構成(這些代碼的安全性可以驗證),同時這些代碼沒有調用Unmanaged 程式碼,那麼在一個Window進程中運行多個Managed 程式碼是沒有問題的。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。
第二節:AppDomain