Files
ui/docs/architecture/performance-optimization.md
2025-10-16 15:08:42 +08:00

10 KiB
Raw Blame History

Performance Optimization

本节定义性能优化策略和最佳实践,确保控件库满足 NFR 性能要求。


Performance Targets (from NFR)

指标 目标值 测量方法
UI 渲染帧率 60fps (16ms/frame) Visual Studio Performance Profiler
主题切换时间 < 100ms Stopwatch 测量
PropertyGrid 生成50 属性) < 200ms Stopwatch 测量
应用启动时间 < 2s 从启动到窗口显示
内存占用 < 100MB (空闲时) Task Manager / dotnet-counters

UI Rendering Performance

关键原则:避免在 UI 线程执行耗时操作

反面示例 - 阻塞 UI 线程:

// 错误:在属性 getter 中执行反射
public ObservableCollection<PropertyItem> Properties
{
    get
    {
        var items = new ObservableCollection<PropertyItem>();
        if (SelectedObject != null)
        {
            // 耗时操作会阻塞 UI 渲染
            var properties = SelectedObject.GetType().GetProperties();
            foreach (var prop in properties)
            {
                items.Add(CreatePropertyItem(SelectedObject, prop));
            }
        }
        return items;
    }
}

正确示例 - 异步加载:

private ObservableCollection<PropertyItem> _properties = new();
public ObservableCollection<PropertyItem> Properties => _properties;

private async void RefreshProperties(object? obj)
{
    _properties.Clear();

    if (obj == null) return;

    // 在后台线程执行反射
    var items = await Task.Run(() =>
    {
        var result = new List<PropertyItem>();
        var properties = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);

        foreach (var prop in properties)
        {
            result.Add(CreatePropertyItem(obj, prop));
        }

        return result;
    });

    // 回到 UI 线程更新集合
    foreach (var item in items)
    {
        _properties.Add(item);
    }
}

Reflection Optimization

问题:反射是 PropertyGrid 的性能瓶颈

优化策略 1缓存 PropertyInfo

private static readonly ConcurrentDictionary<Type, PropertyInfo[]> _propertyCache = new();

private PropertyInfo[] GetCachedProperties(Type type)
{
    return _propertyCache.GetOrAdd(type, t =>
        t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
    );
}

优化策略 2缓存 Attribute 读取

private static readonly ConcurrentDictionary<PropertyInfo, CategoryAttribute?> _categoryCache = new();

private string? GetCategory(PropertyInfo prop)
{
    var attr = _categoryCache.GetOrAdd(prop, p =>
        p.GetCustomAttribute<CategoryAttribute>()
    );

    return attr?.Category;
}

优化策略 3使用编译表达式替代反射调用可选

// 反射调用(慢)
var value = propertyInfo.GetValue(obj);

// 编译表达式(快 10-100 倍)
private static Func<object, object> CreateGetter(PropertyInfo prop)
{
    var instance = Expression.Parameter(typeof(object), "instance");
    var convert = Expression.Convert(instance, prop.DeclaringType!);
    var property = Expression.Property(convert, prop);
    var convertResult = Expression.Convert(property, typeof(object));

    return Expression.Lambda<Func<object, object>>(convertResult, instance).Compile();
}

性能对比:

反射 GetValue:       ~1000 ns/调用
编译表达式:          ~10 ns/调用
直接属性访问:        ~1 ns/调用

Theme Switching Performance

优化主题切换速度(目标 < 100ms

反面示例 - 逐个替换资源:

// 错误:逐个替换资源会触发多次 UI 刷新
foreach (var key in themeColors.Keys)
{
    Application.Current.Resources[key] = themeColors[key];
}

正确示例 - 批量替换 ResourceDictionary

public static void ApplyTheme(ThemeType type)
{
    var app = Application.Current;
    if (app == null) return;

    // 移除旧主题
    var oldTheme = app.Resources.MergedDictionaries
        .FirstOrDefault(d => d.Source?.ToString().Contains("Theme") == true);
    if (oldTheme != null)
        app.Resources.MergedDictionaries.Remove(oldTheme);

    // 一次性加载新主题(单次 UI 刷新)
    var uri = type == ThemeType.Light
        ? new Uri("avares://Penguin.AvaloniaUI/Themes/LightTheme.axaml")
        : new Uri("avares://Penguin.AvaloniaUI/Themes/DarkTheme.axaml");

    var newTheme = new ResourceDictionary { Source = uri };
    app.Resources.MergedDictionaries.Add(newTheme);

    _currentTheme = type;
    ThemeChanged?.Invoke(null, type);
}

预加载主题资源(可选优化):

private static ResourceDictionary? _cachedLightTheme;
private static ResourceDictionary? _cachedDarkTheme;

static ThemeManager()
{
    // 应用启动时预加载主题
    _cachedLightTheme = LoadThemeResource(ThemeType.Light);
    _cachedDarkTheme = LoadThemeResource(ThemeType.Dark);
}

public static void ApplyTheme(ThemeType type)
{
    var theme = type == ThemeType.Light ? _cachedLightTheme : _cachedDarkTheme;
    // 直接使用缓存的主题,无需重新加载
}

Memory Management

避免内存泄漏的常见陷阱:

问题 1事件订阅未取消

// ❌ 可能导致内存泄漏
public class PropertyGrid : TemplatedControl
{
    public PropertyGrid()
    {
        ThemeManager.ThemeChanged += OnThemeChanged;
        // 没有取消订阅!
    }

    private void OnThemeChanged(object? sender, ThemeType type)
    {
        RefreshTheme();
    }
}

// ✅ 正确实现 IDisposable
public class PropertyGrid : TemplatedControl, IDisposable
{
    public PropertyGrid()
    {
        ThemeManager.ThemeChanged += OnThemeChanged;
    }

    public void Dispose()
    {
        ThemeManager.ThemeChanged -= OnThemeChanged;
    }

    private void OnThemeChanged(object? sender, ThemeType type)
    {
        RefreshTheme();
    }
}

问题 2ObservableCollection 过度使用

// ❌ 每次都创建新集合
public ObservableCollection<PropertyItem> Properties
{
    get => new ObservableCollection<PropertyItem>(GetProperties());
}

// ✅ 复用同一个集合
private ObservableCollection<PropertyItem> _properties = new();
public ObservableCollection<PropertyItem> Properties => _properties;

private void RefreshProperties()
{
    _properties.Clear();
    foreach (var item in GetProperties())
    {
        _properties.Add(item);
    }
}

Virtualization (Post-MVP)

如果 PropertyGrid 需要支持数百个属性,使用虚拟化:

<ItemsControl Items="{Binding Properties}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <!-- VirtualizingStackPanel 仅渲染可见项 -->
            <VirtualizingStackPanel />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>

虚拟化性能对比:

100 个属性(非虚拟化): ~300ms 生成 + 50MB 内存
100 个属性(虚拟化):   ~100ms 生成 + 10MB 内存

Avoid Common Pitfalls

陷阱 1在循环中触发属性变化通知

// ❌ 每次循环触发 UI 刷新
for (int i = 0; i < 1000; i++)
{
    Properties.Add(new PropertyItem()); // 触发 CollectionChanged
}

// ✅ 批量添加后触发一次刷新
var items = new List<PropertyItem>();
for (int i = 0; i < 1000; i++)
{
    items.Add(new PropertyItem());
}
Properties.Clear();
foreach (var item in items)
{
    Properties.Add(item);
}

陷阱 2频繁的字符串拼接

// ❌ 每次拼接创建新字符串
string result = "";
for (int i = 0; i < 100; i++)
{
    result += properties[i].Name + ", ";
}

// ✅ 使用 StringBuilder
var sb = new StringBuilder();
for (int i = 0; i < 100; i++)
{
    sb.Append(properties[i].Name).Append(", ");
}
var result = sb.ToString();

陷阱 3XAML 中的复杂绑定

<!-- ❌ 每次渲染都执行转换 -->
<TextBlock Text="{Binding PropertyType, Converter={StaticResource ComplexConverter}}" />

<!-- ✅ 在 ViewModel 中预计算 -->
<TextBlock Text="{Binding PropertyTypeDisplayName}" />

Performance Measurement

使用 Stopwatch 测量关键操作:

#if DEBUG
private void RefreshProperties(object? obj)
{
    var sw = Stopwatch.StartNew();

    // 核心逻辑
    _properties.Clear();
    var items = GetProperties(obj);
    foreach (var item in items)
    {
        _properties.Add(item);
    }

    sw.Stop();
    Debug.WriteLine($"[PropertyGrid] RefreshProperties took {sw.ElapsedMilliseconds}ms for {items.Count} properties");
}
#endif

Visual Studio Performance Profiler

  1. 调试 → 性能探查器
  2. 选择"CPU 使用情况"或".NET 对象分配跟踪"
  3. 启动分析
  4. 执行性能关键操作(如生成 50 个属性)
  5. 停止分析并查看热点代码

dotnet-counters 监控内存:

# 安装工具
dotnet tool install --global dotnet-counters

# 监控运行中的应用
dotnet-counters monitor --process-id <pid> --counters System.Runtime

Optimization Checklist

开发时检查清单:

  • 反射操作已缓存PropertyInfo、Attribute
  • 耗时操作(> 50ms在后台线程执行
  • 事件订阅在控件销毁时取消
  • ObservableCollection 复用而非重建
  • 批量更新集合,避免频繁触发 CollectionChanged
  • 字符串拼接使用 StringBuilder
  • XAML 绑定避免复杂转换器
  • 主题切换批量替换 ResourceDictionary

发布前性能测试:

  • PropertyGrid 50 属性生成 < 200ms
  • 主题切换 < 100ms
  • 应用启动 < 2s
  • 空闲时内存 < 100MB
  • 无明显的 UI 卡顿(帧率 > 50fps