事件与回调
管理器与你的进程之间有三样东西往来:插件发布的事件、它们用的调度器,以及管理器回调你的那两个入口。
context.Events 是 IEventBus。整个进程共用一条总线:
public interface IEventBus{ IDisposable Subscribe<TEvent>(Action<TEvent> handler); void Publish<TEvent>(TEvent @event);}- 分发是同步的,在发布者的线程上。 订阅者阻塞,发布者就阻塞。
- 抛异常的订阅者被隔离。 异常被捕获并上报,其余订阅者照常执行。
- 按类型精确匹配。 用基类实例发布,不会命中按派生类型订阅的 handler。
- 订阅是进程级的。 插件卸载时框架会替它退订,漏掉
Dispose不会钉住插件——但显式退订仍是好习惯。
宿主可以在同一条总线上发布。载体事件就是这样到达插件的:载体发布,插件订阅,两边都不需要认识对方。
context.Scheduler 在线程池上跑任务:
IDisposable once = context.Scheduler.ScheduleOnce(TimeSpan.FromSeconds(5), () => { });IDisposable periodic = context.Scheduler.SchedulePeriodic(TimeSpan.FromMinutes(1), () => { });释放返回的句柄即取消。抛出的异常被捕获并上报,对周期任务来说,不影响后续的次数。
线程池是唯一的保证。"在游戏主线程上跑"是载体的能力,不是框架的——需要它的宿主得通过 carrierServices 提供,否则就要接受回调不在主线程上。
订阅与定时器都按插件作用域登记:卸载时框架先关闸门,之后不再有回调进来,再收回它们。
reportFault
Section titled “reportFault”reportFault 是构造函数里的回调,接收钩子之外发生的问题:子进程死了、协议出错、包在运行前被管理器拒绝。管理器带着可读的消息调用它。
reportFault: entry => logger.LogError("加载问题:{Entry}", entry)把它接到你自己日志的 error 级别。一次故障不会拖垮进程——单个插件失败是被隔离的——但它是"这个插件没在做它该做的事"的唯一信号。
进程内的代码在钩子之外遇到故障,可以自己上报:
context.ReportFault("子进程以退出码 1 结束");管理器按故障处理:卸载该插件,卸载成功后再调 onRemoved。
onRemoved
Section titled “onRemoved”故障插件卸载成功之后,onRemoved 带着插件名触发。它存在的意义是让宿主把这条从自己的注册表里摘掉——父级加载器的清单、状态页,任何在跟踪它的地方。
onRemoved: name => registry.Remove(name)它不是两件事:
- 不是通用的卸载钩子。 它只在故障路径上触发,你自己调
Unload时不会。 - 不保证触发。 卸载失败时插件还在,
onRemoved不会跑。
保持它简短且不抛异常。它跑在管理器的卸载路径里。
拥有事件源的宿主通常这么做:构造管理器时把 reportFault 与 onRemoved 接到自己的日志与注册表,载体事件经管理器的总线发布,插件在 OnLoad 里订阅一次、在 OnUnload 里释放。停用只关闭闸门,订阅还留着——在 OnEnable 里再订阅一次,每轮启停都会多出一个 handler。在 OnLoad 订阅,暂停交给闸门。插件卸载时没退掉的订阅,框架也会收走。
