设为首页 加入收藏

TOP

使用 .NET Core 3.0 的 AssemblyLoadContext 实现插件热加载(三)
2019-10-09 19:59:07 】 浏览:178
Tags:使用 .NET Core 3.0 AssemblyLoadContext 实现 插件 加载
码讲解

接下来是对宿主的源代码中各个部分的详细讲解:

IPlugin 接口

public interface IPlugin : IDisposable
{
    string GetMessage();
}

这是插件项目需要的实现接口,宿主项目在编译插件后会寻找程序集中实现 IPlugin 的类型,创建这个类型的实例并且使用它,创建插件时会调用构造函数,卸载插件时会调用 Dispose 方法。如果你用过 .NET Framework 的 AppDomain 机制可能会想是否需要 Marshalling 处理,答案是不需要,.NET Core 的可回收程序集会加载到当前的 AppDomain 中,回收时需要依赖 GC 清理,好处是使用简单并且运行效率高,坏处是 GC 清理有延迟,只要有一个插件中类型的实例没有被回收则插件程序集使用的数据会一直残留,导致内存泄漏。

PluginController 类型

internal class PluginController : IPlugin
{
    private List<Assembly> _defaultAssemblies;
    private AssemblyLoadContext _context;
    private string _pluginName;
    private string _pluginDirectory;
    private volatile IPlugin _instance;
    private volatile bool _changed;
    private object _reloadLock;
    private FileSystemWatcher _watcher;

这是管理插件的代理类,在内部它负责编译与加载插件,并且把对 IPlugin 接口的方法调用转发到插件的实现中。类成员包括默认 AssemblyLoadContext 中的程序集列表 _defaultAssemblies,用于加载插件的自定义 AssemblyLoadContext _context,插件名称与文件夹,插件实现 _instance,标记插件文件是否已改变的 _changed,防止多个线程同时编译加载插件的 _reloadLock,与监测插件文件变化的 _watcher

PluginController 的构造函数

public PluginController(string pluginName, string pluginDirectory)
{
    _defaultAssemblies = AssemblyLoadContext.Default.Assemblies
        .Where(assembly => !assembly.IsDynamic)
        .ToList();
    _pluginName = pluginName;
    _pluginDirectory = pluginDirectory;
    _reloadLock = new object();
    ListenFileChanges();
}

构造函数会从 AssemblyLoadContext.Default.Assemblies 中获取默认 AssemblyLoadContext 中的程序集列表,包括宿主程序集、System.Runtime 等,这个列表会在 Roslyn 编译插件时使用,表示插件编译时需要引用哪些程序集。之后还会调用 ListenFileChanges 监听插件文件是否有改变。

PluginController.ListenFileChanges

private void ListenFileChanges()
{
    Action<string> onFileChanged = path =>
    {
        if (Path.GetExtension(path).ToLower() == ".cs")
            _changed = true;
    };
    _watcher = new FileSystemWatcher();
    _watcher.Path = _pluginDirectory;
    _watcher.IncludeSubdirectories = true;
    _watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName;
    _watcher.Changed += (sender, e) => onFileChanged(e.FullPath);
    _watcher.Created += (sender, e) => onFileChanged(e.FullPath);
    _watcher.Deleted += (sender, e) => onFileChanged(e.FullPath);
    _watcher.Renamed += (sender, e) => { onFileChanged(e.FullPath); onFileChanged(e.OldFullPath); };
    _watcher.EnableRaisingEvents = true;
}

这个方法创建了 FileSystemWatcher,监听插件文件夹下的文件是否有改变,如果有改变并且改变的是 C# 源代码 (.cs 扩展名) 则设置 _changed 成员为 true,这个成员标记插件文件已改变,下次访问插件实例的时候会触发重新加载。

你可能会有疑问,为什么不在文件改变后立刻触发重新加载插件,一个原因是部分文件编辑器的保存文件实现可能会导致改变的事件连续触发几次,延迟触发可以避免编译多次,另一个原因是编译过程中出现的异常可以传递到访问插件实例的线程中,方便除错与调试 (尽管使用 ExceptionDispatchInfo 也可以做到)。

PluginController.UnloadPlugin

private void UnloadPlugin()
{
    _instance?.Dispose();
    _instance = null;
    _context?.Unload();
    _context = null;
}

这个方法会卸载已加载的插件,首先调用 IPlugin.Dispose 通知插件正在卸载,如果插件创建了新的线程可以在 Dispose 方法中停止线程避免泄漏,然后调用 AssemblyLoadContext.Unload 允许 .NET Core 运行时卸载这个上下文加载的程序集,程序集的数据会在 GC 检测到所有类型的实例都被回收后回收 (参考文章开头的链接)。

PluginController.CompilePlugin

private Assembly CompilePlugin()
{
    var binDirectory = Path.Combine(_pluginDirectory, "bin");
    var dllPath = Path.Combine(binDirectory, $"{_pluginName}.dll");
    if (!Directory.Exists(binDirectory))
        Directory.CreateDirectory(binDirectory);
    if (File.Exists(dllPath))
    {
        File.Delete($"{dllPath}.old");
        File.Move(dllPath, $"
首页 上一页 1 2 3 4 5 下一页 尾页 3/5/5
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇.NET开发者必须学习.NET Core 下一篇.netcore+vue+elementUI 前后端分..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目