当一个程序集被加载使用的时候,出于数据的完整性和安全性考虑,程序集文件(在99.9998%的状况下是.dll文件)会被锁定,若是此时你想更新程序集(其实是替换dll文件),是不能够操做的,这时你得把应用程序退出,替换文件后再启动程序。缓存
多数状况下这样作是可行的,只是有时候,好比ASP.NET或一些须要一直运行的服务进程,重启程序来更新好像不太好。安全
要是想对程序集进行热更新,即在程序运行的同时替换文件,有一个你们很熟悉的方案——影像复制,若是你不熟悉.net,你确定没据说过的。固然了,这个叫法也挺难听的,没办法,只好这样翻译,原词是 Shadow Copy ,Shadow是影子,阴影,影像的意思,那也只好这么翻译了。不过,你不用担忧它很抽象很高端,其实,只要用心学,没什么东西是攻不克的。函数
我用一句话来归纳一下影子复制(也能够叫拷贝,但我不喜欢拷贝这个词,很黄很暴力的感受)——应用程序域在加载程序集时,会把程序集文件复制到另外一个地方,再进行加载。这样一来,当程序集文件被使用时,它锁定的是复制后的文件,即原始文件咱们能够放心地去替换了,等到合适的时间,把应用程序从新启动一下,再次运行时,就会自动把最新的程序集复制到缓存的目录下,而后执行最新版本的代码。最好把这些代码的调用放到一个新的应用程序域中执行,由于这样的好处是不用从新启动应用程序,而只要把某个应用程序域卸载掉再从新建立一个新的,就会自动加载最新的程序集了。并且,一般你都应该这么作的,建立一个应用程序域,在里面执行代码,执行完了就把应用程序域卸掉,能够节约资源。测试
应用程序在运行的时候,默认会建立一个应用程序域的,说白了,一个进程中至少会有一个应用程序域,若是你把某段代码放到一个新的应用程序域中执行,而且你但愿执行完后,能够把结果传回给主应用程序域,那就用老周之前写过的方法,记得老周前面写过的,想按引用传递对象,就从MarshalByRefObject类派生,想让对象按值传递,就让它支持序列化。spa
在建立新的应用程序域时,能够同时传递一个SetupInfo对象,这个对象有一个 ShadowCopyFiles 属性,虽然它定义的类型是 string,但你千万不要理解错,不要把一个文件的路径赋给它。老周之前就见到一位朋友理解错了,它误觉得这个属性是用来设置复制程序集文件的缓存路径,结果代码写了总是不行。唉,这就是不看MSDN的下场。.net
不要乱来,设置复制程序集的缓存目录是 CachePath 属性,不是 ShadowCopyFiles 属性。ShadowCopyFiles 属性只能用两个字符串的值,若是要启用影像复制,就设置为 true,若是想禁用,就设置 false 或者干脆保持默认的null值。也就说,它是一个用字符串表示的 bool 值。命令行
下面,咱们用一个例子来表演一下,很精彩的。翻译
首先,弄一个类库项目,而后在里面写一个全宇宙最简单的类。3d
namespace TestLib { public class Demo { public string Call() { return "Ver - 3"; } } }
而主启动项目是一个控制台应用,这里,老周但愿设置新应用程序域的 PrivateBinPath ,这个属性能够设置一堆目录,能够是相对路径,其实应该是用相对路径的,由于这个目录不能乱设的,它必须是应用程序目录的子目录。若是是多个目录,能够用英文的分号(;)来分隔。code
ApplicationBase路径指定的是应用程序,即.exe启动的目录,无论你建立多个新的应用程序域,这个目录都必须指定为当前exe的启动目录。不然你试试看,不能运行的,由于应用程序域之间是隔离的,因此在新建立的应用程序域中也必须加载当前exe所在的程序集,这个程序集是必须的,由于它是主入口点。
而 PrivateBinPath 属性所指定的路径必须为应用程序目录的子目录,好比,咱们的项目在Debug模式下,一般是把exe生成到 bin \ Debug目录下的,因此,你能够在Debug目录下建立一个子目录,我这里创了一个,叫ExtDlls,随后我会把要用到的dll文件放在这个目录中,并设置 PrivateBinPath = "ExtDlls" ,这样一来,就算项目不引用这个类库项目,在运行阶段它都会自动到这个 ExtDlls 目录下去找,找到了就加载,要是找不到就会“呵呵”。
我这个类库项目名叫 TestLib,为了让它生成后可以自动把最新的版本复制到 ExtDlls 目录中,能够打开类库项目的项目属性窗口,切换到【生成事件】页,并在“后期生成命令行”中输入如下命令:
copy "$(TargetPath)" "$(SolutionDir)MyApp\bin\Debug\ExtDlls\"
这么一搞,每次我从新生成类库项目后,就会自动把dll文件复制过去。
好,下面的重点放在主项目上,在代码中,能够建立一个新的应用程序域,而后调用类库中的代码。
AppDomainSetup setup = new AppDomainSetup(); setup.ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase; setup.ApplicationName = "ExtFuncs"; setup.PrivateBinPath = "ExtDlls"; setup.ShadowCopyFiles = "true"; AppDomain newDom = AppDomain.CreateDomain("hello", null, setup); newDom.DoCallBack(() => { Type t = Type.GetType("TestLib.Demo, TestLib"); // 获取公共无参构造函数 ConstructorInfo costr = t.GetConstructor(new Type[] { }); // 调用构造函数,建立类型实例 object instance = costr.Invoke(new object[] { }); // 找到要调用的方法 MethodInfo m = t.GetMethod("Call", BindingFlags.Public | BindingFlags.Instance); // 调用方法,获得返回值 object retval = m.Invoke(instance, new object[] { }); Console.WriteLine($"调用输出:{retval}"); Console.WriteLine("\n==================================="); // 输出引用程序集的路径 var refAsses = AppDomain.CurrentDomain.GetAssemblies(); foreach (var ass in refAsses) { Console.WriteLine("名称:"+ ass.GetName().Name); Console.WriteLine("路径:" + ass.Location); Console.WriteLine(); } }); AppDomain.Unload(newDom); //卸载应用程序域
实验代表,ApplicationName 属性的值能够随便写,但 ApplicationBase 属性必须是当前应用程序所在目录。
这里我用的是反射的方法来调用的,DoCallBack 方法容许在另外一个应用程序域中执行代码,代码内容经过一个委托来关联。
在反射调用完测试类库后,我还用这段代码来输出新的应用程序域所引用的全部程序集的路径。
var refAsses = AppDomain.CurrentDomain.GetAssemblies(); foreach (var ass in refAsses) { Console.WriteLine("名称:"+ ass.GetName().Name); Console.WriteLine("路径:" + ass.Location); Console.WriteLine(); }
因为这段代码是在新的应用程序域中执行的,因此 CurrentDomain 属性所指的是新建立的应用程序域,而不是进程运行时建立的默认域。
之因此要在反射以后输出路径是由于,应用程序域是动态加载程序集,即当你用到类库中的类型时才会加载,若是不访问类库中的任何东西,是不会加载这个程序集的。
我为啥要输出路径呢,就是让大伙可以清楚地看到,TestLib 类库已经被复制到另外一个目录中执行了。请看:
从这个图你就看到,默认的缓存程序集的路径是在你的用户配置目录下的 AppData \ Local \ assembly 下面。
可能你以为这个默认的缓存路径很差,能不能自定义啊?能,前面老周提了一下 CachePath 属性,对,你给这个属性分配一个路径,缓存的程序集就会放到这个自定义路径中了。好比,我在Debug目录下新建一个 TempAss 目录,用来存放临时复制的程序集。
setup.CachePath = CACHE_PATH;
而后你再看它的路径。
看,是否是变了?
如今,咱们来验证一下,是否是能够热更新。
先运行exe,输出Ver - 1 ,如图。
好,保持exe运行着,不要关,而后修改一下类库项目的代码。
public class Demo { public string Call() { return "Ver - 2"; } }
把 1 改成 2。
从新生成一下类库项目,它会自动复制到 ExtDlls 目录。
如今在控制台窗口按除 Esc 之外的任意键,就会从新建一个应用程序域,并加载执行类库代码,由于我弄了个循环,只有遇到Esc键才会退出。
这时候,你看到,输出的内容变了。
不用退出应用程序,就能实现程序集文件的替换了,这对于服务应用特别好使。
为了写代码有智能提示,若是我不想用反射呢,而是直接在VS中引用类库项目呢,试试,引用以后,把所TestLib属性中的“复制本地”改成false,由于 ExtDlls 目录下已经有文件了,没必要再复制了,在新的应用程序域中执行时,会自动搜索。
而后把DoCallBack 方法中的代码改一下:
newDom.DoCallBack(() => { TestLib.Demo dm = new TestLib.Demo(); Console.WriteLine($"输出:{dm.Call()}"); });
如今代码就变得简单多了,是吧,才两行就完事了。
那能不能运行呢,固然能了。看。
怎么样,牛逼烘烘吧。
好了,老周的芹菜炒鱼蛋饭作好了,肚子饿了,开饭了。