优化性能:对象行为

了解 WPF 对象的固有行为将有助于在功能和性能之间做出正确的权衡。

不删除对象上的事件处理程序可能会使对象保持活动状态

对象传递给其事件的委托是对该对象的有效引用。 因此,事件处理程序可以使对象保持活动时间超过预期。 当对已注册为侦听对象事件的对象执行清理时,在释放对象前删除委托是非常必要的。 使不需要的对象保持活动状态会增加应用程序的内存使用率。 当对象是逻辑树或可视化树的根时,这尤其如此。

WPF 为事件引入了弱事件侦听器模式,这些模式在源和侦听器之间的对象生存期关系难以跟踪的情况下非常有用。 某些现有 WPF 事件使用此模式。 如果你要实现带有自定义事件的对象,此模式可能对你有用。 有关详细信息,请参阅 弱事件模式

有多种工具(如 CLR 探查器和工作集查看器)可提供指定进程的内存使用情况信息。 CLR 探查器包括分配配置文件的许多非常有用的视图,其中包括已分配类型的直方图、分配和调用关系图、显示各代垃圾回收及上述回收之后托管堆的生成状态的时间线,以及显示每个方法分配和程序集加载的调用树。 有关更多信息,请参阅性能

依赖属性和对象

通常,访问 DependencyObject 的依赖属性的速度并不慢于访问 CLR 属性。 虽然设置属性值的性能开销很小,但获取值的速度与从 CLR 属性获取值的速度一样快。 抵消较小的性能开销是依赖属性支持可靠的功能,例如数据绑定、动画、继承和样式设置。 有关详细信息,请参阅 依赖项属性概述

DependencyProperty 优化

应非常仔细地在应用程序中定义依赖项属性。 如果 DependencyProperty 仅影响呈现类型元数据选项,而不影响其他元数据选项(如 AffectsMeasure),则应通替代其元数据来对其进行同样的标记。 有关重写或获取属性元数据的详细信息,请参阅 依赖属性元数据

如果并非所有属性更改都会影响测量、排列和呈现,则通过属性更改处理程序手动使测量、排列和呈现阶段无效的做法可能会更高效。 例如,你可能决定仅在值大于设置限制时重新呈现背景。 在这种情况下,当值超出设置的限制时,你的属性更改处理程序只会使渲染无效。

将 DependencyProperty 设置为可继承会影响性能

默认情况下,已注册的依赖项属性不可继承。 但是,可以显式地使任何属性可继承。 虽然这是一项有用的功能,但将属性转换为可继承的会通过增加属性失效的时间来影响性能。

谨慎使用 RegisterClassHandler

虽然调用 RegisterClassHandler 允许保存实例状态,但请务必注意在每个实例上调用处理程序,这可能会导致性能问题。 仅当应用程序要求保存实例状态时使用 RegisterClassHandler

在注册过程中为 DependencyProperty 设置默认值

创建需要默认值的 DependencyProperty 时,使用作为参数传递给 RegisterDependencyProperty 方法的默认元数据设置该值。 使用此方法,而不是在构造函数或元素的每个实例上设置属性值。

使用 Register 方法设置 PropertyMetadata 的值

创建 DependencyProperty 时,可以选择使用 PropertyMetadataRegister 方法设置 OverrideMetadata。 尽管对象可以具有静态构造函数来调用 OverrideMetadata,但这不是最佳解决方案,并且会影响性能。 为了获得最佳性能,请将调用期间 PropertyMetadata 设置为 Register

可冻结对象

Freezable 是一种特殊类型的对象,具有两种状态:未冻结和冻结。 尽可能冻结对象可提高应用程序的性能,并降低其工作集。 有关详细信息,请参阅 冻结对象概述

每个 Freezable 都有一个 Changed 事件,只要发生更改就会引发该事件。 但是,更改通知在应用程序性能方面成本高昂。

请考虑以下示例,其中每个 Rectangle 使用相同的 Brush 对象:

rectangle_1.Fill = myBrush;
rectangle_2.Fill = myBrush;
rectangle_3.Fill = myBrush;
// ...
rectangle_10.Fill = myBrush;
rectangle_1.Fill = myBrush
rectangle_2.Fill = myBrush
rectangle_3.Fill = myBrush
' ...
rectangle_10.Fill = myBrush

默认情况下,WPF 为 SolidColorBrush 对象的 Changed 事件提供事件处理程序,以便使 Rectangle 对象的 Fill 属性失效。 在这种情况下,每次 SolidColorBrush 必须触发其 Changed 事件时,都需要为每个 Rectangle调用回调函数—这些回调函数调用的累积会产生显著的性能损失。 此外,此时添加和删除处理程序非常耗性能,因为应用程序必须遍历整个列表才能执行此操作。 如果应用程序方案永远不会更改 SolidColorBrush,则需要支付不必要的维护 Changed 事件处理程序的费用。

冻结 Freezable 可以提高其性能,因为它不再需要消耗资源来维护更改通知。 下表显示简单的 SolidColorBrush 在其 IsFrozen 属性设置为 true 时的大小,并列出该属性未设置为该值时的情况以供对比。 这假设将一个画笔应用于十个 Fill 对象的 Rectangle 属性。

大小
已冻结的 SolidColorBrush 212 字节
非冻结的 SolidColorBrush 972 字节

以下代码示例演示了此概念:

Brush frozenBrush = new SolidColorBrush(Colors.Blue);
frozenBrush.Freeze();
Brush nonFrozenBrush = new SolidColorBrush(Colors.Blue);

for (int i = 0; i < 10; i++)
{
    // Create a Rectangle using a non-frozed Brush.
    Rectangle rectangleNonFrozen = new Rectangle();
    rectangleNonFrozen.Fill = nonFrozenBrush;

    // Create a Rectangle using a frozed Brush.
    Rectangle rectangleFrozen = new Rectangle();
    rectangleFrozen.Fill = frozenBrush;
}
Dim frozenBrush As Brush = New SolidColorBrush(Colors.Blue)
frozenBrush.Freeze()
Dim nonFrozenBrush As Brush = New SolidColorBrush(Colors.Blue)

For i As Integer = 0 To 9
    ' Create a Rectangle using a non-frozed Brush.
    Dim rectangleNonFrozen As New Rectangle()
    rectangleNonFrozen.Fill = nonFrozenBrush

    ' Create a Rectangle using a frozed Brush.
    Dim rectangleFrozen As New Rectangle()
    rectangleFrozen.Fill = frozenBrush
Next i

解冻的可冻结对象的已更改处理程序可以使对象保持活动状态

对象传递给 Freezable 对象的 Changed 事件的委托是对该对象的有效引用。 因此,Changed 事件处理程序可以使对象保持活动时间超过预期。 当对已注册为侦听 Freezable 对象的 Changed 事件的对象执行清理时,在释放对象前删除委托是非常必要的。

WPF 还会在内部挂钩 Changed 事件。 例如,将 Freezable 作为值的所有依赖属性将自动侦听 Changed 事件。 采用 FillBrush 属性说明了此概念。

Brush myBrush = new SolidColorBrush(Colors.Red);
Rectangle myRectangle = new Rectangle();
myRectangle.Fill = myBrush;
Dim myBrush As Brush = New SolidColorBrush(Colors.Red)
Dim myRectangle As New Rectangle()
myRectangle.Fill = myBrush

myBrush 赋值给 myRectangle.Fill 时,指向回 Rectangle 对象的委托将添加到 SolidColorBrush 对象的 Changed 事件。 这意味着以下代码实际上无法使 myRect 符合垃圾回收的条件:

myRectangle = null;
myRectangle = Nothing

在这种情况下,myBrush 仍会使 myRectangle 保持活动状态,并在其引发 Changed 事件时对其进行调用。 请注意,将 myBrush 赋值给新 FillRectangle 属性时,仅将另一个事件处理程序添加到 myBrush

建议清理这些类型对象的方法是从 Brush 属性中删除 Fill,这样就会自动删除 Changed 事件处理程序。

myRectangle.Fill = null;
myRectangle = null;
myRectangle.Fill = Nothing
myRectangle = Nothing

用户界面虚拟化

WPF 还提供一种 StackPanel 元素的变体,可以自动“虚拟化”数据绑定的子内容。 在此上下文中,“虚拟化”指的是一种技术,依据屏幕上可见的数据项,从大量数据中生成对象的子集。 如果在指定时刻只有少量 UI 元素位于屏幕上,则此时生成大量 UI 元素需要占用大量内存和处理器。 VirtualizingStackPanel(通过 VirtualizingPanel提供的功能)计算可见项,并与 ItemContainerGenerator(如 ItemsControlListBox)中的 ListView 一起使用,以仅为可见项创建元素。

作为性能优化,仅当这些项在屏幕上可见时,其视觉对象才会被生成或保持活动状态。 当它们不再位于控件的可查看区域中时,可能会删除视觉对象。 这不会与数据虚拟化混淆,其中数据对象并非全部存在于本地集合中,而是根据需要进行流式传输。

下表显示了将 5000 个 TextBlock 元素添加到 StackPanelVirtualizingStackPanel 并使其呈现所需的运行时间。 在此方案中,度量值表示将文本字符串附加到 ItemsSource 对象的 ItemsControl 属性与面板元素显示文本字符串的时间之间的时间。

主机面板 呈现时间(ms)
StackPanel 3210
VirtualizingStackPanel 46

另请参阅