Skip to content

内核的生命周期 ​

内核做的事很少:登记插件、按依赖顺序激活、停用时把一切收回原样。这一页讲它的三条纪律,以及两个必须小心的地方。

三条纪律 ​

代码评审按这三条卡(写在 Kernel.cs 头部):

  1. 内核里不许出现具体功能的名字。 出现下载、Nova、版本,就是设计错误。
  2. 激活顺序:引擎 → 功能 → 助手 → UI,依赖先于被依赖者,懒插件等第一次用到。
  3. 停用必须可逆:契约注销 + 贡献撤下 + 订阅断开,重启用不能重复注册。

第三条是最容易被写坏的一条。插件的 onLoad 里注册了面板、订阅了事件、提供了契约——停用时这些都得消失,不然插件页会看到已经卸载的插件还挂在导航栏里。

顺序与依赖 ​

StartAsync 按 PluginKind 依次激活,同种之内按登记顺序:

csharp
foreach (var kind in new[] { PluginKind.Engine, PluginKind.Feature, PluginKind.Assistant, PluginKind.Ui })
    foreach (var runtime in Snapshot(kind, PluginActivation.Eager))
        await ActivateAsync(runtime.Descriptor.Id, ct);

但登记顺序只是起点,真正的依赖关系由 ActivateAsync 递归处理:

csharp
foreach (var dependency in runtime.Descriptor.Depends.Keys)
{
    if (Find(dependency) is not { } dep) { Fail(runtime, $"缺少依赖插件 {dependency}"); return false; }
    if (dep.State is not PluginState.Active && !await ActivateAsync(dependency, ct))
    { Fail(runtime, $"依赖插件 {dependency} 激活失败"); return false; }
}

也就是说「引擎 → 功能 → 助手 → UI」是兜底顺序,声明了 Depends 的插件永远服从自己的依赖声明。图标缓存插件声明依赖下载插件,就一定在它之后激活,哪怕注册顺序反了。

循环依赖保护 ​

这里有个真实的坑,代码注释里留着单测抓到的现场:

csharp
// 循环依赖保护:状态必须在**递归激活依赖之前**置为 Activating,
// 否则 A→B→A 的依赖会让 ActivateAsync 无限递归到栈溢出(单测抓到过)
if (runtime.State is PluginState.Activating) return false;

runtime.State = PluginState.Activating;

顺序反过来就完了:A 把状态置成 Activating,再去递归激活 B;B 又要 A,此时 A 还是 Registered(因为状态还没改),于是 B 再次递归激活 A,无限下去,最后栈溢出。先把状态置为 Activating 再递归,递归进来的那一层看到 Activating 就知道「我已经在激活链路上了」,直接返回 false。

注意这里的处理是「失败」而不是「报错」——循环依赖里的插件激活失败,状态变 Failed,错误信息写进 runtime.Error,插件页上能看到原因。

同样的循环,插件层有另一套检测

外部插件的 dependencies 由 PluginDependencyResolver 处理:DFS 栈内回边即环,环上全部阻断,给出「循环依赖:A → B → A」这样的可读原因。内核这套是运行时保护,插件那套是加载前预检——两边都留着是因为它们防的不是同一件事。

懒激活 ​

不是所有插件都该在启动时起来。Get<T>() 在找不到实现时会找到「谁能提供这个契约」的插件,并同步激活它:

csharp
public T Get<T>() where T : class
{
    if (_services.TryGet<T>(out var service)) return service;
    if (ProviderOf(typeof(T)) is { State: PluginState.Registered } provider)
    {
        ActivateAsync(provider.Descriptor.Id).GetAwaiter().GetResult();   // 懒激活:第一次要用才起
        if (_services.TryGet<T>(out service)) return service;
    }
    throw new KernelException($"没有插件提供契约 {typeof(T).Name}" + …);
}

ProviderOf 靠 Descriptor.Provides 里声明的类型找提供者。所以 Provides 不只是文档——它是懒激活的索引。

停用的顺序 ​

DeactivateAsync 的 finally 块里顺序是有讲究的:

csharp
// 顺序很重要:先撤贡献,再注销契约,最后断订阅
_contributions.RemoveByPlugin(pluginId);
_services.RemoveByProvider(pluginId);
_instances.Remove(pluginId);
_contexts.Remove(pluginId);
runtime.State = PluginState.Stopped;

先撤贡献,外壳才不会在契约已经没了的时候还试图去取那个能力。订阅的断开靠 PluginContext.BeginShutdown()——上下文代管了所有 Subscribe 返回的句柄,插件忘了自己退订也不会泄漏:

csharp
internal void Track(IDisposable subscription)
{
    if (_shuttingDown) subscription.Dispose();
    else _subscriptions.Add(subscription);
}

为什么订阅要由上下文代管

一个插件崩在半路,它订阅的事件处理器还挂在总线上。别人一广播就调进一个已经不存在的插件,轻则报错,重则拖垮整个广播。代管让「忘记退订」不可能发生。

相关链接:插件与内核 · 权限模型

基于 MIT 许可证(有修改)开源