.Net core 的热插拔机制的深入探索及卸载问题求救指南

一.依赖文件*.deps.json的读取.

依赖文件内容如下.一般位于编译生成目录中

{
 "runtimeTarget": {
 "name": ".NETCoreApp,Version=v3.1",
 "signature": ""
 },
 "compilationOptions": {},
 "targets": {
 ".NETCoreApp,Version=v3.1": {
 "PluginSample/1.0.0": {
 "dependencies": {
 "Microsoft.Extensions.Hosting.Abstractions": "5.0.0-rc.2.20475.5"
 },
 "runtime": {
 "PluginSample.dll": {}
 }
 },
 "Microsoft.Extensions.Configuration.Abstractions/5.0.0-rc.2.20475.5": {
 "dependencies": {
 "Microsoft.Extensions.Primitives": "5.0.0-rc.2.20475.5"
 },
 "runtime": {
 "lib/netstandard2.0/Microsoft.Extensions.Configuration.Abstractions.dll": {
 "assemblyVersion": "5.0.0.0",
 "fileVersion": "5.0.20.47505"
 }
 }
 ...

使用DependencyContextJsonReader加载依赖配置文件源码查看

using (var dependencyFileStream = File.OpenRead("Sample.deps.json"))
{
 using (DependencyContextJsonReader dependencyContextJsonReader = new DependencyContextJsonReader())
 {
 //得到对应的实体文件
 var dependencyContext =
 dependencyContextJsonReader.Read(dependencyFileStream);
 //定义的运行环境,没有,则为全平台运行.
 string currentRuntimeIdentifier= dependencyContext.Target.Runtime;
 //运行时所需要的dll文件
 var assemblyNames= dependencyContext.RuntimeLibraries;
 }
}

二.Net core多平台下RID(RuntimeIdentifier)的定义.

安装 Microsoft.NETCore.Platforms包,并找到runtime.json运行时定义文件.

{
 "runtimes": {
 "win-arm64": {
 "#import": [
 "win"
 ]
 },
 "win-arm64-aot": {
 "#import": [
 "win-aot",
 "win-arm64"
 ]
 },
 "win-x64": {
 "#import": [
 "win"
 ]
 },
 "win-x64-aot": {
 "#import": [
 "win-aot",
 "win-x64"
 ]
 },
}

NET Core RID依赖关系示意图

win7-x64 win7-x86
 | \ / |
 | win7 |
 | | |
win-x64 | win-x86
 \ | /
 win
 |
 any

.Net core常用发布平台RID如下

  • windows (win)

win-x64
win-x32
win-arm

  • macos (osx)

osx-x64

  • linux (linux)

linux-x64
linux-arm

1. .net core的runtime.json文件由微软提供:查看runtime.json.

2. runtime.json的runeims节点下,定义了所有的RID字典表以及RID树关系.

3. 根据*.deps.json依赖文件中的程序集定义RID标识,就可以判断出依赖文件中指向的dll是否能在某一平台运行.

4. 当程序发布为兼容模式时,我们出可以使用runtime.json文件选择性的加载平台dll并运行.

三.AssemblyLoadContext的加载原理

public class PluginLoadContext : AssemblyLoadContext
{
 private AssemblyDependencyResolver _resolver;
 public PluginLoadContext(string pluginFolder, params string[] commonAssemblyFolders) : base(isCollectible: true)
 {
 this.ResolvingUnmanagedDll += PluginLoadContext_ResolvingUnmanagedDll;
 this.Resolving += PluginLoadContext_Resolving;
 //第1步,解析des.json文件,并调用Load和LoadUnmanagedDll函数
 _resolver = new AssemblyDependencyResolver(pluginFolder);
 //第6步,通过第4,5步,解析仍失败的dll会自动尝试调用主程序中的程序集,
 //如果失败,则直接抛出程序集无法加载的错误
 }
 private Assembly PluginLoadContext_Resolving(AssemblyLoadContext assemblyLoadContext, AssemblyName assemblyName)
 {
 //第4步,Load函数加载程序集失败后,执行的事件
 }
 private IntPtr PluginLoadContext_ResolvingUnmanagedDll(Assembly assembly, string unmanagedDllName)
 {
 //第5步,LoadUnmanagedDll加载native dll失败后执行的事件
 }
 protected override Assembly Load(AssemblyName assemblyName)
 {
 //第2步,先执行程序集的加载函数
 }
 protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
 {
 //第3步,先执行的native dll加载逻辑
 }
}

微软官方示例代码如下:示例具体内容

class PluginLoadContext : AssemblyLoadContext
{
 private AssemblyDependencyResolver _resolver;

 public PluginLoadContext(string pluginPath)
 {
 _resolver = new AssemblyDependencyResolver(pluginPath);
 }

 protected override Assembly Load(AssemblyName assemblyName)
 {
 string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
 if (assemblyPath != null)
 {
 //加载程序集
 return LoadFromAssemblyPath(assemblyPath);
 }
 //返回null,则直接加载主项目程序集
 return null;
 }

 protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
 {
 string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
 if (libraryPath != null)
 {
 //加载native dll文件
 return LoadUnmanagedDllFromPath(libraryPath);
 }
 //返回IntPtr.Zero,即null指针.将会加载主项中runtimes文件夹下的dll
 return IntPtr.Zero;
 }
}

1. 官方这个示例是有问题的.LoadFromAssemblyPath()函数有bug,
该函数并不会加载依赖的程序集.正确用法是LoadFormStream()

2. Load和LoadUnmanagedDll函数实际上是给开发者手动加载程序集使用的,
自动加载应放到Resolving和ResolvingUnmanagedDll事件中
原因是,这样的加载顺序不会导致项目的程序集覆盖插件的程序集,造成程序集加载失败.

3. 手动加载时可以根据deps.json文件定义的runtime加载当前平台下的unmanaged dll文件.

这些平台相关的dll文件,一般位于发布目录中的runtimes文件夹中.

四.插件项目一定要和主项目使用同样的运行时.

  1. 如果主项目是.net core 3.1,插件项目不能选择.net core 2.0等,甚至不能选择.net standard库
  2. 否则会出现不可预知的问题.
  3. 插件是.net standard需要修改项目文件,<TargetFrameworks>netstandard;netcoreapp3.1</TargetFrameworks>
  4. 这样就可以发布为.net core项目.
  5. 若主项目中的nuget包不适合当前平台,则会报Not Support Platform的异常.这时如果主项目是在windows上, 就需要把项目发布目标设置为win-x64.这属于nuget包依赖关系存在错误描述.

五.AssemblyLoadContext.UnLoad()并不会抛出任何异常.

当你调用AssemblyLoadContext.UnLoad()卸载完插件以为相关程序集已经释放,那你可能就错了.官方文档表明卸载执行失败会抛出InvalidOperationException,不允许卸载官方说明
但实际测试中,卸载失败,但并未报错.

六.反射程序集相关变量的定义为何阻止插件程序集卸载?

插件

namespace PluginSample
{
 public class SimpleService
 {
 public void Run(string name)
 {
 Console.WriteLine($"Hello World!");
 }
 }
}

加载插件

namespace Test
{
 public class PluginLoader
 {
 pubilc AssemblyLoadContext assemblyLoadContext;
 public Assembly assembly;
 public Type type;
 public MethodInfo method;
 public void Load()
 {
 assemblyLoadContext = new PluginLoadContext("插件文件夹");
 assembly = alc.Load(new AssemblyName("PluginSample"));
 type = assembly.GetType("PluginSample.SimpleService");
 method=type.GetMethod()
 }
 }
}

1. 在主项目程序中.AssemblyLoadContext,Assembly,Type,MethodInfo等不能直接定义在任何类中.
否则在插件卸载时会失败.当时为了测试是否卸载成功,采用手动加载,执行,卸载了1000次,
发现内存一直上涨,则表示卸载失败.

2. 参照官方文档后了解了WeakReferece类.使用该类与AssemblyLoadContext关联,当手动GC清理时,
AssemblyLoadContext就会变为null值,如果没有变为null值则表示卸载失败.

3. 使用WeakReference关联AssemblyLoadContext并判断是否卸载成功

public void Load(out WeakReference weakReference)
 {
 var assemblyLoadContext = new PluginLoadContext("插件文件夹");
 weakReference = new WeakReference(pluginLoadContext, true);
 assemblyLoadContext.UnLoad();
 }
 public void Check()
 {
 WeakReference weakReference=null;
 Load(out weakReference);
 //一般第二次,IsAlive就会变为False,即AssemblyLoadContext卸载失败.
 for (int i = 0; weakReference.IsAlive && (i < 10); i++)
 {
 GC.Collect();
 GC.WaitForPendingFinalizers();
 }
 }

4. 为了解决以上问题.可以把需要的变量放到静态字典中.在Unload之前把对应的Key值删除掉,即可.

七.程序集的异步函数执行为何会阻止插件程序的卸载?

public class SimpleService
{
 //同步执行,插件卸载成功
 public void Run(string name)
 {
 Console.WriteLine($"Hello {name}!");
 }
 //异步执行,卸载成功
 public Task RunAsync(string name)
 {
 Console.WriteLine($"Hello {name}!");
 return Task.CompletedTask;
 }
 //异步执行,卸载成功
 public Task RunTask(string name)
 {
 return Task.Run(() => {
 Console.WriteLine($"Hello {name}!");
 });
 }
 //异步执行,卸载成功
 public Task RunWaitTask(string name)
 {
 return Task.Run( async ()=> {
 while (true)
 {
 if (CancellationTokenSource.IsCancellationRequested)
 {
  break;
 }
 await Task.Delay(1000);
 Console.WriteLine($"Hello {name}!");
 }
 });
 }
 //异步执行,卸载成功
 public Task RunWaitTaskForCancel(string name, CancellationToken cancellation)
 {
 return Task.Run(async () => {
 while (true)
 {
 if (cancellation.IsCancellationRequested)
 {
  break;
 }
 await Task.Delay(1000);
 Console.WriteLine($"Hello {name}!");
 }
 });
 }
 //异步执行,卸载失败
 public async Task RunWait(string name)
 {
 while (true)
 {
 if (CancellationTokenSource.IsCancellationRequested)
 {
 break;
 }
 await Task.Delay(1000);
 Console.WriteLine($"Hello {name}!");
 }

 }
 //异步执行,卸载失败
 public Task RunWaitNewTask(string name)
 {
 return Task.Factory.StartNew(async ()=> {
 while (true)
 {
 if (CancellationTokenSource.IsCancellationRequested)
 {
  break;
 }
 await Task.Delay(1000);
 Console.WriteLine($"Hello {name}!");
 }
 },TaskCreationOptions.DenyChildAttach);
 }
}

1. 以上测试可以看出,如果插件调用的是一个常规带wait的async异步函数,则插件一定会卸载失败.
原因推测是返回的结果是编译器自动生成的状态机实现的,而状态机是在插件中定义的.

2. 如果在插件中使用Task.Factory.StartNew函数也会调用失败,原因不明.
官方文档说和Task.Run函数是Task.Factory.StartNew的简单形式,只是参数不同.官方说明
按照官方提供的默认参数测试,卸载仍然失败.说明这两种方式实现底层应该是不同的.

八.正确卸载插件的方式

  • 任何与插件相关的非局部变量,不能定义在类中,如果想全局调用只能放到Dictionary中,
  • 在调用插件卸载之前,删除相关键值.
  • 任何通过插件返回的变量,不能为插件内定义的变量类型.尽量使用json传递参数.
  • 插件入口函数尽量使用同步函数,如果为异步函数,只能使用Task.Run方式裹所有逻辑.
  • 如果有任何疑问或不同意见,请赐教.

NFinal2开源框架。https://git.oschina.net/LucasDot/NFinal2/tree/master

到此这篇关于.Net core 的热插拔机制的深入探索及卸载问题求救指南的文章就介绍到这了,更多相关.Net core热插拔机制内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • .Net Core2.1 WebAPI新增Swagger插件详解

    说明 Swagger是一个WebAPI在线注解.调试插件,过去我们主要通过手工撰写WebAPI接口的交互文档供前端开发人员或外部开发者, 官网地址:https://swagger.io/. 但是在实际工作中,往往咋们的文档工作通常落后于实际的环境,导致文档和实际接口不一致,前后端开发人员苦不堪言. Swagger的出现解放了接口文档撰写的麻烦也提高了前后端开发者的工作效率,所谓"工欲善其事,必先利其器 ".现在让咋们 了解下在.NET Core 2.1下如何实现Swagger. 1.N

  • .Net core 的热插拔机制的深入探索及卸载问题求救指南

    一.依赖文件*.deps.json的读取. 依赖文件内容如下.一般位于编译生成目录中 { "runtimeTarget": { "name": ".NETCoreApp,Version=v3.1", "signature": "" }, "compilationOptions": {}, "targets": { ".NETCoreApp,Version=v

  • Kotlin扩展函数及实现机制的深入探索

    前言 2017年Google IO大会宣布使用Kotlin作为Android的官方开发语言,相比较与典型的面相对象的JAVA语言,Kotlin作为一种新式的函数式编程语言,也有人称之为Android平台的Swift语言. 先让我们看下实现同样的功能,Java和Kotiln的对比: // JAVA,20多行代码,充斥着findViewById,类型转换,匿名内部类这样的无意义代码 public class MainJavaActivity extends Activity { @Override

  • .NET Core中本地化机制的深入讲解

    前言 ASP.NET Core中提供了一些本地化服务和中间件,可将网站本地化为不同的语言文化. ASP.NET Core中我们可以使用Microsoft.AspNetCore.Localization库来实现本地化. 在.NET Core 2.0以上版本, Microsoft.AspNetCore.Localization已经包含在了Microsoft.AspNetCore.All中,所以我们并不需要手动引入其他的类库. 创建一个MVC网站 为了测试ASP.NET Core的本地化,我们首先在V

  • 理解ASP.NET Core 错误处理机制(Handle Errors)

    目录 使用中间件进行错误处理 开发人员异常页 IDeveloperPageExceptionFilter 异常处理程序 通过lambda提供异常处理程序 异常处理程序页 无响应正文的Http错误状态码处理 UseStatusCodePages UseStatusCodePagesWithRedirects UseStatusCodePagesWithReExecute 使用过滤器进行错误处理 错误处理中间件 VS 异常过滤器 注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博

  • ASP.NET Core 中间件的使用之全局异常处理机制

    目录 1.创建项目 2.创建全局异常过滤器 3.依赖注入全局异常处理机制 4.测试全局异常处理机制 前言: 我们经常听到"秒修复秒上线",觉得很厉害的样子. 其实不然,这只是一个调侃而已,出现问题的方式很多(逻辑漏洞.代码异常.操作方式不正确等). 我们今天来说代码异常问题怎么快速定位,减少不必要的时间浪费. 这就是今天的主题"添加全局异常处理机制"捕捉异常存储到数据库(mongodb.SqlServer.MySQL等). PS:输出txt的话不怎么友好,不是所有人

  • .Net Core3 用Windows 桌面应用开发Asp.Net Core网站

    前言 曾经在开发 Asp.Net 网站时就在想,为什么一定要把网站挂到 IIS 上?网站项目的 Main 函数哪儿去了?后来才知道这个 Main 函数在 w3wp.exe 里,这也是 IIS 的主进程.Asp.Net 网站的命门被 IIS 捏着,我无力改变.有时需要临时搭建一个简单的 Web 服务器,去网上一通度娘,发现了MyWebServer.HFS (HTTP File Server) 等简单实用的 Web 服务器,而且它们还是桌面应用.我就在想,什么时候能用 .Net 来开发一个桌面 We

  • Asp.Net Core Web应用程序—探索

    前言 作为一个Windows系统下的开发者,我对于Core的使用机会几乎为0,但是考虑到微软的战略规划,我觉得,Core还是有先了解起来的必要. 因为,目前微软已经搞出了两个框架了,一个是Net标准(.NetFramework),一个是Net Core. 而新特性的更新几乎都是在Net Core这个框架中. 所以,考虑到未来,一旦Core完善了,那微软肯定会放弃现在的.NetFrameWork. 因此,.Net程序员集体改用Net Core,想来,一定是大趋势. 所以让我们怀着探索的精神来看看A

  • ASP.NET Core框架探索之Authentication的权限认证过程解析

    今天我们来探索一下ASP.NET Core中关于权限认证,所谓权限认证,就是通过某些方式获取到用户的信息. 需要开启权限认证,我们首先需要在容器中注入认证服务,使用services.AddAuthentication.进入该方法的源码,最重要的其实就是AddAuthenticationCore方法,他向容器中注入了认证体系中很重要的对象:IAuthenticationService.IAuthenticationHandlerProvider.IAuthenticationSchemeProvi

  • ASP.NET Core如何添加统一模型验证处理机制详解

    一.前言 模型验证自ASP.NET MVC便有提供,我们可以在Model(DTO)的属性上加上数据注解(Data Annotations)特性,在进入Action之前便会根据数据注解,来验证输入的数据是否合法,下面介绍以下如何统一处理验证并返回错误信息.话不多说了,来一起看看详细的介绍吧. 二.Action过滤器实现统一验证 我们在判断验证状态时一般会在Action里判断ModelState.IsValid是否为true. public IActionResult Create([FromBod

  • ASP.Net Core中的内存和GC机制

    托管代码 在 .NET 中, CLR(Common Language Runtime) 负责提取托管代码并编译成机器语言,然后执行它.在此过程中,CLR 提供自动内存管理.安全边界.类型安全等服务,保证了代码安全. 托管代码指在其执行过程中由 CLR(Common Language Runtime) 管理的代码,托管代码是可在 .NET 上运行得一种高级语言(C#.F#等),编写的托管代码被编译后会被生成 中间语言(IL). CLR 有 .NET Core/.NET5+.Mono..NET Fr

随机推荐