Files
LanMountainDesktop/docs/01-插件开发/02-核心概念/01-插件生命周期.md
2026-06-08 12:18:58 +08:00

17 KiB

插件生命周期

本文档详细介绍阑山桌面插件的生命周期、加载流程和各个阶段的职责。

生命周期概览

插件从加载到卸载经历以下阶段:

┌─────────────────────────────────────────────────────────┐
│                    插件生命周期                           │
└─────────────────────────────────────────────────────────┘

1. 发现 (Discovery)
   ├─ 扫描插件目录
   ├─ 读取 plugin.json
   └─ 验证基本信息
         ↓
2. 加载 (Load)
   ├─ 创建 PluginLoadContext
   ├─ 加载程序集
   ├─ 解析依赖关系
   └─ 验证兼容性
         ↓
3. 实例化 (Instantiate)
   ├─ 反射查找 IPlugin 实现
   ├─ 创建插件实例
   └─ 注入依赖
         ↓
4. 初始化 (Initialize)
   ├─ 调用 InitializeAsync()
   ├─ 注册组件
   ├─ 注册设置页
   ├─ 注册服务
   └─ 订阅事件
         ↓
5. 运行中 (Running)
   ├─ 组件渲染和更新
   ├─ 处理用户交互
   ├─ 响应事件
   └─ 执行后台任务
         ↓
6. 关闭 (Shutdown)
   ├─ 调用 ShutdownAsync()
   ├─ 保存状态
   ├─ 取消订阅
   ├─ 清理资源
   └─ 卸载程序集

详细阶段说明

1. 发现阶段 (Discovery)

时机: 宿主启动时

职责: 扫描和识别插件

流程:

// PluginDiscoveryService.cs (宿主代码)
public List<PluginDescriptor> DiscoverPlugins()
{
    var pluginsDir = Path.Combine(
        AppDataPath,
        "plugins"
    );
    
    var descriptors = new List<PluginDescriptor>();
    
    // 1. 扫描插件目录
    foreach (var dir in Directory.GetDirectories(pluginsDir))
    {
        var manifestPath = Path.Combine(dir, "plugin.json");
        
        // 2. 读取 plugin.json
        if (!File.Exists(manifestPath))
        {
            _logger.LogWarning($"Plugin manifest not found: {dir}");
            continue;
        }
        
        try
        {
            var json = File.ReadAllText(manifestPath);
            var manifest = JsonSerializer.Deserialize<PluginManifest>(json);
            
            // 3. 验证基本信息
            if (string.IsNullOrEmpty(manifest?.Id))
            {
                _logger.LogWarning($"Invalid plugin manifest: {dir}");
                continue;
            }
            
            descriptors.Add(new PluginDescriptor
            {
                Id = manifest.Id,
                Name = manifest.Name,
                Version = manifest.Version,
                Directory = dir,
                Manifest = manifest
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, $"Failed to read plugin manifest: {dir}");
        }
    }
    
    return descriptors;
}

开发者注意事项:

  • 确保 plugin.json 存在且格式正确
  • 确保插件 ID 唯一
  • 确保版本号符合语义化版本规范

2. 加载阶段 (Load)

时机: 发现插件后

职责: 加载插件程序集

流程:

// PluginLoader.cs (宿主代码)
public PluginLoadResult LoadPlugin(PluginDescriptor descriptor)
{
    try
    {
        // 1. 创建隔离的加载上下文
        var loadContext = new PluginLoadContext(descriptor.Directory);
        
        // 2. 查找主程序集
        var assemblyPath = Path.Combine(
            descriptor.Directory,
            $"{descriptor.Id}.dll"
        );
        
        if (!File.Exists(assemblyPath))
        {
            return PluginLoadResult.Failed(
                $"Plugin assembly not found: {assemblyPath}"
            );
        }
        
        // 3. 加载程序集
        var assembly = loadContext.LoadFromAssemblyPath(assemblyPath);
        
        // 4. 验证依赖
        if (!ValidateDependencies(descriptor.Manifest.Dependencies))
        {
            return PluginLoadResult.Failed("Dependency validation failed");
        }
        
        // 5. 验证宿主版本兼容性
        if (!IsHostVersionCompatible(descriptor.Manifest.MinHostVersion))
        {
            return PluginLoadResult.Failed(
                $"Incompatible host version. Required: {descriptor.Manifest.MinHostVersion}"
            );
        }
        
        return PluginLoadResult.Success(assembly, loadContext);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, $"Failed to load plugin: {descriptor.Id}");
        return PluginLoadResult.Failed(ex.Message);
    }
}

依赖解析:

// plugin.json
{
  "Dependencies": [
    {
      "PluginId": "com.example.anotherplugin",
      "MinVersion": "1.0.0"
    }
  ]
}

开发者注意事项:

  • 主程序集名称应与插件 ID 匹配(或在清单中指定)
  • 所有依赖的 DLL 应在插件目录中
  • 声明对其他插件的依赖关系

3. 实例化阶段 (Instantiate)

时机: 程序集加载后

职责: 创建插件实例

流程:

// PluginActivator.cs (宿主代码)
public IPlugin? CreatePluginInstance(Assembly assembly)
{
    try
    {
        // 1. 查找 IPlugin 实现类
        var pluginType = assembly.GetTypes()
            .FirstOrDefault(t => 
                typeof(IPlugin).IsAssignableFrom(t) &&
                !t.IsAbstract &&
                !t.IsInterface
            );
        
        if (pluginType == null)
        {
            _logger.LogError("No IPlugin implementation found");
            return null;
        }
        
        // 2. 创建实例
        var plugin = Activator.CreateInstance(pluginType) as IPlugin;
        
        if (plugin == null)
        {
            _logger.LogError("Failed to create plugin instance");
            return null;
        }
        
        return plugin;
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to instantiate plugin");
        return null;
    }
}

开发者注意事项:

  • 插件类必须有公共无参构造函数
  • 一个插件程序集只能有一个 IPlugin 实现
  • 不要在构造函数中执行耗时操作

4. 初始化阶段 (Initialize)

时机: 插件实例创建后

职责: 注册组件、服务和事件

插件代码示例:

public class MyPlugin : IPlugin
{
    public async Task InitializeAsync(IPluginContext context)
    {
        // 1. 记录日志
        context.Logger.LogInformation($"{Name} is initializing...");
        
        // 2. 注册组件
        var componentRegistry = context.Services
            .GetService<IComponentRegistry>();
        
        if (componentRegistry != null)
        {
            // 注册多个组件
            componentRegistry.RegisterComponent<WeatherComponent>();
            componentRegistry.RegisterComponent<ClockComponent>();
            
            context.Logger.LogInformation("Components registered");
        }
        
        // 3. 注册设置页
        var settingsRegistry = context.Services
            .GetService<ISettingsPageRegistry>();
        
        if (settingsRegistry != null)
        {
            settingsRegistry.RegisterPage<MySettingsPage>(
                title: "我的插件",
                category: "插件"
            );
            
            context.Logger.LogInformation("Settings page registered");
        }
        
        // 4. 注册公共 IPC 服务(如果需要)
        var ipcBuilder = context.Services
            .GetService<IPluginPublicIpcBuilder>();
        
        if (ipcBuilder != null)
        {
            ipcBuilder.AddService<IMyPublicService>(
                objectId: "default",
                notifyIds: new[] { "myplugin.event.changed" }
            );
        }
        
        // 5. 订阅宿主事件
        var eventBus = context.Services
            .GetService<IEventBus>();
        
        if (eventBus != null)
        {
            eventBus.Subscribe<ThemeChangedEvent>(OnThemeChanged);
        }
        
        // 6. 初始化后台服务(如果有)
        await InitializeBackgroundServicesAsync(context);
        
        context.Logger.LogInformation($"{Name} initialized successfully");
    }
    
    private void OnThemeChanged(ThemeChangedEvent evt)
    {
        // 响应主题变更
    }
    
    private async Task InitializeBackgroundServicesAsync(IPluginContext context)
    {
        // 启动定时任务等
        await Task.CompletedTask;
    }
}

初始化最佳实践:

public async Task InitializeAsync(IPluginContext context)
{
    try
    {
        // ✅ 使用 try-catch 捕获异常
        // ✅ 记录详细的日志
        // ✅ 验证服务是否可用
        // ✅ 使用 async/await 处理异步操作
        // ❌ 不要阻塞 UI 线程
        // ❌ 不要执行超过 5 秒的操作
        
        _context = context;
        
        // 快速初始化
        RegisterComponents(context);
        RegisterSettings(context);
        
        // 耗时操作使用后台任务
        _ = Task.Run(async () => 
        {
            await LoadDataAsync();
        });
    }
    catch (Exception ex)
    {
        context.Logger.LogError(ex, "Plugin initialization failed");
        throw; // 让宿主知道初始化失败
    }
}

开发者注意事项:

  • InitializeAsync 应尽快完成(< 5 秒)
  • 耗时操作放在后台线程
  • 妥善处理异常
  • 保存 IPluginContext 引用供后续使用
  • 不要在此阶段访问其他插件的服务(可能还未加载)

5. 运行中阶段 (Running)

时机: 初始化完成后

职责: 响应用户交互和系统事件

组件更新循环:

// 宿主会定时调用组件的 UpdateAsync()
public class MyComponent : ComponentBase
{
    private HttpClient _httpClient;
    private DateTime _lastUpdate;
    
    public override async Task UpdateAsync()
    {
        // 定时更新数据(默认 1 秒调用一次)
        if (DateTime.Now - _lastUpdate > TimeSpan.FromMinutes(5))
        {
            await FetchDataAsync();
            _lastUpdate = DateTime.Now;
        }
    }
    
    private async Task FetchDataAsync()
    {
        try
        {
            var data = await _httpClient.GetStringAsync("https://api.example.com/data");
            // 更新组件属性
            Data = ParseData(data);
        }
        catch (Exception ex)
        {
            Logger.LogError(ex, "Failed to fetch data");
        }
    }
}

事件响应:

public class MyPlugin : IPlugin
{
    public async Task InitializeAsync(IPluginContext context)
    {
        // 订阅系统事件
        var eventBus = context.Services.GetService<IEventBus>();
        
        eventBus?.Subscribe<ThemeChangedEvent>(OnThemeChanged);
        eventBus?.Subscribe<LanguageChangedEvent>(OnLanguageChanged);
        eventBus?.Subscribe<SettingChangedEvent>(OnSettingChanged);
    }
    
    private void OnThemeChanged(ThemeChangedEvent evt)
    {
        _logger.LogInformation($"Theme changed to: {evt.NewTheme}");
        // 更新组件外观
    }
    
    private void OnLanguageChanged(LanguageChangedEvent evt)
    {
        _logger.LogInformation($"Language changed to: {evt.NewLanguage}");
        // 重新加载本地化资源
    }
    
    private void OnSettingChanged(SettingChangedEvent evt)
    {
        if (evt.Key.StartsWith("MyPlugin."))
        {
            // 响应插件设置变更
        }
    }
}

开发者注意事项:

  • 组件更新应快速完成
  • 使用缓存避免重复计算
  • 异步操作使用 async/await
  • 妥善处理网络错误
  • 不要在 UpdateAsync 中执行超过 1 秒的操作

6. 关闭阶段 (Shutdown)

时机:

  • 宿主应用退出
  • 插件被禁用
  • 插件热重载

职责: 清理资源和保存状态

插件代码示例:

public class MyPlugin : IPlugin
{
    private IDisposable? _eventSubscription;
    private HttpClient? _httpClient;
    private CancellationTokenSource? _cts;
    
    public async Task ShutdownAsync()
    {
        try
        {
            _logger.LogInformation($"{Name} is shutting down...");
            
            // 1. 取消正在进行的操作
            _cts?.Cancel();
            
            // 2. 取消事件订阅
            _eventSubscription?.Dispose();
            
            // 3. 保存状态
            await SaveStateAsync();
            
            // 4. 释放资源
            _httpClient?.Dispose();
            
            // 5. 停止后台任务
            await StopBackgroundServicesAsync();
            
            _logger.LogInformation($"{Name} shutdown completed");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error during plugin shutdown");
            // 不要抛出异常,避免影响其他插件
        }
    }
    
    private async Task SaveStateAsync()
    {
        // 保存插件状态到设置
        _context?.Settings.SetValue("LastUpdateTime", DateTime.Now);
        await Task.CompletedTask;
    }
    
    private async Task StopBackgroundServicesAsync()
    {
        // 停止定时任务等
        await Task.CompletedTask;
    }
}

关闭最佳实践:

public async Task ShutdownAsync()
{
    try
    {
        // ✅ 尽快完成(< 3 秒)
        // ✅ 使用 try-catch 避免异常
        // ✅ 按相反顺序清理资源
        // ✅ 保存关键状态
        // ❌ 不要抛出异常
        // ❌ 不要执行耗时操作
        
        // 取消异步操作
        _cancellationTokenSource?.Cancel();
        
        // 取消订阅(防止内存泄漏)
        UnsubscribeEvents();
        
        // 释放托管资源
        DisposeResources();
        
        // 保存状态(快速)
        SaveCriticalState();
    }
    catch (Exception ex)
    {
        // 记录但不抛出
        _logger?.LogError(ex, "Shutdown error");
    }
}

开发者注意事项:

  • ShutdownAsync 必须快速完成(< 3 秒)
  • 取消所有异步操作
  • 取消事件订阅(防止内存泄漏)
  • 释放所有 IDisposable 资源
  • 保存关键状态
  • 不要抛出异常

生命周期事件

插件可以监听宿主的生命周期事件:

public class MyPlugin : IPlugin
{
    public async Task InitializeAsync(IPluginContext context)
    {
        var hostLifecycle = context.Services
            .GetService<IHostLifecycleService>();
        
        if (hostLifecycle != null)
        {
            hostLifecycle.Starting += OnHostStarting;
            hostLifecycle.Started += OnHostStarted;
            hostLifecycle.Stopping += OnHostStopping;
            hostLifecycle.Stopped += OnHostStopped;
        }
    }
    
    private void OnHostStarting(object? sender, EventArgs e)
    {
        // 宿主正在启动
    }
    
    private void OnHostStarted(object? sender, EventArgs e)
    {
        // 宿主已启动完成
    }
    
    private void OnHostStopping(object? sender, EventArgs e)
    {
        // 宿主即将关闭
    }
    
    private void OnHostStopped(object? sender, EventArgs e)
    {
        // 宿主已关闭
    }
    
    public async Task ShutdownAsync()
    {
        // 取消订阅
        var hostLifecycle = _context?.Services
            .GetService<IHostLifecycleService>();
        
        if (hostLifecycle != null)
        {
            hostLifecycle.Starting -= OnHostStarting;
            hostLifecycle.Started -= OnHostStarted;
            hostLifecycle.Stopping -= OnHostStopping;
            hostLifecycle.Stopped -= OnHostStopped;
        }
    }
}

错误处理

初始化失败

如果插件初始化失败,宿主会:

  1. 记录错误日志
  2. 标记插件为"加载失败"
  3. 继续加载其他插件
  4. 在 UI 中显示失败状态

运行时异常

组件代码中的未捕获异常:

  1. 被宿主捕获并记录
  2. 组件标记为"错误"状态
  3. 组件停止更新
  4. 不影响其他组件

关闭超时

如果 ShutdownAsync 超过 5 秒:

  1. 宿主强制终止
  2. 记录超时警告
  3. 继续关闭其他插件

插件热重载

宿主支持插件热重载(开发中功能):

1. 用户触发重载
    ↓
2. 调用 ShutdownAsync()
    ↓
3. 卸载程序集
    ↓
4. 重新加载程序集
    ↓
5. 创建新实例
    ↓
6. 调用 InitializeAsync()
    ↓
7. 恢复组件状态

小结

插件生命周期的关键点:

  • 发现: 确保 plugin.json 正确
  • 加载: 管理好依赖关系
  • 初始化: 快速注册,耗时操作后台执行
  • 运行: 高效更新,异步处理
  • 关闭: 及时清理,避免异常

下一步