专栏首页雪雁的专栏【A】兼容Core3.0后 Natasha 的隔离域与热编译操作。

【A】兼容Core3.0后 Natasha 的隔离域与热编译操作。

一、 2.0预览版本增加了哪些功能

大部分为底层的升级优化,例如:

  • 引擎兼容 Core3.0
  • 优化编译流程,增加编译前语法检测及日志,统一采用流加载方式
  • 在 Vito 的建议下改进了日志目录及命名
  • ALC 同类覆盖编译
  • 支持域的创建、卸载、锁操作
  • 支持共享域与独立域协作
  • 支持独立域的程序集创建、覆盖操作
  • 支持插件及依赖的加载

构建方面的强化,例如:

  • 支持枚举的构建和编译
  • 在 Vito 的建议下增加了多维数组反解器
  • 在 Vito 的建议下增加了锯齿数组反解器
  • 命名反解器支持锯齿和多维数组

二、我们经历了哪些实践

深度克隆:https://github.com/night-moon-studio/DeepClone

本项目由 Net_win、Vito、myFirstway、白开水组队开发,可在运行时动态生成克隆方法。深度克隆作为基础项目,锻炼了开源工作者的类型辨识技能,趟过了坑为以后的封装之路打下基础。

快速调用:https://github.com/night-moon-studio/NCaller

本项目由 AzulX 和 FUTURE* 开发,可以对运行时实体类、静态类的字段/属性进行动态调用和赋值,目前有两个主要分支,哈希二叉查找算法动态实现以及 FUTURE* 的指针二叉查找算法动态实现,在算法的动态实现上,Natasha 表现出了相当强大的优势。

三、谈一谈‘热更新’

'热更新'是 Core3.0 的亮点特性之一,不少小伙伴在看到译文的时候可能就已经想到了N多场景,历经两代 .NET 的洗礼,‘热更新’现在发展到什么样子了?下面简单谈一谈:

.NET Framework 开荒时期有 AppDomain 域之隔离术,包括有创建、加载程序集、卸载等方法,囊括百家程序集,一刀以斩之。对于前辈们来说谈到 AppDomain 可以口若悬河滔滔不绝,可惜我进入 C# 时间比较晚,对 AppDomain 的印象并不是很深,在应用上也没有什么造诣,仅此泛泛而言。

时间进入了 .NETCore 时代,AppDomain 在升级大潮中受到了致命打击, Create 方法和 Unload 方法经岁月升级后的源码中充斥着 throw 和 throw ,完全丧失了功能,取而代之的是 ALC(AssemblyLoadContext) ,Core3.0 的 ALC 是一个更为完善的操作类,官方为其定义了三大洪荒场景:

1、插件编程

2、动态编译,运行/刷新代码,网站/脚本引擎

3、外部程序集的一次性内省(我个人理解就是类的信息,IsArray , IsClass 这种元数据只读属性)

据描述:Roslyn 之前一直用 AppDomain , 每个测试都腰酸背痛相当慢,自从换了 ALC( A blue Ca.) 一口气上5楼不费劲!官方画了大饼:未来 Roslyn 分析器执行编译时也都在ALC里进行,用完就卸载,卸磨就杀驴。

AppDomain 当初被定位在高性能、安全,历史证明这个定位跟 GPS 一样不准,ASP.NET 深受其害,历史车轮碾过了 ASP.NET 迎来了 ASP.NET Core ,在域功能被阉割的期间,ASP.NET Core 转向了相对静态的模型,增加了若干学习成本,详见 dotnet watch 命令。还有 Razor , 它从 .cshtml 编译到 .dll 的环境就是 ALC ,自建了一个名为 Razor-Server 的域环境。

另外还涉及到 LINQPad 和 Prism 框架, 精力有限,谁有兴趣就去研究研究吧。

ALC 的场景和案例可能激起了您的好奇心,下面讲一下 ALC 的应用:

我们可以在程序里创建多个 ALC 实例,但前提是你需要继承并实现它。每一个 ALC 的实例都是一个域(这里我就不叫它上下文了)。程序刚跑起来的时候是在 Defualt 域中的,这个域属于系统域卸不了,又称为共享域,不同域之间是无法访问和引用的不同域中信息的,却共用 Default 域中的信息,这个域至关重要,所以尽量避免向其中加载乱七八糟的程序集。

ALC 的使用需要注意以下几点:

1、子类继承时需指定 ALC 的构造参数,base(isCollectible) , 这个参数可以赋予 ALC 卸载的能力。

2、时刻注意反射信息的引用,只有清除引用,才能保证 ALC 实例被 GC 回收。

3、在针对不同域的编程时可使用 EnterContextualReflection 方法锁住域内上下文,EnterContextualReflection 方法是放在 using 里的,这样你的花括号内就是一个域,并用 CurrentContextualReflectionContext 属性来获取当前操作域。

4、注意 ALC 被线程占用的情况,被占用的对象是无法被回收的,如果你在测试中没有达到预期,除了排除代码问题之外你还需要注意函数是否被内联进入主线程或一个带有阻塞功能的线程,如果你不确定,可以在方法上使用 [MethodImpl(MethodImplOptions.NoInlining)] 阻止代码内联优化,正常情况下优化功能是开启的 。

5、插件加载要注意与插件 dll 同目录的依赖文件,3.0 提供了 AssemblyDependencyResolver 操作类自动解析依赖,建议使用带有.deps.json文件的完整插件。

6、当你的外部文件引用并使用了 Json.net/SqlConnection 等(测试日期9月3日),会造成不可回收的情况,不是你的代码出问题了,而是库本身的问题(待解决,3.1或者5.0)。

对 ALC 封装的一些建议:

1、如果没有非托管代码,尽量不要在析构函数里折腾代码。

2、如果你的域管理代码有些复杂,建议对外给个 IDispose 接口,以便清除对该域的程序集、元数据等信息的引用。

3、肉眼观测内存时,测试代码中尽量不要在 Main 函数里做元数据的相关操作,主线程是 GC 的一个干扰点。

4、若对内存的开销比较敏感,请尽可能分域,并结合弱引用实现创建与销毁。

5、有时显式调用 Unload 方法会报异常,可以在 Dispose 里清除完引用之后再使用,实测你不用 Unload 方法也能回收。

Core3.0 中随 ALC 一起的还有反射的自省信息。

例如:MemberInfo.IsCollectibleAssembly.IsCollectible 等元数据,它将告诉你它是否能被回收,当然了这种自省的信息都是只读的。说到只读,.NET 中还存有一条进化路线即 :ReflectionOnlyLoad -> TypeLoader -> MetadataLoadContext (感谢WeiHanLi提供的信息), 只读元数据,相比 ALC 可执行,可调用,MLC ( MetadataLoadContext 在包 System.Reflection.MetadataLoadContext 中) 关注的是元数据只读操作,它并不能执行程序集的内容,仅仅反射出元数据,配套使用的是PathAssemblyResolver.

对于无法卸载的情况,官方建议使用 windbg sos 组件进行调试,新版 sos 将独立出来,各位可以使用以下命令进行安装(建议开源工作者在封装此功能时添加UT测试检测卸载功能,尽可能保证在正常的情况下不需要用户自己去调试)。

$ dotnet tool install -g dotnet-sos --version 3.0.0-preview8.19412.1
$ dotnet-sos install

更多的实践还需要大家去探索。

四、Natasha是如何实现‘热更新’的

据以上信息,Natasha2.0 中动态构建遵循以下结构。

这两幅图说展示了 Natasha 中自定义编译域的结构,如果在创建程序集时不指定名字,程序集名将以 GUID 形式创建,故名随机程序集。在编译时未被移除的引用都将参与编译,该引用的来源:1、共享域;2、当前域;

  • 关于域的操作您可以
//创建一个域
DomainManagment.Create("MyDomain");
//移除一个域,移除将无法进行DomainManagment的其他任何操作
DomainManagment.Remove("MyDomain");
//判断域是否被卸载(被GC回收)
DomainManagment.IsDeleted("MyDomain");
//获取一个ALC上下文
DomainManagment.Get("MyDomain");


//锁住已存在的域上下文
using(DomainManagment.Lock("MyDomain"))
{
    var domain = DomainManagment.CurrentDomain;
    //code in 'MyDomain' domain 
}
//创建并锁定一个域上下文
using(DomainManagment.CreateAndLock("MyDomain"))
{
    var domain = DomainManagment.CurrentDomain;
    //code in 'MyDomain' domain 
}
  • 关于程序域的插件操作
//向域中注入插件 
string dllPath = @"1/2/3.dll";
var domain = DomainManagment.Get/Create("MyDomain");
var assembly = domain.LoadFile(dllPath);


//锁域与插件解构操作
string dllPath = @"1/2/3.dll";
using(DomainManagment.CreateAndLock("MyDomain"))
{
    var (Assembly,TypeCache) = dllPath;
    //Assembly: Assembly
    //TypeCache: ConcurrentDictionary<string,Type> 
}


//将引用从当前域内移除,下次编译将不会带着该程序集的信息
//下面方法三选一均可实现引用移除操作
domain.RemoveDll(dllPath);
domain.RemoveAssembly(assembly);
domain.RemoveType(type);
  • 关于程序集的操作:
//从指定域创建一个程序集操作实例
var asm = domain.CreateAssembly("MyAssembly");


//向程序集中添加一段已经写好的类/结构体/接口/枚举
asm.AddScript(@"using xxx; namespace xxx{xxxx}");
asm.AddFile(@"Class1.cs");


//使用Natasha内置的操作类
asm.CreateEnum(name=null);
asm.CreateClass(name=null);
asm.CreateStruct(name=null);
asm.CreateInterface(name=null);


//使用Natasha内置的方法操作类
//并不是很推荐使用这两个方法
//建议在一个单独的程序集内编译方法 
asm.CreateFastMethod(name=null);
asm.CreateFakeMethod(name=null);


//使用程序集进行编译并获得程序集
var assembly = asm.Complier();
asm.GetType(name);
  • 结合域和程序集动态编译,实例
using(DomainManagment.CreateAndLock("MyDomain"))
{
    var domain = DomainManagment.CurrentDomain;
    var assembly = domain.CreateAssembly("MyAssembly");
    
    
    //创建一个接口
    assembly
        .CreateInterface("InterfaceTest")
        .Using("System")
        .OopAccess(AccessTypes.Public)
        .OopBody("string ShowMethod(string str);");
        
        
     //创建一个类并实现接口
     assembly
        .CreateClass("TestClass")
        .Using("System")
        .OopAccess(AccessTypes.Public)        
        .Inheritance("InterfaceTest")
        .Method(method => method
          .MemberAccess(AccessTypes.Public)
          .Name("ShowMethod")
          .Param<string>("str")
          .Body("return str+\" World!\";")
          .Return<string>());
          
          
      //编译并获取类型
      var result = assembly.Complier();
      var type = assembly.GetType("TestClass");
      
      
      //Operator默认单独创建一个程序集
      var @delegate = FastMethodOperator.New
        .Using(type)
        .MethodBody(@"
            TestClass obj = new TestClass();
            return obj.ShowMethod(arg);")
        .Complie<Func<string, string>>();
       
       
       @delegate("Hello");  //result = "Hello World!";
       domain.Dispose();    //卸磨杀驴
}

不要从公众号里复制代码到VS,会有意外字符。

五、Bug有缘人

访问以下链接:https://github.com/dotnet/corefx/blob/1b3a095c277476b746e6fb9884662cf12334c6a1/src/System.Reflection.MetadataLoadContext/src/System/Reflection/MetadataLoadContext.Loading.cs

自右向左选中 MetadataLoadContext , 如果页面崩溃了, 老铁握爪。

https://github.com/dotnetcore

本文分享自微信公众号 - magiccodes(xl----0)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-09-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • HTTP:伟大而又无闻的协议

      Hello,大家好啊,我是Connor,一个从无到有的技术小白。有的人一说什么是HTTP协议就犯愁,写东西的时候也没想过什么是HTTP协议,只是知道HTTP...

    心莱科技雪雁
  • Magicodes.IE 2.3重磅发布——.NET Core开源导入导出库

    在2.3这一版本的更新中,我们迎来了众多的使用者、贡献者,在这个里程碑中我们也添加并修复了一些功能。对于新特点的功能我将在下面进行详细的描述,当然也欢迎更多的人...

    心莱科技雪雁
  • C#机器学习之判断日报是否合格

    简单来说机器学习的核心步骤在于“获取学习数据;选择机器算法;定型模型;评估模型,预测模型结果”,下面本人就以判断日报内容是否合格为例为大家简单的阐述一下C#的机...

    心莱科技雪雁
  • 解决python递归栈溢出

    使用python写的递归程序如果递归太深, 那么极有可能因为超过系统默认的递归深度限制而出现

    py3study
  • 从阶乘、斐波那契、汉诺塔剖析彻底搞懂递归算法

    递归:就是函数自己调用自己。子问题须与原始问题为同样的事,或者更为简单; 递归通常可以简单的处理子问题,但是不一定是最好的。

    bigsai
  • nodejs中的bcryptjs密码加密

    bcryptjs是一个第三方密码加密库,是对原有bcrypt的优化,优点是不需要安装任何依赖,npmjs地址为:https://www.npmjs.com/pa...

    ccf19881030
  • canvas之文字换行 原

    当前我们用来绘制文本的样式. 这个字符串使用和 CSS font 属性相同的语法. 默认的字体是 10px sans-serif。

    tianyawhl
  • 用 Linux 下所有的压缩、解压命令造轮子

    5G 来了,5G 是未来的一个驱动力。比 5G 更重要的一个是 AI,我们赢了 5G,并不代表我们赢了未来。

    业余草
  • LLVM+Clang+Libcxx+Libcxxabi(3.6)工具链编译(完成自举编译)

    LLVM和Clang工具链的生成配置文件写得比较搓,所以略微麻烦,另外这个脚本没有经过多环境测试,不保证在其他Linux发行版里正常使用。

    owent

扫码关注云+社区

领取腾讯云代金券