Files
LanMountainDesktop/docs/Plugins develop/02-核心概念与原理/01-插件生命周期.md
lincube abfa64b3d7 Avalonia12 (#7)
* ava12升级

* Enable centralized package versioning

Add <Project> and <PropertyGroup> with <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> to Directory.Packages.props to enable centralized package version management across the repository. This allows package versions to be controlled from this single file instead of individual project files.

* Migrate codebase to Avalonia 12 APIs

Apply Avalonia 12 migration changes: replace SystemDecorations with WindowDecorations and remove ExtendClientAreaChromeHints/ExtendClientAreaTitleBarHeightHint usages; update BindingPlugins removal logic (no-op); switch clipboard usage to ClipboardExtensions.SetTextAsync; update Bitmap.CopyPixels calls to the new signature. Replace TextBox.Watermark with PlaceholderText, convert NumberBox styles to FANumberBox and adjust templates, change Checked/Unchecked handlers to IsCheckedChanged, and adapt FluentIcons usages (SymbolIconSource -> FASymbol/FAFont/FluentIcon equivalents). Fix MainWindow partial classes to inherit Window and correct missing variables/fields/usings. Add migration docs/specs/tasks under .trae and include a small TestFluentIcons project for icon testing.

* Migrate to Avalonia 12 and Plugin SDK v5

Upgrade project to the Avalonia 12 baseline and Plugin SDK v5: centralize Avalonia packages, remove legacy WebView.Avalonia usage (use NativeWebView/WebView2 EnvironmentRequested), and update Fluent/Material icon/package usages. Bump multiple package/project versions to 5.0.0 and Avalonia 12.0.1, update plugin template and README/docs to SDK v5, and add PLUGIN_SDK_V5_MIGRATION.md.

Also fix runtime/behavior bugs: make DataLocationResolver use a fixed bootstrap launcher data path and avoid recursive ResolveDataRoot; add legacy-state handling and extraction in OobeStateService; and update component settings tests to reflect migrated storage (DB/backup) and reset cache for test reloads. Various csproj, tests, and docs updated to reflect the migration and ensure build/test compatibility.

* Update icon glyphs and symbol mappings

Replace and refine icon sources across settings pages and controls: many FAFontIconSource glyphs were updated to specific Seagull Fluent Icons codepoints, some FASymbolIconSource usages were replaced with FAFontIconSource, and a number of symbol-to-Symbol enum mappings were adjusted (e.g. "Bell" -> AlertOn, "Shield" -> ShieldLock). Also clarified a comment in SettingsWindow and fixed a trailing newline in StudySettingsPage. Changes standardize icon visuals and bridge FluentIcons glyphs into FluentAvalonia icon sources.

* fix.修复合并产生的问题。
2026-04-29 12:14:29 +08:00

13 KiB
Raw Blame History

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. 实例化插件入口类

代码示例:

[PluginEntrance]  // ← 这个特性标记入口类
public sealed class Plugin : PluginBase
{
    // 插件实例在此阶段被创建
}

⚠️ 重要: 此阶段不要执行耗时操作,只应进行简单的字段初始化。


阶段 3初始化Initialize

时机: 插件加载完成后

这是插件开发中最重要的阶段!

方法签名

public override void Initialize(
    HostBuilderContext context,      // 宿主构建上下文
    IServiceCollection services)     // 服务注册集合

可执行的操作

可以做的:

  • 注册桌面组件
  • 注册设置页面
  • 注册服务到依赖注入容器
  • 读取配置
  • 初始化资源

不应该做的:

  • 访问 UI 元素UI 尚未就绪)
  • 执行耗时阻塞操作
  • 创建窗口或对话框

典型初始化代码

public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
    // 1. 注册服务
    services.AddSingleton<IWeatherService, WeatherService>();
    
    // 2. 注册桌面组件
    services.AddPluginDesktopComponent<WeatherWidget>(
        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 v5 暂无显式的卸载回调方法
  • 资源释放依赖 .NET 垃圾回收
  • 建议:
    • 使用 IDisposable 模式管理资源
    • 在组件卸载事件中清理资源

⏱️ 启动时序图

阑山桌面          插件系统           你的插件
    │                │                │
    │── 启动 ───────►│                │
    │                │                │
    │                │── 发现插件 ───►│
    │                │                │ (读取 plugin.json)
    │                │◄───────────────│
    │                │                │
    │                │── 加载 DLL ───►│
    │                │                │ (AssemblyLoadContext)
    │                │◄───────────────│
    │                │                │
    │                │── 创建实例 ───►│
    │                │                │ (调用构造函数)
    │                │◄───────────────│
    │                │                │
    │                │── Initialize ─►│
    │                │                │ (注册组件/服务)
    │                │◄───────────────│
    │                │                │
    │◄───────────────│                │
    │                │                │
    │── UI就绪 ─────►│                │
    │                │                │
    │                │── 用户添加组件 ─►│
    │                │                │ (创建组件实例)

💡 最佳实践

1. Initialize 方法保持轻量

// ✅ 好的做法:快速注册
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
    services.AddSingleton<IDataService, DataService>();
    services.AddPluginDesktopComponent<MyWidget>(options);
}

// ❌ 避免:耗时操作
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
    // 不要这样做!
    var data = FetchDataFromInternet().Result;  // 阻塞!
    services.AddSingleton(data);
}

2. 延迟加载数据

public class MyWidget : Border
{
    private readonly IDataService _dataService;
    
    public MyWidget(PluginDesktopComponentContext context)
    {
        _dataService = context.ServiceProvider.GetRequiredService<IDataService>();
        
        // 在 Loaded 事件中加载数据,而不是构造函数
        Loaded += async (_, _) =>
        {
            await LoadDataAsync();
        };
    }
    
    private async Task LoadDataAsync()
    {
        var data = await _dataService.GetDataAsync();
        // 更新 UI
    }
}

3. 正确处理资源释放

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. 避免循环依赖

// ❌ 避免:服务之间相互依赖
public class ServiceA
{
    public ServiceA(ServiceB b) { }  // 循环依赖风险
}

public class ServiceB
{
    public ServiceB(ServiceA a) { }
}

// ✅ 好的做法:使用接口解耦
public class ServiceA
{
    public ServiceA(IServiceB b) { }
}

🐛 常见问题

问题 1Initialize 中访问 UI 报错

现象: InvalidOperationException 或空引用

原因: Initialize 在 UI 就绪前调用

解决: 延迟到组件创建后再访问 UI

问题 2服务注册顺序问题

现象: 依赖注入找不到服务

原因: 服务注册顺序不正确

解决: 先注册服务,再注册依赖这些服务的组件

问题 3插件加载慢

现象: 宿主启动变慢

原因: Initialize 中执行耗时操作

解决: 将耗时操作移到后台线程或延迟执行


📚 参考资源


🎯 下一步

理解生命周期后,学习如何创建桌面组件:

👉 02-桌面组件系统 - 创建可视化组件


最后更新2026年4月