【.net 深呼吸】程序集的热更新
当一个程序集被加载使用的时候,出于数据的完整性和安全性考虑,程序集文件(在99.9998%的情况下是.dll文件)会被锁定,如果此时你想更新程序集(实际上是替换dll文件),是不可以操作的,这时你得把应用程序退出,替换文件后再启动程序。
多数情况下这样做是可行的,只是有时候,比如ASP.NET或一些需要一直运行的服务进程,重启程序来更新好像不太好。
要是想对程序集进行热更新,即在程序运行的同时替换文件,有一个大家很熟悉的方案——影像复制,如果你不熟悉.net,你肯定没听说过的。当然了,这个叫法也挺难听的,没办法,只好这样翻译,原词是 Shadow Copy ,Shadow是影子,阴影,影像的意思,那也只好这么翻译了。不过,你不用担心它很抽象很高端,其实,只要用心学,没什么东西是攻不克的。
我用一句话来概括一下影子复制(也可以叫拷贝,但我不喜欢拷贝这个词,很黄很暴力的感觉)——应用程序域在加载程序集时,会把程序集文件复制到另一个地方,再进行加载。这样一来,当程序集文件被使用时,它锁定的是复制后的文件,即原始文件我们可以放心地去替换了,等到合适的时间,把应用程序重新启动一下,再次运行时,就会自动把最新的程序集复制到缓存的目录下,然后执行最新版本的代码。最好把这些代码的调用放到一个新的应用程序域中执行,因为这样的好处是不用重新启动应用程序,而只要把某个应用程序域卸载掉再重新创建一个新的,就会自动加载最新的程序集了。而且,通常你都应该这么做的,创建一个应用程序域,在里面执行代码,执行完了就把应用程序域卸掉,可以节约资源。
应用程序在运行的时候,默认会创建一个应用程序域的,说白了,一个进程中至少会有一个应用程序域,如果你把某段代码放到一个新的应用程序域中执行,并且你希望执行完后,可以把结果传回给主应用程序域,那就用老周以前写过的方法,记得老周前面写过的,想按引用传递对象,就从MarshalByRefObject类派生,想让对象按值传递,就让它支持序列化。
在创建新的应用程序域时,可以同时传递一个SetupInfo对象,这个对象有一个 ShadowCopyFiles 属性,虽然它定义的类型是 string,但你千万不要理解错,不要把一个文件的路径赋给它。老周以前就见到一位朋友理解错了,它误以为这个属性是用来设置复制程序集文件的缓存路径,结果代码写了老是不行。唉,这就是不看MSDN的下场。
不要乱来,设置复制程序集的缓存目录是 CachePath 属性,不是 ShadowCopyFiles 属性。ShadowCopyFiles 属性只能用两个字符串的值,如果要启用影像复制,就设置为 true,如果想禁用,就设置 false 或者干脆保持默认的null值。也就说,它是一个用字符串表示的 bool 值。
下面,我们用一个例子来表演一下,很精彩的。
首先,弄一个类库项目,然后在里面写一个全宇宙最简单的类。
namespace TestLib
{
public class Demo
{
public string Call()
{
return "Ver - 3";
}
}
}
而主启动项目是一个控制台应用,这里,老周希望设置新应用程序域的 PrivateBinPath ,这个属性可以设置一堆目录,可以是相对路径,其实应该是用相对路径的,因为这个目录不能乱设的,它必须是应用程序目录的子目录。如果是多个目录,可以用英文的分号(;)来分隔。
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()}");
});
现在代码就变得简单多了,是吧,才两行就完事了。
那能不能运行呢,当然能了。看。
怎么样,牛逼烘烘吧。
好了,老周的芹菜炒鱼蛋饭做好了,肚子饿了,开饭了。
【.net 深呼吸】程序集的热更新的更多相关文章
- 一步一步开发Game服务器(三)加载脚本和服务器热更新(二)完整版
上一篇文章我介绍了如果动态加载dll文件来更新程序 一步一步开发Game服务器(三)加载脚本和服务器热更新 可是在使用过程中,也许有很多会发现,动态加载dll其实不方便,应为需要预先编译代码为dll文 ...
- 手游热更新方案--Unity3D下的CsToLua技术
WeTest 导读 CsToLua工具将客户端 C#源码自动转换为Lua,实现热更新,本文以麻将项目为例介绍客户端技术细节. 麻将项目架构 其中ChinaMahjong-CSLua为C#工程,实现麻将 ...
- Unity实现c#热更新方案探究(一)
转载请标明出处:http://www.cnblogs.com/zblade/ 最近研究了一下如何在unity中实现c#的热更新,对于整个DLL热更新的过程和方案有一个初步的了解,这儿就写下来,便于后续 ...
- unity3d热更新插件uLua学习整理
前言 IOS不能热更新,不是因为不能用反射,是因为System.Reflection.Assembly.Load 无法使用System.Reflection.Emit 无法使用System.CodeD ...
- Unity3d dll 热更新 基础框架
APK包装到用户手机上后,代码如何更新,总不能全用LUA吧?特别是代码非常多的战斗手游 昨晚上有了dll 热更新的想法,今天做了一天的实验,也遇到些坑,这里总结下 工作环境: U3D5.3.2 + v ...
- 深入理解xLua热更新原理
热更新简介 热更新是指在不需要重新编译打包游戏的情况下,在线更新游戏中的一些非核心代码和资源,比如活动运营和打补丁.热更新分为资源热更新和代码热更新两种,代码热更新实际上也是把代码当成资源的一种热更新 ...
- 深入理解xLua基于IL代码注入的热更新原理
目前大部分手游都会采用热更新来解决应用商店审核周期长,无法满足快节奏迭代的问题.另外热更新能够有效降低版本升级所需的资源大小,节省玩家的时间和流量,这也使其成为移动游戏的主流更新方式之一. 热更新可以 ...
- 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新
本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...
- 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新
[原]Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代. Tinker github地址:http ...
随机推荐
- C# ini文件操作【源码下载】
介绍C#如何对ini文件进行读写操作,C#可以通过调用[kernel32.dll]文件中的 WritePrivateProfileString()和GetPrivateProfileString()函 ...
- jQuery实践-网页版2048小游戏
▓▓▓▓▓▓ 大致介绍 看了一个实现网页版2048小游戏的视频,觉得能做出自己以前喜欢玩的小游戏很有意思便自己动手试了试,真正的验证了这句话-不要以为你以为的就是你以为的,看视频时觉得看懂了,会写了, ...
- .Net中的AOP系列之构建一个汽车租赁应用
返回<.Net中的AOP>系列学习总目录 本篇目录 开始一个新项目 没有AOP的生活 变更的代价 使用AOP重构 本系列的源码本人已托管于Coding上:点击查看. 本系列的实验环境:VS ...
- Partition1:新建分区表
未分区的表,只能存储在一个FileGroup中:对Table进行分区后,每一个分区都存储在一个FileGroup,或分布式存储在不同的FileGroup中.对表进行分区的过程,是将逻辑上完整的一个表, ...
- PhotoView实现图片随手势的放大缩小的效果
项目需求:在listView的条目中如果有图片,点击条目,实现图片的放大,并且图片可以根据手势来控制图片放大缩小的比例.类似于微信朋友圈中查看好友发布的照片所实现的效果. 思路是这样的:当点击条目的时 ...
- HTML+CSS 项目总结
在过去的大概一个月的学习,基本掌握了HTML+CSS的用法和特性. 这个星期老师给我们布置了一个PC端的实战项目,并且要求在3-4天内完成,我不惜废寝忘食,在紧迫的时间内大致地完成了,但是有些效果不能 ...
- .Net语言 APP开发平台——Smobiler学习日志:手机应用的TextTabBar快速实现方式
参考页面: http://www.yuanjiaocheng.net/webapi/create-crud-api-1-put.html http://www.yuanjiaocheng.net/we ...
- ios 获取或修改网页上的内容
UIWebView是iOS最常用的SDK之一,它有一个stringByEvaluatingJavaScriptFromString方法可以将javascript嵌 入页面中,通过这个方法我们可 ...
- 打开程序总是会提示“Enter password to unlock your login keyring” ,如何成功关掉?
p { margin-bottom: 0.1in; line-height: 120% } 一.一开始我是按照网友所说的 : rm -f ~/.gnome2/keyrings/login.keyrin ...
- linux下配置matlab运行环境(MCR)
在安装好的matlab下有MCR(MatlabCompilerRuntime)在matlab2011/toolbox/compiler/deploy/glnxa64下找到MCRInstaller.zi ...