基于AppDomain的"插件式"开发

时间:2021-03-16 04:56:20


基于AppDomain的"插件式"开发

2011-08-01 09:58 by 空逸云, 4226 阅读, 42 评论, 收藏,编辑

很多时候,我们都想使用(开发)USB式(热插拔)的应用,例如,开发一个WinForm应用,并且这个WinForm应用能允许开发人员定制扩展插件,又例如,我们可能维护着一个WinService管理系统,这个WinService系统管理的形形色色各种各样的服务,这些服务也是各个"插件式"的类库,例如:

    public interface IJob
{
void Run(DateTime time);
}

public class CollectUserInfo : IJob
{

public void Run(DateTime time)
{
//doing some thing...
}
}

我们提供了一个IJob接口,所有"服务"都继承该接口,然后做相关的配置,在服务启动时,就可以根据配置,反射加载程序集,执行我们预期的任务.

更新程序集(dll/exe)

服务/插件程序(后面只称为服务,虽然两者应用不同,但是在此处他们所运用的原理和作用是相同的 :-) )很健稳的运行着.但在服务/插件程序运行一段时间之后,某些"插件"的业务需求发生的变化,或者版本升级等种种外部原因,导致我们对原本的"插件"程序集进行了升级(可能从v1.0升级至v2.0).当我们想像Asp.net应用一样.把新的dll替换旧dll的时候,错误发生了.

基于AppDomain的"插件式"开发

发生该错误的原因很简单,因为我们的程序中已经调用了该dll,那么在CLR加载该dll到文件流中也给其加了锁,所以,当我们要进行覆盖,修改,删除的时候自然就无法操作该文件了.那我们该怎么做?为什么Asp.net可以直接覆盖?

AppDomain登场

我们知道,AppDomain是.Net平台里一个很重要的特性,在.Net以前,每个程序是"封装"在不同的进程中的,这样导致的结果就造就占用资源大,可复用性低等缺点.而AppDomain在同一个进程内划分出多个"域",一个进程可以运行多个应用,提高了资源的复用性,数据通信等.详见应用程序域

CLR在启动的时候会创建系统域(System Domain),共享域(Shared Domain)和默认域(Default Domain),系统域与共享域对于用户是不可见的,默认域也可以说是当前域,它承载了当前应用程序的各类信息(堆栈),所以,我们的一切操作都是在这个默认域上进行."插件式"开发很大程度上就是依靠AppDomain来进行.

"热插拔"实现说明

当加载了一个程序集之后,该程序集就会被加入到指定AppDomain中,按照原来的想法,要实现"热插拔",只要在需要使用该"插件"的时候,加载该"插件"的程序集(dll),使用结束后,卸载掉该程序集便可达到我们预期的效果.加载程序集很简单,.C#提供一个Assembly类,方便又快捷.

var  _assembly = Assembly.LoadFrom(assemblyFile);

Assembly提供了数个加载方法详见Assembly类.

然后,C#却没有提供卸载程序集的方法,唯一能卸载程序集的方法只有卸载该程序集所在的AppDomain,这样,该AppDomain下的程序集都会被释放.知道这一点,我们便可以利用AppDomain来达到我们预期的效果.

AppDomain实现"热插拔"

首先,我们需要先实例化一个新AppDomain作为"插件"的宿主.在实例化一个Domain之前,先声明该Domain的一些基本配置信息

            AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationName = "ApplicationLoader";
setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
setup.PrivateBinPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "private");
setup.CachePath = setup.ApplicationBase;
setup.ShadowCopyFiles = "true"; //启用影像复制程序集
setup.ShadowCopyDirectories = setup.ApplicationBase;
AppDomain.CurrentDomain.SetShadowCopyFiles();

setup.ShadowCopyFiles = "true";这句很重要,其作用就是启用影像复制程序集,什么是影像复制程序集,复制程序集是保证"热插拔"

实现的主要工作.AppDomain加载程序集的时候,如果没有ShadowCopyFiles,那就直接加载程序集,结果就是程序集被锁定,相反,如果启用了ShadowCopyFiles,则CLR会将准备加载的程序集拷贝一份至CachePath,再加载CachePath的这一份程序集,这样原程序集也就不会被锁定了. AppDomain.CurrentDomain.SetShadowCopyFiles();的作用就是当前AppDomain也启用ShadowCopyFiles,在此,当前AppDomain也就是前面我们说过的那个默认域(Default Domain),为什么当前域也要启用ShadowCopyFiles呢?

主AppDomian在调用子AppDomain提供过来的类型,方法,属性的时候,也会将该程序集添加到自身程序集引用当中去,所以,"插件"程序集就被主AppDomain锁定,这也是为什么创建了单独的AppDomain程序集也不能删除,替换(释放)的根本原因

利用SOS,可以很清楚的看到这一点

0:018> !dumpdomain
--------------------------------------
System Domain: 5b912478
LowFrequencyHeap: 5b912784
HighFrequencyHeap: 5b9127d0
StubHeap: 5b91281c
Stage: OPEN
Name: None
--------------------------------------
Shared Domain: 5b912140
LowFrequencyHeap: 5b912784
HighFrequencyHeap: 5b9127d0
StubHeap: 5b91281c
Stage: OPEN
Name: None
Assembly: 00109de0 [C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll]
ClassLoader: 00110f68
Module Name
58631000 C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll

--------------------------------------
Domain 1: 000f4598
LowFrequencyHeap: 000f4914
HighFrequencyHeap: 000f4960
StubHeap: 000f49ac
Stage: OPEN
SecurityDescriptor: 000f5568
Name: AppDomainTest.exe
Assembly: 00109de0 [C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll]
ClassLoader: 00110f68
SecurityDescriptor: 001097b0
Module Name
58631000 C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll

Assembly: 0011d448 [E:\Test\AppDomainTest\AppDomainTest\bin\Debug\AppDomainTest.exe]
ClassLoader: 00117fd0
SecurityDescriptor: 0011d3c0
Module Name
001c2e9c E:\Test\AppDomainTest\AppDomainTest\bin\Debug\AppDomainTest.exe

Assembly: 00131370 [C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Windows.Forms\v4.0_4.0.0.0__b77a5c561934e089\System.Windows.Forms.dll]
ClassLoader: 0011fa00
SecurityDescriptor: 001299a0
Module Name
579c1000 C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Windows.Forms\v4.0_4.0.0.0__b77a5c561934e089\System.Windows.Forms.dll

Assembly: 00131400 [C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Drawing\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Drawing.dll]
ClassLoader: 00131490
SecurityDescriptor: 0012e9c0
Module Name
62661000 C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Drawing\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Drawing.dll

Assembly: 00131d20 [C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll]
ClassLoader: 00133d08
SecurityDescriptor: 0012f078
Module Name
5aa81000 C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll

Assembly: 00131ed0 [C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Configuration\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Configuration.dll]
ClassLoader: 001415a8
SecurityDescriptor: 0012f430
Module Name
5a981000 C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Configuration\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Configuration.dll

Assembly: 00132080 [C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Xml\v4.0_4.0.0.0__b77a5c561934e089\System.Xml.dll]
ClassLoader: 00141620
SecurityDescriptor: 0012f5c8
Module Name
546e1000 C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Xml\v4.0_4.0.0.0__b77a5c561934e089\System.Xml.dll

Assembly: 00132ce0 [E:\Test\AppDomainTest\AppDomainTest\bin\Debug\CrossDomainController.dll]
ClassLoader: 001b3450
SecurityDescriptor: 06f94560
Module Name
001c7428 E:\Test\AppDomainTest\AppDomainTest\bin\Debug\CrossDomainController.dll

Assembly: 00132350 [C:\Users\kong\AppData\Local\assembly\dl3\6ZYK3XE9.86Q\2AQ35O7C.VHE\1f704bbb\b7cca5cf_8c4fcc01\ShowHelloPlug.DLL]
ClassLoader: 001b32e8
SecurityDescriptor: 070a8620
Module Name
001c7d78 C:\Users\kong\AppData\Local\assembly\dl3\6ZYK3XE9.86Q\2AQ35O7C.VHE\1f704bbb\b7cca5cf_8c4fcc01\ShowHelloPlug.DLL

--------------------------------------
Domain 2: 06fd0238
LowFrequencyHeap: 06fd05b4
HighFrequencyHeap: 06fd0600
StubHeap: 06fd064c
Stage: OPEN
SecurityDescriptor: 06724510
Name: ApplicationLoaderDomain
Assembly: 00109de0 [C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll]
ClassLoader: 00110f68
SecurityDescriptor: 06f93bd0
Module Name
58631000 C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll

Assembly: 00132e90 [E:\Test\AppDomainTest\AppDomainTest\bin\Debug\ApplicationLoader\assembly\dl3\c91a2898\f6f7f865_9a4fcc01\CrossDomainController.DLL]
ClassLoader: 001b3540
SecurityDescriptor: 06f92be0
Module Name
00a833c4 E:\Test\AppDomainTest\AppDomainTest\bin\Debug\ApplicationLoader\assembly\dl3\c91a2898\f6f7f865_9a4fcc01\CrossDomainController.DLL

Assembly: 001330d0 [E:\Test\AppDomainTest\AppDomainTest\bin\Debug\ApplicationLoader\assembly\dl3\32519346\b7cca5cf_8c4fcc01\ShowHelloPlug.DLL]
ClassLoader: 001b39f0
SecurityDescriptor: 06f92f98
Module Name
00a83adc E:\Test\AppDomainTest\AppDomainTest\bin\Debug\ApplicationLoader\assembly\dl3\32519346\b7cca5cf_8c4fcc01\ShowHelloPlug.DLL

除了新建的AppDomain(Domain2)中的Module引用了ShowHelloPlug.dll,默认域(Domian1)也有ShowHelloPlug.dll的

程序集引用.

应用程序域之间的通信

每个AppDomain都有自己的堆栈,内存块,也就是说它们之间的数据并非共享了.若想共享数据,则涉及到应用程序域之间的通信.C#提供了MarshalByRefObject类进行跨域通信,那么,我们必须提供自己的跨域访问器.

    public class RemoteLoader : MarshalByRefObject
{
private Assembly _assembly;

public void LoadAssembly(string assemblyFile)
{
try
{
_assembly = Assembly.LoadFrom(assemblyFile);
//return _assembly;
}
catch (Exception ex)
{
throw ex;
}
}

public T GetInstance<T>(string typeName) where T : class
{
if (_assembly == null) return null;
var type = _assembly.GetType(typeName);
if (type == null) return null;
return Activator.CreateInstance(type) as T;
}

public void ExecuteMothod(string typeName, string methodName)
{
if (_assembly == null) return;
var type = _assembly.GetType(typeName);
var obj = Activator.CreateInstance(type);
Expression<Action> lambda = Expression.Lambda<Action>(Expression.Call(Expression.Constant(obj), type.GetMethod(methodName)), null);
lambda.Compile()();
}
}

为了更好的操作这个跨域访问器,接下来我构建了一个名为AssemblyDynamicLoader的类,它内部封装了RemoteLoader类

的操作.

    public class AssemblyDynamicLoader
{
private AppDomain appDomain;
private RemoteLoader remoteLoader;
public AssemblyDynamicLoader()
{
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationName = "ApplicationLoader";
setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
setup.PrivateBinPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "private");
setup.CachePath = setup.ApplicationBase;
setup.ShadowCopyFiles = "true";
setup.ShadowCopyDirectories = setup.ApplicationBase;
AppDomain.CurrentDomain.SetShadowCopyFiles();
this.appDomain = AppDomain.CreateDomain("ApplicationLoaderDomain", null, setup);
String name = Assembly.GetExecutingAssembly().GetName().FullName;
this.remoteLoader = (RemoteLoader)this.appDomain.CreateInstanceAndUnwrap(name, typeof(RemoteLoader).FullName);
}

public void LoadAssembly(string assemblyFile)
{
remoteLoader.LoadAssembly(assemblyFile);
}

public T GetInstance<T>(string typeName) where T : class
{
if (remoteLoader == null) return null;
return remoteLoader.GetInstance<T>(typeName);
}

public void ExecuteMothod(string typeName, string methodName)
{
remoteLoader.ExecuteMothod(typeName, methodName);
}

public void Unload()
{
try
{
if (appDomain == null) return;
AppDomain.Unload(this.appDomain);
this.appDomain = null;
}
catch (CannotUnloadAppDomainException ex)
{
throw ex;
}
}
}

这样我们每次都是通过AssemblyDynamicLoader类进行跨域的访问.

            AppDomain.CurrentDomain.SetShadowCopyFiles();
this.appDomain = AppDomain.CreateDomain("ApplicationLoaderDomain", null, setup);
String name = Assembly.GetExecutingAssembly().GetName().FullName;
this.remoteLoader = (RemoteLoader)this.appDomain.CreateInstanceAndUnwrap(name, typeof(RemoteLoader).FullName);

通过我们前面构造的一个AppDomainSetup,构建了一个我们所需的AppDomain,并且在这个appDomain中构建了

一个RemoteLoader类的实例(此时该实例已具备跨域访问能力,也就是说我们在主域能获取子域内部的数据信息).目前RemoteLoader只提供了少数的几个方法.

跨域操作

下面,我们就模拟一次"插件式"的跨域操作.首先我们构造了一个窗体,其有以下元素.

基于AppDomain的"插件式"开发

选择程序集路径之后,加载程序集,然后就触发程序集指定类型(通过配置获取)的特定操作.这里我们定义了一个公共接口,它是所有"插件"操作的主要入口了.

    public interface IPlug
{
void Run();
}

随后定义了一个实现该接口的类.

    [Serializable]
public class ShowHelloPlug : IPlug
{
public void Run()
{
MessageBox.Show("Hello World...");
}
}

这个"插件"的工作很简单.仅仅弹出一个对话框,说声"Hello World…",接下来将其编译成一个dll.

基于AppDomain的"插件式"开发

回到界面,选择刚才编译的Dll,然后直接加载.

基于AppDomain的"插件式"开发

到这里,我们的工作完成了一半了.呼呼.OK.我们的需求发生了变化,不再是弹出Hello World了.而时候弹出Hi,I'm Kinsen,我们修改刚才的子类,并再编译一次.再将Dll替换刚才的Dll,这次,Dll没有没锁定(因为我们前面启用了ShadowCopyFiles.).再加载一下程序集,你会发现结果并不是"Hi,I'm Kinsen",而是"Hello World.."为什么会这样呢?这时候,借助SOS的力量(前面有SOS结果).

我们发现Domain1(Default Domain)和Domain2(新创建Domain)都引用了程序集ShowHelloPlug.DLL,但是两个引用的Dll地址却不相同,这是因为启用了ShadowCopyFiles,它们加载的都是各自程序集的备份,我们根据Domain2的Assembly地址查看ShowHelloPlug的编译代码.

0:011> !dumpmt 00fc40ac     
00fc40ac is not a MethodTable
0:011> !dumpmd 00fc40ac
Method Name: Plug.ShowHelloPlug.Run()
Class: 046812b4
MethodTable: 00fc40bc
mdToken: 06000001
Module: 00fc3adc
IsJitted: no
CodeAddr: ffffffff
Transparency: Critical

从IsJitted为no可以看出,该程序集并没有被调用,那调用的是谁?我们再次查看Domain1(Default Domain

)中的ShowHelloPlug.

0:011> !dumpmd 001f8240      
Method Name: Plug.ShowHelloPlug.Run()
Class: 004446e4
MethodTable: 001f8250
mdToken: 06000001
Module: 001f7d78
IsJitted: yes
CodeAddr: 00430de0
Transparency: Critical

已知每个AppDomain都有自己的堆栈信息,各自不互相影响,所以,当我们在主域中获取到了子域中的数据,并非新建一个指向该实例的引用,而是在自己的堆栈上开辟出一块空间"深度拷贝"该实例,那么必然就达不到我们我需的结果.

子域内部调用

那么为了达到我们预期的效果,我们必须在子域内部执行我们所需的操作(调用),所以在RemoteLoader类中增加了一个Execute方法

        public void ExecuteMothod(string typeName, string methodName)
{
if (_assembly == null) return;
var type = _assembly.GetType(typeName);
var obj = Activator.CreateInstance(type);
Expression<Action> lambda = Expression.Lambda<Action>(Expression.Call(Expression.Constant(obj), type.GetMethod(methodName)), null);
lambda.Compile()();
}

此处我暂时只想到了利用反射调用,这样的代价就是调用所需消耗的资源更多,效率低下.目前还没有

想出较好的解决方案,有经验的童鞋欢迎交流.

这样外部的调用就变成以下

loader = new AssemblyDynamicLoader();
loader.LoadAssembly(txt_dllName.Text);
//var obj = loader.GetInstance<IPlug>("Plug.ShowHelloPlug");
//obj.Run();
loader.ExecuteMothod("Plug.ShowHelloPlug", "Run");

现在在将Dll替换,结果正常.

基于AppDomain的"插件式"开发

尾声

做"插件式"开发,除了利用AppDomain之外,也有童鞋给出了另一种解决方案,也就是在加载Dll的时候,先将Dll在内存中复制一份,这样原来的Dll也就不会被锁定了.详见插件的“动态替换”.

以上实例本人皆做过实验,但可能还存在一定不足或概念错误,若有不当之处,欢迎各位童鞋批评指点.

更多

通过应用程序域AppDomain加载和卸载程序集

什么是的AppDomain


System.AppDomain类

   进程是存在独立的内存和资源的,但是AppDomain仅仅是逻辑上的一种抽象。一个process可以存在多个AppDomain。各个AppDomain之间的数据时相互独立的。一个线程可以穿梭多个AppDomain。

一、属性

ActivationContext           获取当前应用程序域的激活上下文。
ApplicationIdentity           获得应用程序域中的应用程序标识。
ApplicationTrust            获取说明授予应用程序的权限以及应用程序是否拥有允许其运行的信任级别的信息。
BaseDirectory              获取基目录,它由程序集冲突解决程序用来探测程序集。
CurrentDomain              获取当前 Thread 的当前应用程序域。
DomainManager             获得初始化应用程序域时主机提供的域管理器。
DynamicDirectory             获取目录,它由程序集冲突解决程序用来探测动态创建的程序集。
Evidence                获取与该应用程序域关联的 Evidence。
FriendlyName              获取此应用程序域的友好名称。
Id                     获得一个整数,该整数唯一标识进程中的应用程序域。
IsFullyTrusted               获取一个值,该值指示加载到当前应用程序域的程序集是否是以完全信任方式执行的。
IsHomogenous              获取一个值,该值指示当前应用程序域是否拥有一个为加载到该应用程序域的所有程序集授予的权限集。
MonitoringIsEnabled           获取或设置一个值,该值指示是否对当前进程启用应用程序域的 CPU 和内存监视。 一旦对进程启用了监视,则无法将其禁用。
MonitoringSurvivedMemorySize       获取上次完全阻止回收后保留下来的、已知由当前应用程序域引用的字节数。
MonitoringSurvivedProcessMemorySize   获取进程中所有应用程序域的上次完全阻止回收后保留下来的总字节数。
MonitoringTotalAllocatedMemorySize    获取自从创建应用程序域后由应用程序域进行的所有内存分配的总大小(以字节为单位,不扣除已回收的内存)。
MonitoringTotalProcessorTime       获取自从进程启动后所有线程在当前应用程序域中执行时所使用的总处理器时间。
PermissionSet        获取沙盒应用程序域的权限集。
RelativeSearchPath     获取基目录下的路径,在此程序集冲突解决程序应探测专用程序集。
SetupInformation     获取此实例的应用程序域配置信息。
ShadowCopyFiles     获取应用程序域是否配置为影像副本文件的指示。

基于AppDomain的"插件式"开发
    public class Program
{
static void Main(string[] args)
{
AppDomain appdomain
= AppDomain.CurrentDomain; //获取当前 Thread 的当前应用程序域。
Console.WriteLine(appdomain.Id); //输出 1 获得一个整数,唯一标识进程中的应用程序域。
Console.WriteLine(appdomain.ActivationContext); //输出 空白 获取当前应用程序域的激活上下文。
Console.WriteLine(appdomain.ApplicationIdentity); //输出 空白 获得应用程序域中的应用程序标识。

ApplicationTrust AT
= appdomain.ApplicationTrust; //获取说明授予应用程序的权限以及应用程序是否拥有允许其运行的信任级别的信息。
Console.WriteLine(appdomain.ApplicationTrust); //输出 System.Security.Policy.ApplicationTrust

Console.WriteLine(appdomain.BaseDirectory);
//输出 F:\xxx\xxx\ConsoleApplication1\bin\Debug\ 获取基目录,它由程序集冲突解决程序用来探测程序集。

AppDomainManager Adm
= appdomain.DomainManager; //获得初始化应用程序域时主机提供的域管理器。
Console.WriteLine(Adm.EntryAssembly.Location); //输出 F:\xxx\xxx\ConsoleApplication1\bin\Debug\ConsoleApplication1.exe
Console.WriteLine(appdomain.DomainManager); //输出 Microsoft.VisualStudio.HostingProcess.VSHostAppDomainManager

Console.WriteLine(appdomain.DynamicDirectory);
//输出 空白 获取目录,它由程序集冲突解决程序用来探测动态创建的程序集。

Evidence ed
= appdomain.Evidence; //另外一个东西了 获取与该应用程序域关联的 Evidence。
Console.WriteLine(appdomain.Evidence.ToString()); //输出 System.Security.Policy.Evidence

Console.WriteLine(appdomain.FriendlyName);
//输出 ConsoleApplication1.vshost.exe 获取此应用程序域的友好名称。

Console.WriteLine(appdomain.IsFullyTrusted);
//输出 True 获取一个值,该值指示加载到当前应用程序域的程序集是否是以完全信任方式执行的。

Console.WriteLine(appdomain.IsHomogenous);
//输出 True 获取一个值,该值指示当前应用程序域是否拥有一个为加载到该应用程序域的所有程序集授予的权限集。

Console.WriteLine(AppDomain.MonitoringIsEnabled);
//输出 False 获取或设置一个值,该值指示是否对当前进程启用应用程序域的 CPU 和内存监视。 一旦对进程启用了监视,则无法将其禁用。

AppDomain.MonitoringIsEnabled
= true;
//要上面那个属性启用之后,这个属性才能用
Console.WriteLine(appdomain.MonitoringSurvivedMemorySize); //输出 0 获取上次完全阻止回收后保留下来的、已知由当前应用程序域引用的字节数。
Console.WriteLine(appdomain.MonitoringTotalAllocatedMemorySize); //输出 0 获取自从创建应用程序域后由应用程序域进行的所有内存分配的总大小(以字节为单位,不扣除已回收的内存)。
Console.WriteLine(appdomain.MonitoringTotalProcessorTime); //输出 00:00:00 获取自从进程启动后所有线程在当前应用程序域中执行时所使用的总处理器时间。

System.Security.PermissionSet ps
= appdomain.PermissionSet; //获取沙盒应用程序域的权限集。

Console.WriteLine(appdomain.PermissionSet.ToString());
//输出 <PermissionSet class="System.Security.PermissionSet" version="1" Unrestricted="true"/>

Console.WriteLine(appdomain.RelativeSearchPath);
//输出 空白

Console.WriteLine(appdomain.SetupInformation);
//输出 AppDomainSetup 获取此实例的应用程序域配置信息。

Console.WriteLine(appdomain.ShadowCopyFiles);
//输出 False

File.WriteAllText(
@"D:\123.txt", appdomain.PermissionSet.ToString());

//SetupInformation 获取此实例的应用程序域配置信息。
//ShadowCopyFiles 获取应用程序域是否配置为影像副本文件的指示。

Console.ReadKey();
}
基于AppDomain的"插件式"开发

二、方法

名称 说明
ApplyPolicy             返回应用策略后的程序集显示名称。
CreateComInstanceFrom(String, String) 创建指定 COM 类型的新实例。 形参指定文件的名称,该文件包含含有类型和类型名称的程序集。
CreateDomain(String)        使用指定的名称新建应用程序域。
CreateInstance(String, String)   创建在指定程序集中定义的指定类型的新实例。
CreateInstanceAndUnwrap(String, String) 创建指定类型的新实例。 形参指定定义类型的程序集以及类型的名称。
CreateInstanceFrom(String, String)   创建在指定程序集文件中定义的指定类型的新实例。
CreateObjRef               创建一个对象,该对象包含生成用于与远程对象进行通信的代理所需的全部相关信息。 (继承自 MarshalByRefObject。)
DefineDynamicAssembly(AssemblyName, AssemblyBuilderAccess)   以指定名称和访问模式定义动态程序集。
DoCallBack           在另一个应用程序域中执行代码,该应用程序域由指定的委托标识。
ExecuteAssembly(String)     执行指定文件中包含的程序集。
ExecuteAssemblyByName(String)     在给定其显示名称的情况下执行程序集。
GetAssemblies      获取已加载到此应用程序域的执行上下文中的程序集。
GetData         为指定名称获取存储在当前应用程序域中的值。
GetLifetimeService      检索控制此实例的生存期策略的当前生存期服务对象。 (继承自 MarshalByRefObject。)
InitializeLifetimeService   通过防止创建租约来给予 AppDomain 无限生存期。 (重写 MarshalByRefObject.InitializeLifetimeService()。)
IsCompatibilitySwitchSet  获取可以为 null 的布尔值,该值指示是否设置了任何兼容性开关,如果已设置,则指定是否设置了指定的兼容性开关。
IsDefaultAppDomain      返回一个值,指示应用程序域是否是进程的默认应用程序域。
IsFinalizingForUnload     指示此应用程序域是否正在卸载以及公共语言运行时是否正在终止该域包含的对象。
Load(AssemblyName)    在给定 AssemblyName 的情况下加载 Assembly。
ReflectionOnlyGetAssemblies   返回已加载到应用程序域的只反射上下文中的程序集。
SetData(String, Object)   为指定的应用程序域属性分配指定值。
SetData(String, Object, IPermission)   将指定值分配给指定应用程序域属性,检索该属性时要求调用方具有指定权限。
SetPrincipalPolicy      指定在此应用程序域中执行时如果线程尝试绑定到用户,用户和标识对象应如何附加到该线程。
SetThreadPrincipal     设置在以下情况下要附加到线程的默认主体对象,即当线程在此应用程序域中执行时,如果线程尝试绑定到主体这种情况。
Unload           卸载指定的应用程序域。

基于AppDomain的"插件式"开发
 class Program
{
static void Main(string[] args)
{
AppDomain app
= AppDomain.CreateDomain("测试程序域"); //使用指定的名称创建应用程序域
ObjectHandle objHan = app.CreateInstance("MySpace", "MySpace.Person"); //创建指定程序集中指定的对象
var obj = objHan.Unwrap();
Console.WriteLine(obj.ToString());
//输出 MySpace.Person

object obj2 = app.CreateInstanceAndUnwrap("MySpace","MySpace.Person"); //创建指定程序中指定的对象,顺带执行了Unwrap方法
Console.WriteLine(obj2.ToString()); //输出 MySpace.Person

ObjectHandle objHan2
= app.CreateInstanceFrom(@"D:\MySpace.dll", "MySpace.Person"); //从指定的程序集中创建类的对象
object obj3 = objHan2.Unwrap();
Console.WriteLine(obj3.ToString());
//输出 MySpace.Person

//app.CreateObjRef();

app.ExecuteAssembly(
@"D:\ConsoleApplication1.exe"); //执行指定文件中包含的程序集 输出0123456789
//D:\ConsoleApplication1.exe的代码如下
//static void Main(string[] args)
//{
// for (int i = 0; i < 10; i++)
// {
// Console.WriteLine(i);
// }

// Console.ReadKey();
//}

//下面改动一下D:\ConsoleApplication1.exe,改为在D盘下创建一个文件,并写入一段文字
app.ExecuteAssembly(@"D:\ConsoleApplication1.exe"); //运行之后在D盘下创建了一个文件,并写入文字
//static void Main(string[] args)
//{
// File.WriteAllText(@"D:\AppDomainTest.txt","测试测试");
// Console.ReadKey();
//}
//注意,此次由于有Console.ReadKey(); 因此执行到这里要按两次键盘,让app程序域执行完毕才会又回到主程序域
Assembly[] assArr = app.GetAssemblies(); //获取加载到app应用程序域中的所有程序集

foreach (var ass in assArr)
{
Console.WriteLine(ass.FullName);
//诸如此类 ConsoleApplication1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
}

//给定Assembly的情况下装载程序集
app.Load("MySpace");
Console.WriteLine(app.IsDefaultAppDomain());
//输出 False 是否是进程默认的应用程序域

app.SetData(
"data1", "我靠"); //为指定的应用程序域属性分配指定值。
object o = app.GetData("data1");
Console.WriteLine(o.ToString());
//输出 我靠

//ApplyPolicy 返回应用策略后的程序集显示名称。
//CreateComInstanceFrom(String, String) 创建指定 COM 类型的新实例。 形参指定文件的名称,该文件包含含有类型和类型名称的程序集。
//CreateObjRef 创建一个对象,该对象包含生成用于与远程对象进行通信的代理所需的全部相关信息。 (继承自 MarshalByRefObject。)
//DefineDynamicAssembly(AssemblyName, AssemblyBuilderAccess) 以指定名称和访问模式定义动态程序集。
//DoCallBack 在另一个应用程序域中执行代码,该应用程序域由指定的委托标识。
//ExecuteAssemblyByName(String) 在给定其显示名称的情况下执行程序集。
//GetLifetimeService 检索控制此实例的生存期策略的当前生存期服务对象。 (继承自 MarshalByRefObject。)
//InitializeLifetimeService 通过防止创建租约来给予 AppDomain 无限生存期。 (重写 MarshalByRefObject.InitializeLifetimeService()。)
//IsCompatibilitySwitchSet 获取可以为 null 的布尔值,该值指示是否设置了任何兼容性开关,如果已设置,则指定是否设置了指定的兼容性开关。
//IsFinalizingForUnload 指示此应用程序域是否正在卸载以及公共语言运行时是否正在终止该域包含的对象。
//ReflectionOnlyGetAssemblies 返回已加载到应用程序域的只反射上下文中的程序集。
//SetPrincipalPolicy 指定在此应用程序域中执行时如果线程尝试绑定到用户,用户和标识对象应如何附加到该线程。
//SetThreadPrincipal 设置在以下情况下要附加到线程的默认主体对象,即当线程在此应用程序域中执行时,如果线程尝试绑定到主体这种情况。
//Unload 卸载指定的应用程序域。

Console.ReadKey();
}
}
基于AppDomain的"插件式"开发

 


通过应用程序域AppDomain加载和卸载程序集

微软装配车的大门似乎只为货物装载敞开大门,却将卸载工人拒之门外。车门的钥匙只有一把,若要获得还需要你费一些心思。我在学习Remoting的时候,就遇到一个扰人的问题,就是Remoting为远程对象仅提供Register的方法,如果你要注销时,只有另辟蹊径。细心的开发员,会发现Visual Studio.Net中的反射机制,同样面临这个问题。你可以找遍MSDN的所有文档,在Assembly类中,你永远只能看到Load方法,却无法寻觅到Unload的踪迹。难道我们装载了程序集后,就不能再将它卸载下来吗?

想一想这样一个场景。你通过反射动态加载了一个dll文件,如今你需要在未关闭程序的情况下,删除或覆盖该文件,那么结果会怎样?很遗憾,系统会提示你无法访问该文件。事实上该文件正处于被调用的状态,此时要对该文件进行修改,就会出现争用的情况。

显然,为程序集提供卸载功能是很有必要的,但为什么微软在其产品中不提供该功能呢?CLR 产品单元经理(Unit Manager) Jason Zander 在文章 Why isn't there an Assembly.Unload method? 中解释了没有实现该功能的原因。Flier_Lu在其博客里(Assembly.Unload)有详细的中文介绍。文中介绍了解决卸载程序集的折中方法。Eric Gunnerson在文章《AppDomain 和动态加载》中也提到:Assembly.Load() 通常运行良好,但程序集无法独立卸载(只有 AppDomain 可以卸载)。Enrico Sabbadin 在文章《Unload Assemblies From an Application Domain》也有相关VB.Net实现该功能的相关说明。

尤其是Flier_Lu的博客里已经有了很详细的代码。不过,这些代码没有详细地说明。我在我的项目中也需要这一项功能。这段代码给了我很大的提示。但在实际的实现中,还是遇到一些具体的问题。所以我还是想再谈谈我的体会。

通过AppDomain来实现程序集的卸载,这个思路是非常清晰的。由于在程序设计中,非特殊的需要,我们都是运行在同一个应用程序域中。由于程序集的卸载存在上述的缺陷,我们必须要关闭应用程序域,方可卸载已经装载的程序集。然而主程序域是不能关闭的,因此唯一的办法就是在主程序域中建立一个子程序域,通过它来专门实现程序集的装载。一旦要卸载这些程序集,就只需要卸载该子程序域就可以了,它并不影响主程序域的执行。

不过现在看来,最主要的问题不是子程序域如何创建,关键是我们必须实现一种机制,来达到两个程序域之间完成通讯的功能。如果大家熟悉Remoting,就会想到这个问题不是和Remoting的机制有几分相似之处吗?那么答案就可以呼之欲出了,对了,就是使用代理的方法!不过与Remoting不同的是两个程序域之间的关系。因为子程序域是在主程序域中建立的,因此对该域的控制显然就与Remoting不相同了。

我想先用一副图来表述实现的机制:基于AppDomain的"插件式"开发

说明:
1、Loader类提供创建子程序域和卸载程序域的方法;
2、RemoteLoader类提供装载程序集方法;
3、Loader类获得RemoteLoader类的代理对象,并调用RemoteLoader类的方法;
4、RemoteLoader类的方法在子程序域中完成;
5、Loader类和RemoteLoader类均放在AssemblyLoader.dll程序集文件中;

我们再来看代码:
Loader类:

SetRemoteLoaderObject()方法:

  private AppDomain domain = null;
  private Hashtable domains = new Hashtable();  
  private RemoteLoader rl = null;
基于AppDomain的"插件式"开发public RemoteLoader SetRemoteLoaderObject(string dllName)
基于AppDomain的"插件式"开发
{
基于AppDomain的"插件式"开发    AppDomainSetup setup 
= new AppDomainSetup();            
基于AppDomain的"插件式"开发    setup.ShadowCopyFiles 
= "true";
基于AppDomain的"插件式"开发    domain 
= AppDomain.CreateDomain(dllName,null,setup);
基于AppDomain的"插件式"开发            
基于AppDomain的"插件式"开发    domains.Add(dllName,domain);    
基于AppDomain的"插件式"开发    
try
基于AppDomain的"插件式"开发    
{
                rl = (AssemblyLoader.RemoteLoader)domain.CreateInstanceFromAndUnwrap(
                "AssemblyLoader.dll","AssemblyLoader.RemoteLoader"); 基于AppDomain的"插件式"开发        
基于AppDomain的"插件式"开发    }

基于AppDomain的"插件式"开发    
catch
基于AppDomain的"插件式"开发    
{
基于AppDomain的"插件式"开发        
throw new Exception();
基于AppDomain的"插件式"开发    }

基于AppDomain的"插件式"开发}


代码中的变量rl为RemoteLoader类对象,在Loader类中是其私有成员。SetRemoteLoaderObject()方法实际上提供了两个功能,一是创建了子程序域,第二则是获得了RemoteLoader类对象。

请大家一定要注意语句:
rl = (AssemblyLoader.RemoteLoader)domain.CreateInstanceFromAndUnwrap("AssemblyLoader.dll","AssemblyLoader.RemoteLoader");

这条语句就是实现两个程序域之间通讯的关键。因为Loader类是在主程序域中,RemoteLoader类则是在子程序域中。如果我们在Loader类即主程序域中显示实例化RemoteLoader类对象rl,此时调用rl的方法,实际上是在主程序域中调用的。因此,我们必须使用代理的方式,来获得rl对象,这就是CreateInstanceFromAndUnwrap方法的目的。其中参数一为要创建类对象的程序集文件名,参数二则是该类的类型名。

CreateCreateInstanceFromAndUnwrap方法有多个重载。代码中的调用方式是当RemoteLoader类为默认构造函数时的其中一种重载。如果RemoteLoader类的构造函数有参数,则方法应改为:

基于AppDomain的"插件式"开发object[] parms = {dllName};
基于AppDomain的"插件式"开发BindingFlags bindings 
= BindingFlags.CreateInstance |
基于AppDomain的"插件式"开发BindingFlags.Instance 
| BindingFlags.Public;
基于AppDomain的"插件式"开发rl 
= (AssemblyLoader.RemoteLoader)domain.CreateInstanceFromAndUnwrap("AssemblyLoader.dll","AssemblyLoader.RemoteLoader",true,bindings,
基于AppDomain的"插件式"开发
null,parms,null,null,null);

详细的调用方式可以参考MSDN。

以下Loader类的Unload方法和LoadAssembly方法():

基于AppDomain的"插件式"开发public Assembly LoadAssembly(string dllName)
基于AppDomain的"插件式"开发
{
基于AppDomain的"插件式"开发    
try
基于AppDomain的"插件式"开发    
{
基于AppDomain的"插件式"开发        SetRemoteLoaderObject(dllName);
基于AppDomain的"插件式"开发        
return rl.LoadAssembly(dllName);
基于AppDomain的"插件式"开发    }

基于AppDomain的"插件式"开发    
catch (Exception)
基于AppDomain的"插件式"开发    
{
基于AppDomain的"插件式"开发        
throw new AssemblyLoadFailureException();
基于AppDomain的"插件式"开发    }

基于AppDomain的"插件式"开发}
基于AppDomain的"插件式"开发public void Unload(string dllName)
基于AppDomain的"插件式"开发
{
基于AppDomain的"插件式"开发    
if (domains.ContainsKey(dllName))
基于AppDomain的"插件式"开发    
{
基于AppDomain的"插件式"开发        AppDomain appDomain 
= (AppDomain)domains[dllName];
基于AppDomain的"插件式"开发        AppDomain.Unload(appDomain);
基于AppDomain的"插件式"开发        domains.Remove(dllName);
基于AppDomain的"插件式"开发    }
            
基于AppDomain的"插件式"开发}

当我们调用Unload方法时,则程序域domain加载的程序集也将随着而被卸载。LoadAssembly方法中的异常AssemblyLoadFailureException为自定义异常:

基于AppDomain的"插件式"开发    public class AssemblyLoadFailureException:Exception
基于AppDomain的"插件式"开发    
{
基于AppDomain的"插件式"开发        
public AssemblyLoadFailureException():base()
基于AppDomain的"插件式"开发        
{            
基于AppDomain的"插件式"开发        }

基于AppDomain的"插件式"开发
基于AppDomain的"插件式"开发        
public override string Message
基于AppDomain的"插件式"开发        
{
基于AppDomain的"插件式"开发            
get
基于AppDomain的"插件式"开发            
{
基于AppDomain的"插件式"开发                
return "Assembly Load Failure";
基于AppDomain的"插件式"开发            }

基于AppDomain的"插件式"开发        }

基于AppDomain的"插件式"开发
基于AppDomain的"插件式"开发    }


既然在Loader类获得的RemoteLoader类实例必须通过代理的方式,因此该类对象必须支持被序列化。所以我们可以令该类派生MarshalByRefObject。RemoteLoader类的代码:

基于AppDomain的"插件式"开发    public class RemoteLoader:MarshalByRefObject
基于AppDomain的"插件式"开发    
{
基于AppDomain的"插件式"开发        
public RemoteLoader(string dllName)
基于AppDomain的"插件式"开发        
{
基于AppDomain的"插件式"开发            
if (assembly == null)
基于AppDomain的"插件式"开发            
{
基于AppDomain的"插件式"开发                assembly 
= Assembly.LoadFrom(dllName);
基于AppDomain的"插件式"开发            }

基于AppDomain的"插件式"开发        }
        
基于AppDomain的"插件式"开发
基于AppDomain的"插件式"开发        
private Assembly assembly = null;
基于AppDomain的"插件式"开发
基于AppDomain的"插件式"开发        
public Assembly LoadAssembly(string dllName)
基于AppDomain的"插件式"开发        
{
基于AppDomain的"插件式"开发            
try
基于AppDomain的"插件式"开发            
{
基于AppDomain的"插件式"开发                assembly 
= Assembly.LoadFrom(dllName);                
基于AppDomain的"插件式"开发                
return assembly;
基于AppDomain的"插件式"开发            }

基于AppDomain的"插件式"开发            
catch (Exception)
基于AppDomain的"插件式"开发            
{
基于AppDomain的"插件式"开发                
throw new AssemblyLoadFailureException();
基于AppDomain的"插件式"开发            }

基于AppDomain的"插件式"开发        }

基于AppDomain的"插件式"开发    }


通过上述的两个类,我们就可以实现程序集的加载和卸载。另外,为了保证应用程序域的对象在内存中被清除,应该令这两个类都实现IDisposable接口,和实现Dispose()方法。

然而在实际的操作过程中,我发现在RemoteLoader类的LoadAssembly方法,是存在遗患的。在我的LoadAssembly方法中,会返回一个Assembly对象。令我百思不得其解的是,虽然都是Assembly对象,但在加载某些程序集并返回Assembly时,在Loader类中会抛出SerializationException异常,并报告反序列化的对象状态不足。这个异常是在序列化获反序列化过程中发生的。我反复比较了两个程序集,一个可以正常加载并序列化,一个会抛出如上异常。会抛出异常的程序集并没有什么特殊之处,且我在程序中的其他地方也没有重复加载该程序集。这是一个疑问!!

不过通常我们在RemoteLoader类中,要实现的方法并非返回一个Assembly对象,而是通过反射加载程序集后,创建该程序集的对象。由于类对象都为object类型,此时序列化就不会出现问题。在我的项目中,因为要获得程序集的版本号,比较版本号在确定是否需要更新,因此我在RemoteLoader类中,只需要在加载程序集后,返回程序集的版本号字符串类型就可以了。字符串类型是绝对支持序列化的。

AssemlbyLoader.Dll的源代码可以点击这里获得。在应用程序中,显示添加对该程序集的引用,然后实例化Loader类对象,来调用该方法即可。我还做了一个简单的测试程序,用的是LoadAssembly方法。大家可以测试一下,是否如我所说,对于某些程序集,可能会抛出序列化的异常!?

测试的代码请点击这里获得,测试界面如下:
基于AppDomain的"插件式"开发

同时,大家也可以测试一下,直接加载和通过AppDomain加载,删除程序集文件时会有什么区别?