# 01-插件生命周期 理解插件的生命周期,是开发稳定可靠插件的基础。本文详细讲解插件从加载到卸载的完整过程。 --- ## 🔄 生命周期概览 ``` ┌─────────────────────────────────────────────────────────────┐ │ 阑山桌面启动 │ └───────────────────────┬─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 1. 发现插件 │ │ - 扫描插件目录 │ │ - 解析 plugin.json │ │ - 验证 API 版本兼容性 │ └───────────────────────┬─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 2. 加载插件 │ │ - 创建 AssemblyLoadContext │ │ - 加载插件 DLL │ │ - 查找入口类(带 [PluginEntrance] 特性) │ └───────────────────────┬─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 3. 初始化(Initialize) │ │ - 调用 Plugin.Initialize() │ │ - 注册组件、设置页面、服务 │ │ - ⚠️ 此时 UI 尚未完全就绪 │ └───────────────────────┬─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 4. 运行中 │ │ - 组件被添加到桌面 │ │ - 用户与组件交互 │ │ - 设置页面被打开 │ └───────────────────────┬─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 5. 停用/卸载 │ │ - 用户禁用插件 │ │ - 或关闭阑山桌面 │ │ - 释放资源(当前版本无显式卸载回调) │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 📋 各阶段详解 ### 阶段 1:发现插件 **时机:** 阑山桌面启动时 **过程:** 1. 扫描 `%LOCALAPPDATA%\LanMountainDesktop\plugins\` 目录 2. 读取每个 `.laapp` 包中的 `plugin.json` 3. 验证 `apiVersion` 是否与宿主兼容 4. 检查 `id` 是否唯一 **可能失败的原因:** - `plugin.json` 格式错误 - `apiVersion` 不兼容 - `id` 与其他插件冲突 --- ### 阶段 2:加载插件 **时机:** 发现成功后 **过程:** 1. 创建独立的 `AssemblyLoadContext` 2. 加载插件 DLL 及其依赖项 3. 查找带有 `[PluginEntrance]` 特性的类 4. 实例化插件入口类 **代码示例:** ```csharp [PluginEntrance] // ← 这个特性标记入口类 public sealed class Plugin : PluginBase { // 插件实例在此阶段被创建 } ``` ⚠️ **重要:** 此阶段**不要**执行耗时操作,只应进行简单的字段初始化。 --- ### 阶段 3:初始化(Initialize) **时机:** 插件加载完成后 **这是插件开发中最重要的阶段!** #### 方法签名 ```csharp public override void Initialize( HostBuilderContext context, // 宿主构建上下文 IServiceCollection services) // 服务注册集合 ``` #### 可执行的操作 ✅ **可以做的:** - 注册桌面组件 - 注册设置页面 - 注册服务到依赖注入容器 - 读取配置 - 初始化资源 ❌ **不应该做的:** - 访问 UI 元素(UI 尚未就绪) - 执行耗时阻塞操作 - 创建窗口或对话框 #### 典型初始化代码 ```csharp public override void Initialize(HostBuilderContext context, IServiceCollection services) { // 1. 注册服务 services.AddSingleton(); // 2. 注册桌面组件 services.AddPluginDesktopComponent( new PluginDesktopComponentOptions { ComponentId = "MyPlugin.Weather", DisplayName = "天气", IconKey = "Weather", Category = "工具", MinWidthCells = 4, MinHeightCells = 3 }); // 3. 注册设置页面 services.AddPluginSettingsSection( "myplugin-settings", "天气设置", section => section .AddToggle("auto_refresh", "自动刷新", defaultValue: true) .AddNumber("interval", "刷新间隔(分钟)", defaultValue: 30), iconKey: "Settings"); } ``` --- ### 阶段 4:运行中 **时机:** 初始化完成后,直到插件被禁用或宿主关闭 **特点:** - 组件可以被添加到桌面 - 用户可以与组件交互 - 设置页面可以被打开 - 定时器可以运行 #### 组件生命周期 ``` 用户添加组件 │ ▼ ┌─────────────────┐ │ 创建组件实例 │ ← 调用构造函数 │ (Dependency │ 注入 IServiceProvider │ Injection) │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ 组件初始化 │ ← 可在此时加载数据 │ (Loaded事件) │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ 渲染显示 │ ← 用户看到组件 └────────┬────────┘ │ ┌────┴────┐ ▼ ▼ 用户交互 定时更新 │ │ └────┬────┘ │ ▼ ┌─────────────────┐ │ 组件移除 │ ← 用户删除组件 │ (Unloaded事件) │ 或关闭宿主 └─────────────────┘ ``` --- ### 阶段 5:停用/卸载 **时机:** - 用户在设置中禁用插件 - 卸载插件 - 关闭阑山桌面 **当前限制:** - SDK v4 暂无显式的卸载回调方法 - 资源释放依赖 .NET 垃圾回收 - 建议: - 使用 `IDisposable` 模式管理资源 - 在组件卸载事件中清理资源 --- ## ⏱️ 启动时序图 ``` 阑山桌面 插件系统 你的插件 │ │ │ │── 启动 ───────►│ │ │ │ │ │ │── 发现插件 ───►│ │ │ │ (读取 plugin.json) │ │◄───────────────│ │ │ │ │ │── 加载 DLL ───►│ │ │ │ (AssemblyLoadContext) │ │◄───────────────│ │ │ │ │ │── 创建实例 ───►│ │ │ │ (调用构造函数) │ │◄───────────────│ │ │ │ │ │── Initialize ─►│ │ │ │ (注册组件/服务) │ │◄───────────────│ │ │ │ │◄───────────────│ │ │ │ │ │── UI就绪 ─────►│ │ │ │ │ │ │── 用户添加组件 ─►│ │ │ │ (创建组件实例) ``` --- ## 💡 最佳实践 ### 1. Initialize 方法保持轻量 ```csharp // ✅ 好的做法:快速注册 public override void Initialize(HostBuilderContext context, IServiceCollection services) { services.AddSingleton(); services.AddPluginDesktopComponent(options); } // ❌ 避免:耗时操作 public override void Initialize(HostBuilderContext context, IServiceCollection services) { // 不要这样做! var data = FetchDataFromInternet().Result; // 阻塞! services.AddSingleton(data); } ``` ### 2. 延迟加载数据 ```csharp public class MyWidget : Border { private readonly IDataService _dataService; public MyWidget(PluginDesktopComponentContext context) { _dataService = context.ServiceProvider.GetRequiredService(); // 在 Loaded 事件中加载数据,而不是构造函数 Loaded += async (_, _) => { await LoadDataAsync(); }; } private async Task LoadDataAsync() { var data = await _dataService.GetDataAsync(); // 更新 UI } } ``` ### 3. 正确处理资源释放 ```csharp public class MyWidget : Border, IDisposable { private readonly Timer _timer; private bool _disposed; public MyWidget() { _timer = new Timer(OnTimerTick, null, TimeSpan.Zero, TimeSpan.FromMinutes(1)); // 在卸载时释放资源 Unloaded += (_, _) => Dispose(); } public void Dispose() { if (_disposed) return; _disposed = true; _timer?.Dispose(); } } ``` ### 4. 避免循环依赖 ```csharp // ❌ 避免:服务之间相互依赖 public class ServiceA { public ServiceA(ServiceB b) { } // 循环依赖风险 } public class ServiceB { public ServiceB(ServiceA a) { } } // ✅ 好的做法:使用接口解耦 public class ServiceA { public ServiceA(IServiceB b) { } } ``` --- ## 🐛 常见问题 ### 问题 1:Initialize 中访问 UI 报错 **现象:** `InvalidOperationException` 或空引用 **原因:** Initialize 在 UI 就绪前调用 **解决:** 延迟到组件创建后再访问 UI ### 问题 2:服务注册顺序问题 **现象:** 依赖注入找不到服务 **原因:** 服务注册顺序不正确 **解决:** 先注册服务,再注册依赖这些服务的组件 ### 问题 3:插件加载慢 **现象:** 宿主启动变慢 **原因:** Initialize 中执行耗时操作 **解决:** 将耗时操作移到后台线程或延迟执行 --- ## 📚 参考资源 - [PluginBase 源码](../../LanMountainDesktop.PluginSdk/PluginBase.cs) - [IPlugin 接口](../../LanMountainDesktop.PluginSdk/IPlugin.cs) - [02-桌面组件系统](02-桌面组件系统.md) - [03-设置系统集成](03-设置系统集成.md) --- ## 🎯 下一步 理解生命周期后,学习如何创建桌面组件: 👉 **[02-桌面组件系统](02-桌面组件系统.md)** - 创建可视化组件 --- *最后更新:2026年4月*