码讲解
接下来是对宿主的源代码中各个部分的详细讲解:
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, $"