优化性能:对象行为

了解 WPF 对象的内部行为有助于在功能和性能之间做出适当的取舍。

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

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

WPF 为事件引入了弱事件侦听器模式,在很难跟踪源和侦听器之间的对象生存期关系时,这种模式特别有用。 某些现有 WPF 事件使用此模式。 如果要实现具有自定义事件的对象,此模式可能会有用。 有关详细信息,请参阅弱事件模式

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

依赖属性和对象

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

DependencyProperty 优化

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

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

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

默认情况下,注册的依赖属性是不可继承的。 但可以显式地将所有属性设置为可继承。 尽管这是一个有用的功能,但是将属性转换为可继承会影响性能,因为会增加属性无效的时长。

谨慎使用 RegisterClassHandler

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

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

创建要求默认值的 DependencyProperty 时,请将传递的默认元数据用作 DependencyPropertyRegister 方法的参数来设置此值。 请使用此技术,而不要在构造函数中或在元素的每个实例上设置属性值。

使用 Register 设置 PropertyMetadata 值

创建 DependencyProperty 时,可以选择使用 RegisterOverrideMetadata 方法设置 PropertyMetadata。 尽管对象可能有一个静态构造函数来调用 OverrideMetadata,但这不是最佳方案,并且会影响性能。 为了获得最佳性能,建议在调用过程中将 PropertyMetadata 设置为 Register

Freezable 对象

Freezable 是一种特殊类型的对象,具有两种状态:解冻和冻结。 尽可能冻结对象会改进应用程序性能,并缩小其工作集。 有关详细信息,请参阅 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 时的大小,并列出该属性未设置为该值时的情况以供对比。 这假设将一个画笔应用于十个 Rectangle 对象的 Fill 属性。

State 大小
已冻结的 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 事件。 采用 BrushFill 属性说明了此概念。

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 赋值给新 RectangleFill 属性时,仅将另一个事件处理程序添加到 myBrush

清理这些类型的对象的建议方式是从 Fill 属性删除 Brush,这将进而删除 Changed 事件处理程序。

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

用户界面虚拟化

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

作为性能优化的结果,将仅生成这些项的视觉对象,或如果它们在屏幕上是可见的,则保持活动状态。 这些视觉对象不再位于控件的可视区域时,则可能已被删除。 请勿将此与数据虚拟化发生混淆,数据虚拟化中的数据对象不会全部出现在本地集合中 - 而是根据需要进行流式处理。

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

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

另请参阅