最近再搞.NET中的插件开发,其中涉及到应用程序的热升级,在很多情况下、我们希望用户对应用程序的升级是无感知的,并且尽可能不打断用户操作的。
虽然在Web 或者 WebAPI上,由于多点的存在可以逐个停用单点进行系统升级,而不影响整个服务。但是 客户端却不能这样做,毕竟用户一直在使用着。
那么有没有一种方式,可以在用户无感知的情况下(即、不停止进程的情况下)对客户端进行升级呢?
答案是肯定的, 这就是我今天想说的、可以对应用程序进行热升级。当然这种方式也同样适用于 ASP.NET ,这里最核心的就是需要理解:应用程序域AppDomain
不过当前随笔是以 WPF为例子的,并且原理是一样的、代码逻辑也是一样的。

一、应用程序域AppDomain

在介绍插件技术之前、我们需要先了解一些基础性的知识,第一个就是应用程序域AppDomain.
操作系统和运行时环境通常会在应用程序间提供某种形式的隔离。 例如,Windows 使用进程来隔离应用程序。 为确保在一个应用程序中运行的代码不会对其他不相关的应用程序产生不良影响,这种隔离是必需的。这种隔离可以为应用程序域提供安全性、可靠性, 并且为卸载程序集提供了可能。
在 .NET中应用程序域AppDomain是CLR的运行单元,它可以加载应用程序集Assembly、创建对象以及执行程序。
在 CLR 里、AppDomain就是用来实现代码隔离的,每一个AppDomain可以单独创建、运行、卸载。
如果默认AppDomain监听了 UnhandledException 事件,任何线程的任何未处理异常都会引发该事件,无论线程是从哪个AppDomain中开始的。
如果一个线程开始于一个已经监听了 UnhandledException事件的 app domain, 那么该事件将在这个app domain 中引发。
如果这个app domian 不是默认的app domain, 并且 默认 app domain 中也监听了 UnhandledException 事件, 那么 该事件将会在两个app domain 中引发。
CLR启用时,会创建一个默认的AppDomain,程序的入口点(Main方法)就是在这个默认的AppDomain中执行。
AppDomain是可以在运行时进行动态的创建和卸载的,正因如此,才为插件技术提供了基础(注:应用程序集和类型是不能卸载的,只能卸载整个AppDomain)。

AppDomain和其他概念之间的关系

1、AppDomain vs 进程Process
AppDomain被创建在Process中,一个Process内可以有多个AppDomain。一个AppDomain只能属于一个Process。
2、AppDomain vs 线程Thread
应该说两者之间没有关系,AppDomain出现的目的是隔离,隔离对象,而 Thread 是 Process中的一个实体、是程序执行流中的最小单元,保存有当前指令指针 和 寄存器集合,为线程(上下文)切换提供可能。如果说有关系的话,可以牵强的认为一个Thread可以使用多个AppDomain中的对象,一个AppDomain中可以使用多个Thread.
3、AppDomain vs 应用程序集Assembly
Assembly是.Net程序的基本部署单元,它可以为CLR提供元数据等。
Assembly不能单独执行,它必须被加载到AppDomain中,然后由AppDomain创建程序集中的类型 及 对象。
一个Assembly可以被多个AppDomain加载,一个AppDomain可以加载多个Assembly。
每个AppDomain引用到某个类型的时候需要把相应的assembly在各自的AppDomain中初始化。因此,每个AppDomain会单独保持一个类的静态变量。
4、AppDomain vs 对象object
任何对象只能属于一个AppDomain,AppDomain用来隔离对象。 同一应用程序域中的对象直接通信、不同应用程序域中的对象的通信方式有两种:一种是跨应用程序域边界传输对象副本(通过序列化对对象进行隐式值封送完成),一种是使用代理交换消息。

二、创建 和 卸载AppDomain

前文已经说明了,我们可以在运行时动态的创建和卸载AppDomain, 有这样的理论基础在、我们就可以热升级应用程序了 。
那就让我们来看一下如何创建和卸载AppDomain吧
创建:
AppDomainSetup objSetup = new AppDomainSetup();
objSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
this.domain = AppDomain.CreateDomain(“RemoteAppDomain”, null, objSetup);
创建AppDomain的逻辑非常简单:使用 AppDomain.CreateDomain 静态方法、传递了一个任意字符串 和 AppDomainSetup 对象。
卸载:
AppDomain.Unload(this.domain);
卸载就更简单了一行代码搞定:AppDomain.Unload 静态方法,参数就一个 之前创建的AppDomain对象。

三、在新AppDomain中创建对象

上文已经说了创建AppDomain了,但是创建的新AppDomain却是不包含任何对象的,只是一个空壳子。那么如何在新的AppDomain中创建对象呢?
this.remoteIPlugin = this.domain.CreateInstance(“PluginDemo.NewDomain”, “PluginDemo.NewDomain.Plugin”).Unwrap() as IPlugin;
使用刚创建的AppDomain对象的实例化方法: this.domain.CreateInstance,传递了两个字符串,分别为 assemblyName 和 typeName.
并且该方法的重载方法 和 相似功能的重载方法多达十几个。

四、影像复制程序集

创建、卸载AppDomain都有、创建新对象也可以了,但是如果想完成热升级,还有一点小麻烦,那就是一个程序集被加载后会被锁定,这时候是无法对其进行修改的。
所以就需要打开 影像复制程序集 功能,这样在卸载AppDomain后,把需要升级的应用程序集进行升级替换,然后再创建新的AppDomain即可了。
打开 影像复制程序集 功能,需要在创建新的AppDomain时做两步简单的设定即可:
AppDomainSetup objSetup = new AppDomainSetup();
objSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
// 打开 影像复制程序集 功能
objSetup.ShadowCopyFiles = “true”;
// 虽然此方法已经被标记为过时方法, msdn备注也提倡不使用该方法,
// 但是 以.net 4.0 + win10环境测试,还必须调用该方法 否则,即便卸载了应用程序域 dll 还是未被解除锁定
AppDomain.CurrentDomain.SetShadowCopyFiles();
this.domain = AppDomain.CreateDomain(“RemoteAppDomain”, null, objSetup);