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

415 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 线程:**
```csharp
// 错误:在属性 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;
}
}
```
**✅ 正确示例 - 异步加载:**
```csharp
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**
```csharp
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 读取**
```csharp
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使用编译表达式替代反射调用可选**
```csharp
// 反射调用(慢)
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**
**❌ 反面示例 - 逐个替换资源:**
```csharp
// 错误:逐个替换资源会触发多次 UI 刷新
foreach (var key in themeColors.Keys)
{
Application.Current.Resources[key] = themeColors[key];
}
```
**✅ 正确示例 - 批量替换 ResourceDictionary**
```csharp
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);
}
```
**预加载主题资源(可选优化):**
```csharp
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事件订阅未取消**
```csharp
// ❌ 可能导致内存泄漏
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 过度使用**
```csharp
// ❌ 每次都创建新集合
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 需要支持数百个属性,使用虚拟化:**
```xml
<ItemsControl Items="{Binding Properties}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<!-- VirtualizingStackPanel 仅渲染可见项 -->
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
```
**虚拟化性能对比:**
```
100 个属性(非虚拟化): ~300ms 生成 + 50MB 内存
100 个属性(虚拟化): ~100ms 生成 + 10MB 内存
```
---
### Avoid Common Pitfalls
**陷阱 1在循环中触发属性变化通知**
```csharp
// ❌ 每次循环触发 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频繁的字符串拼接**
```csharp
// ❌ 每次拼接创建新字符串
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 中的复杂绑定**
```xml
<!-- ❌ 每次渲染都执行转换 -->
<TextBlock Text="{Binding PropertyType, Converter={StaticResource ComplexConverter}}" />
<!-- ✅ 在 ViewModel 中预计算 -->
<TextBlock Text="{Binding PropertyTypeDisplayName}" />
```
---
### Performance Measurement
**使用 Stopwatch 测量关键操作:**
```csharp
#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 监控内存:**
```bash
# 安装工具
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
---