优化性能:对象行为

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

本主题包括下列各节。

  • 不移除对象的事件处理程序可能会使对象保持活动状态
  • 依赖项属性和对象
  • Freezable 对象
  • 使用接口虚拟化
  • 相关主题

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

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

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

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

依赖项属性和对象

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

DependencyProperty 优化

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

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

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

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

谨慎使用 RegisterClassHandler

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

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

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

使用 Register 设置 PropertyMetadata 值

创建 DependencyProperty 时,您可以选择是使用 Register 还是 OverrideMetadata 方法来设置 PropertyMetadata。 尽管您的对象可能有一个静态构造函数来调用 OverrideMetadata,但这不是最佳方案,它会影响性能。 为了获得最佳的性能,请在调用 Register 期间设置 PropertyMetadata

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 属性。

状态

大小

冻结 SolidColorBrush

212 字节

非冻结 SolidColorBrush

972 字节

下面的代码示例演示了此概念:

            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
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;
}

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

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

WPF 还在内部挂钩 Changed 事件。 例如,所有值为 Freezable 的依赖项属性将自动侦听 Changed 事件。 Fill 属性(带一个 Brush)说明了此概念。

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

将 myRectangle.Fill 赋值为 myBrush 时,指回 Rectangle 对象的委托将添加到 SolidColorBrush 对象的 Changed 事件。 这意味着以下代码实际不会使 myRect 可以进行垃圾回收:

            myRectangle = Nothing
myRectangle = null;

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

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

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

使用接口虚拟化

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

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

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

主机面板

呈现时间(毫秒)

StackPanel

3210

VirtualizingStackPanel

46

请参见

概念

优化 WPF 应用程序性能

规划应用程序性能

优化性能:利用硬件

优化性能:布局和设计

优化性能:二维图形和图像处理

优化性能:应用程序资源

优化性能:文本

优化性能:数据绑定

优化性能:其他建议