AppDomain
在目前的項目開發中,分布式開發已經逐漸成為主流。一個項目要是沒有采用分布式架構,都不好意思跟別人說這是一個完整的項目。這句話雖然有些過激,但是隨着人們對效率的要求在提高,以及產品需要提升用戶體驗。只有在軟件項目的效率和體驗做到高質量,才可以贏得用戶和市場。
對於.NET項目,我們使用較多的分布式結構有Webservice,.Net remoting,MSMQ,WCF,WebAPI等等,我們在使用這些框架的時候,從這些分布式框架中得到了很好的用戶體驗。在.NET項目中,分布式架構對項目的開發也有很大的效率提升。
很多人會問,這些分布式框架的底層原理是什么呢?恐怕誰也不敢輕言幾句就可以描述完畢,在這個博文系列中,就是簡單的描述一下這些分布式結構的底層實現原理。
本文主要講解對象在應用程序域中的傳遞。主要講解應用程序域的一些核心對象,對於應用程序域的操作出現的比較少,所以在這里給出的是程序集的一些基本操作。如有不足之處,還望多多指正。
一.AppDomain解析:
AppDomain在很多場合都是被翻譯為“應用程序域”,在本文中也將采用這一翻譯。對於.NET的開發者,對於CLR應該是最熟悉不過了,CLR類似於java的JVM。在CLR中,AppDomain規定了代碼的執行范圍,提供了錯誤隔離的程度,提供了一個安全隔離度,並且擁有自己的資源。AppDomain的具體功能,有如下圖:

1.AppDomain概述:
AppDomain類似與系統的進程,進程是有操作系統進行創建,AppDomain是由CLR進行創建。一個給定的AppDomain必須駐留在一個操作系統的進程中,而一個給定的進程可以寄宿多個AppDomain。有如下圖:

如上圖所示,一個對象正好存放在一個AppDomain種,值也一樣。一個AppDomain中的對象引用必須是引用同一AppDomain中的對象,AppDomain的行為就好像擁有自己私有的地址空間。如果兩個AppDomain需要使用一個類型,必須為每個AppDomain分別初始化和分配一次類型。必須為各個用到類型的AppDomain分別加載和初始化一次類型的方法和程序集。進程種的各個AppDomain要維護類型的不同拷貝。對於類型的靜態子類,每個AppDomain都有其自己的私有副本。
AppDomain的資源有如圖:

對於應用AppDomain的資源被加載,一直在內存中,卸載AppDomain資源是唯一卸載模塊或者程序集的途徑,卸載AppDomain資源也是回收類型靜態字段所占內存的唯一方式。
在上面提到過操作系統的線程與AppDomain類似,在CLR中定義了System.Threading.Thread,在AppDomain中表示為可調度的實體,在這里提出一個新的概念,那就是“軟線程”和“硬線程”,顧名思義,操作系統的線程被稱為“硬線程”,CLR中的System.Threading.Thread被稱為“軟線程”。一個CLR軟線程對象駐留在一個確定的AppDomain中;一個給定的AppDomain可能有多個軟線程對象。在當前的CLR中,對於給定的AppDomain,硬線程至多有一個軟線程對象屬於他,如果一個硬線程運行在多個AppDomain中,每個AppDomain都會有一個明顯的軟線程對象屬於該線程。當給定的硬線程進入AppDomain后,就會得到同樣的軟線程對象。
2.AppDomain核心對象解析:
上面介紹了一些AppDomain的基本概念,接下來我們來簡單了解一下AppDomain的相關操作和核心對象。在.NET種可以通過System.AppDomain類型訪問AppDomain。在這里我們具體了解一下System.AppDomain類型的方法和屬性。對於該類的說明:https://msdn.microsoft.com/en-us/library/system.appdomain(v=vs.110).aspx。
(1).CurrentDomain:獲取當前Thread 的當前應用程序域。
public static AppDomain CurrentDomain
{
get
{
return Thread.GetDomain();
}
}
由以上代碼可知,該屬性為一個靜態屬性,並且只有一個只讀屬性。該屬性只是簡單地提取存儲在硬線程的TLS(線程本地存儲區)中的AppDomain引用。你可以在Thread.CurrentThread屬性中,從硬線程的TLS中提取當前的軟線程對象。
(2).GetData():為指定名稱獲取存儲在當前應用程序域中的值。
[SecuritySafeCritical]
public object GetData(string name)
{
if (name == null)
throw new ArgumentNullException("name");
switch (AppDomainSetup.Locate(name))
{
case -1:
if (name.Equals(AppDomainSetup.LoaderOptimizationKey))
return (object) this.FusionStore.LoaderOptimization;
object syncRoot = ((ICollection) this.LocalStore).SyncRoot;
bool lockTaken = false;
object[] objArray;
try
{
Monitor.Enter(syncRoot, ref lockTaken);
this.LocalStore.TryGetValue(name, out objArray);
}
finally
{
if (lockTaken)
Monitor.Exit(syncRoot);
}
if (objArray == null)
return (object) null;
if (objArray[1] != null)
((IPermission) objArray[1]).Demand();
return objArray[0];
case 0:
return (object) this.FusionStore.ApplicationBase;
case 1:
return (object) this.FusionStore.ConfigurationFile;
case 2:
return (object) this.FusionStore.DynamicBase;
case 3:
return (object) this.FusionStore.DeveloperPath;
case 4:
return (object) this.FusionStore.ApplicationName;
case 5:
return (object) this.FusionStore.PrivateBinPath;
case 6:
return (object) this.FusionStore.PrivateBinPathProbe;
case 7:
return (object) this.FusionStore.ShadowCopyDirectories;
case 8:
return (object) this.FusionStore.ShadowCopyFiles;
case 9:
return (object) this.FusionStore.CachePath;
case 10:
return (object) this.FusionStore.LicenseFile;
case 11:
return (object) (bool) (this.FusionStore.DisallowPublisherPolicy ? 1 : 0);
case 12:
return (object) (bool) (this.FusionStore.DisallowCodeDownload ? 1 : 0);
case 13:
return (object) (bool) (this.FusionStore.DisallowBindingRedirects ? 1 : 0);
case 14:
return (object) (bool) (this.FusionStore.DisallowApplicationBaseProbing ? 1 : 0);
case 15:
return (object) this.FusionStore.GetConfigurationBytes();
default:
return (object) null;
}
}
每一個AppDomain有自己的環境屬性集,可以通過SetData和GetData方法訪問,在這里給出了GetData()方法的源碼。該方法接收一個string參數,預定義應用程序域屬性的名稱,或已定義的應用程序域屬性的名稱。返回一個屬性的值,或 null(如果屬性不存在)。AppDomainSetup類為一個封閉類,表示可以添加到System.AppDomain的實例的程序集綁定信息。
(3).CreateDomain:使用指定的名稱、證據和應用程序域設置信息創建新的應用程序域。
[SecuritySafeCritical]
[SecurityPermission(SecurityAction.Demand, ControlAppDomain = true)]
public static AppDomain CreateDomain(string friendlyName, Evidence securityInfo, AppDomainSetup info)
{
return AppDomain.InternalCreateDomain(friendlyName, securityInfo, info);
}
該方法存在幾個重載,接收三個參數,域的友好名稱。friendlyName:此友好名稱可在用戶界面中顯示以標識域;securityInfo:確定代碼標識的證據,該代碼在應用程序域中運行。傳遞 null 以使用當前應用程序域的證據。info:包含應用程序域初始化信息的對象。該方法返回一個新創建的應用程序域。
(4).ExecuteAssembly():使用指定的證據和實參執行指定文件中包含的程序集。
[Obsolete("Methods which use evidence to sandbox are obsolete and will be removed in a future release of the .NET Framework. Please use an overload of ExecuteAssembly which does not take an Evidence parameter. See http://go.microsoft.com/fwlink/?LinkID=155570 for more information.")]
public int ExecuteAssembly(string assemblyFile, Evidence assemblySecurity, string[] args)
{
if (assemblySecurity != null && !this.IsLegacyCasPolicyEnabled)
throw new NotSupportedException(Environment.GetResourceString("NotSupported_RequiresCasPolicyImplicit"));
RuntimeAssembly assembly = (RuntimeAssembly) Assembly.LoadFrom(assemblyFile, assemblySecurity);
if (args == null)
args = new string[0];
return this.nExecuteAssembly(assembly, args);
}
當創建一個AppDomain后,可以使用一系列技術強制它加載和執行代碼,可以采用ExecuteAssembly方法。該方法將目標AppDomain加載到程序集中,並且執行其主入口點。在父AppDomain種,ExecuteAssembly方法不會加載或者初始化指定的程序集。ExecuteAssembly是一個同步的例程,這就意味着調用者將被阻塞,直到程序的Main方法把控制權交還運行時。
ExecuteAssembly方法存在幾個重載版本,在這里只拿出一個版本來說明。該方法接收三個參數,assemblyFile:包含要執行程序集的文件的名稱;assemblySecurity:為程序集提供的證據;args:程序集的入口點的實參。該方法返回 程序集的入口點返回的值。該方法使用Assembly.LoadFrom來加載程序集。有關程序集的內容將在下一篇講解。
(5).DoCallBack():在另一個應用程序域中執行代碼,該應用程序域由指定的委托標識。
public void DoCallBack(CrossAppDomainDelegate callBackDelegate)
{
if (callBackDelegate == null)
throw new ArgumentNullException("callBackDelegate");
callBackDelegate();
}
這個指定方法必須是靜態的,並且它的簽名與CrossAppDomainDelegate簽名匹配。
三.程序集操作實例:
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
namespace AppDomainToolkit
{
/// <summary>
/// 用於確定加載器應加載哪些加載上下文程序集。
/// </summary>
public enum LoadMethod
{
/// <summary>
/// 將程序集加載到LoadFrom上下文中,這將使程序集及其所有引用被發現
///並加載到目標應用程序域中。 盡管它對DLL地獄的傾向,這可能是去的方式
/// default,只要確保將應用程序的基本目錄傳遞給AssemblyResolver實例等
///可以正確解析引用。 這也允許同時加載同名的多個程序集
///維護單獨的文件名。 這是推薦的方式。
/// </summary>
LoadFrom,
/// <summary>
/// 使用原始文件名將組合件加載到內存中。 這將以匿名方式加載程序集,因此它不會有
///一個加載上下文。 使用這個,如果你想要的位加載,但確保通過這個文件所在的目錄
/// AssemblyResolver實例,以便您可以再次找到它。 這是類似於LoadFrom,除非你沒有得到免費
///通過融合查找已經存在的程序集名稱。 使用它可以更好地控制匯編文件加載。
/// </summary>
LoadFile,
/// <summary>
/// 使用原始文件名將目標程序集的位加載到內存中。 這本質上是一個動態組件
///為所有的CLR關心。 你將永遠不能找到這個與程序集解析器,所以不要使用這,除非你看
///按名稱。 小心這一個。
/// </summary>
LoadBits
}
/// <summary>
/// 這個類將會把程序集加載到它加載到的任何應用程序域中。 這只是一個簡單的方便
/// wrapper環繞靜態Assembly.Load *方法,主要的好處是能夠加載程序集
///匿名按位。 當您以這種方式加載程序集時,不會有任何DLL文件的鎖定。
/// </summary>
public class AssemblyLoader : MarshalByRefObject, IAssemblyLoader
{
#region Public Methods
/// <inheritdoc />
/// <remarks>
/// 如果此實例的LoadMethod設置為LoadBits,並且PDB文件的路徑未指定,那么我們將嘗試猜測
///到PDB的路徑並加載它。 注意,如果一個程序集被加載到內存中而沒有調試符號,那么
/// image將被拋出。 警惕這個。 使用LoadBits方法加載程序集不會鎖定
/// DLL文件,因為整個程序集被加載到內存中並且文件句柄被關閉。 但是,
///以這種方式加載的程序集不會有與之關聯的位置,因此您必須鍵入程序集
///它的強名。 當將同一程序集的多個版本加載到一個程序集時,這可能會導致問題
///應用程序域。
/// </remarks>
public Assembly LoadAssembly(LoadMethod loadMethod, string assemblyPath, string pdbPath = null)
{
Assembly assembly = null;
switch (loadMethod)
{
case LoadMethod.LoadFrom:
assembly = Assembly.LoadFrom(assemblyPath);
break;
case LoadMethod.LoadFile:
assembly = Assembly.LoadFile(assemblyPath);
break;
case LoadMethod.LoadBits:
// Attempt to load the PDB bits along with the assembly to avoid image exceptions.
pdbPath = string.IsNullOrEmpty(pdbPath) ? Path.ChangeExtension(assemblyPath, "pdb") : pdbPath;
// Only load the PDB if it exists--we may be dealing with a release assembly.
if (File.Exists(pdbPath))
{
assembly = Assembly.Load(
File.ReadAllBytes(assemblyPath),
File.ReadAllBytes(pdbPath));
}
else
{
assembly = Assembly.Load(File.ReadAllBytes(assemblyPath));
}
break;
default:
// In case we upadate the enum but forget to update this logic.
throw new NotSupportedException("The target load method isn't supported!");
}
return assembly;
}
/// <inheritdoc />
/// <remarks>
/// 這個實現將執行目標程序集的盡力負載,它是必需的引用
///進入當前應用程序域。 .NET框架在我們允許使用的調用上鎖定我們
///當加載這些程序集時,所以我們需要依賴於AssemblyResolver實例附加的
/// AppDomain為了加載我們想要的方式。
/// </remarks>
public IList<Assembly> LoadAssemblyWithReferences(LoadMethod loadMethod, string assemblyPath)
{
var list = new List<Assembly>();
var assembly = this.LoadAssembly(loadMethod, assemblyPath);
list.Add(assembly);
foreach (var reference in assembly.GetReferencedAssemblies())
{
list.Add(Assembly.Load(reference));
}
return list;
}
/// <inheritdoc />
/// <remarks>
/// Just a simple call to AppDomain.CurrentDomain.GetAssemblies(), nothing more.
/// </remarks>
public Assembly[] GetAssemblies()
{
return AppDomain.CurrentDomain.GetAssemblies();
}
#endregion
}
}
四.總結:
本文主要講解了應用程序域的相關概念,本系列主要講解.NET對象的跨應用程序域的傳遞,由於設計應用程序域的內容,所以本文主要講解了一些基本概念,以及一些基本的對象,對於應用程序域包含的程序集的相關內容將在下面進行操作。在實際的項目中,很少直接取操作應用程序域,比較多的是直接操作程序集,所以在本文的最后給出了一個就暗淡的程序集的操作方法。

