415 lines
10 KiB
Markdown
415 lines
10 KiB
Markdown
# 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();
|
||
}
|
||
}
|
||
```
|
||
|
||
**问题 2:ObservableCollection 过度使用**
|
||
|
||
```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();
|
||
```
|
||
|
||
**陷阱 3:XAML 中的复杂绑定**
|
||
|
||
```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)
|
||
|
||
---
|
||
|