WPF进阶:万字详解WPF如何性能优化

文章目录

WPF性能优化

性能优化是任何应用程序开发过程中不可或缺的一环,对于Windows Presentation Foundation(WPF)应用而言,优化不仅关乎用户体验,更是确保应用在各种环境下都能够流畅运行的关键。本文将通过比较和对比的形式,探讨在WPF开发中提高应用响应速度的几种方法,并通过具体的示例代码展示如何实施这些优化策略。

WPF(Windows Presentation Foundation)应用程序在没有图形加速设备的机器上运行速度很慢是个公开的秘密,给用户的感觉是它太吃资源了,WPF程序的性能和硬件确实有很大的关系,越高档的机器性能越有优势。 程序性能改善不是一蹴而就的,好的设计可以消除影响性能的问题,例如,在运行时构造对象就会对程序的性能造成影响。虽然WPF通过增强的导航等功能提供了更丰富的用户界面,但你应该考虑你的用户是否的确需要富图形界面,尽管WPF有这样那样的问题,但在UI设计,特别是自定义风格和控件模板方面,的确给开发人员提供了不少灵活性。

WPF架构,蓝色是Windows组件,褐色是WPF组件

渲染WPF程序的主要因素是它包含的像素量,WPF使用微软的DirectX在程序运行的硬件上进行渲染,因此,如果你的机器有独立显卡,运行WPF程序会更流畅。

1.性能分析工具

1.1.WPF性能分析工具

WPF提供了一套性能分析工具,使您可以分析应用程序的运行时行为并确定可以应用的性能优化类型。下表列出了Windows SDK工具WPF Performance Suite中包含的性能分析工具:

工具 描述
Perforator 用于分析渲染行为。
Visual Profiler 用于通过可视树中的元素来概要分析WPF服务的使用,例如布局和事件处理。

WPF Performance Suite提供了性能数据的丰富图形视图。有关WPF性能工具的更多信息,请参见WPF Performance Suite

1.2.DirectX诊断工具

DirectX诊断工具Dxdiag.exe旨在帮助您解决与DirectX相关的问题。DirectX诊断工具的默认安装文件夹是:

复制代码
~\Windows\System32

运行DirectX诊断工具时,主窗口包含一组选项卡,可用于显示和诊断与DirectX相关的信息。例如,"**系统"**选项卡提供有关您的计算机的系统信息,并指定计算机上安装的DirectX的版本。

DirectX诊断工具主窗口

1.3.Visual Studio性能分析工具

使用性能分析器:Visual Studio提供了性能分析工具,可以帮助你检测应用程序的性能瓶颈。性能分析器能够捕获应用程序的CPU使用情况、内存使用情况等。

查看CPU使用情况:分析CPU使用情况,检查是否存在高CPU使用的情况,并优化相应的代码。

1.4.内存监测软件

Ants Memory Profiler

下载地址: https://pan.baidu.com/s/1nLF6njntaVgrXVdIaT1mOw 提取码: phsy

使用方法:https://www.cnblogs.com/jingridong/p/6385661.html

dotMemory

https://www.jetbrains.com/dotmemory/

snoop

官网:https://chocolatey.org/packages/snoop

csharp 复制代码
// 使用管理员权限打开 Powershell
Set-ExecutionPolicy RemoteSigned
iwr https://chocolatey.org/install.ps1 -UseBasicParsing | iex
// 安装snoop
choco install snoop --pre
// 升级
choco upgrade snoop --pre
// 卸载
choco uninstall snoop --pre

2.内存优化

WPF内存优化,防止内存泄漏

2.1.WPF的Window的生命周期

WPF的Window的生命周期与WPF程序的运行过程有很大的关系,需要开发者仔细的管理和控制。

WPF的Window的生命周期经历以下几个阶段:

  1. 构造函数(Constructor):创建窗口的实例对象。在此阶段可以设置窗口的属性和注册事件。
  2. 加载事件(Loaded Event):窗口对象被添加到界面树中,但是还没有完全渲染。在此阶段可以执行一些初始化的操作。
  3. 可见性事件(IsVisibleChanged Event):窗口被设置为可见状态,这时候才可以看到界面上的内容。在此阶段可以进行一些页面加载的操作。
  4. 激活事件(Activated Event):窗口被激活,即成为用户当前的操作窗口,此时窗口获取焦点。
  5. 关闭事件(Closing Event):窗口关闭前的事件,可以在此阶段进行一些保存数据的操作及按钮等注册的事件。
  6. 已关闭事件(Closed Event):窗口已关闭,如果没有绑定事件,窗口将被销毁。
  7. 从界面树移除事件(Unloaded Event):窗口从界面树中移除,但仍然存在于内存中。在此阶段可以进行一些清理和释放资源的操作。

2.2.内存优化

本文内存优化主要涉及以下几个方面

  1. 防止内存泄漏
  2. 增大可用内存
  3. 节省内存
  4. 定时GC
  5. 使用虚拟内存

这几点中首先要保证不要出现内存泄漏,开发过程中尽量注意节省内存。

针对虚拟内存

使用虚拟内存这个方式并不建议,因为其实它只是减少了内存条的内存占用而使用了硬盘虚拟的内存,在任务管理器中内存确实占用少了,但是其实上并没少,只要超过了程序的最大使用内存依旧会崩溃,表现就是任务管理器中明明显示的内存占用不高,但是程序却报内存不足的错误,这是自欺欺人的办法,也不利于排查问题,但是如果程序本身部分内存占用长时间不用,并且程序本身没有内存泄漏,倒是可以用这种方法减少物理内存的使用。

防止内存泄漏

内存泄露原因

内存泄露主要原因:

  • 托管资源仍保持引用(静态引用、未注销的事件绑定)
  • 非托管代码资源未Dispose

对于静态对象尽量少或者不用,非托管资源可通过手动Dispose来释放。

Beware of Memory Leaks(StackOverflow)

Memory leaks are the number one cause of performance problems in most WPF applications. They are easy to have but can be difficult to find. For example, using the DependencyPropertyDescriptor.AddValueChanged can cause the WPF framework to take a strong reference to the source of the event that isn't removed until you manually call DependencyPropertyDescriptor.RemoveValueChanged. If your views or behaviors rely on events being raised from an object or ViewModel (such as INotifyPropertyChanged), subscribe to them weakly or make sure you are manually unsubscribing. Also, if you are binding to properties in a ViewModel which does not implement INotifyPropertyChanged, chances are you have a memory leak.

图片处理
加载图片

如果在针对图片很大的情况下,或者频繁的调用体积很大的图片,直接引用地址,很可能就会造成内存溢出的问题

csharp 复制代码
 Uri uri = new Uri(ImageSavePath, UriKind.Absolute);
 BitmapImage bimg = new BitmapImage(uri);
 myimage.Source = bitmap;

使用Image控件显示图片后,虽然自己释放了图片资源,Image.Source = null 了一下,但是图片实际没有释放。

修改加载方式

csharp 复制代码
public static BitmapImage GetImage(string imagePath)
{
  BitmapImage bi = new BitmapImage();
  if (File.Exists(imagePath))
  {
    bi.BeginInit();
    bi.CacheOption = BitmapCacheOption.OnLoad;
    using (Stream ms = new MemoryStream(File.ReadAllBytes(imagePath)))
    {
      bi.StreamSource = ms;
      bi.EndInit();
      bi.Freeze();
    }
  }
  return bi;
}
// 使用时直接通过调用此方法获得Image后立马释放掉资源
myimage.Source = GetImage(path); //path为图片的路径
// 释放
myimage.Source = null;

注意

如果 StreamSource 和 UriSource 均设置,则忽略 StreamSource 值。
要在创建 BitmapImage 后关闭流,请将 CacheOption 属性设置为 BitmapCacheOption.OnLoad。
默认 OnDemand 缓存选项保留对流的访问,直至需要位图并且垃圾回收器执行清理为止。

列表中加载图片

页面中直接绑定图片地址的时候,会出现这么个情况,页面数据已经清空了,但是引用的图片依旧被占用,导致图片无法删除。

csharp 复制代码
using System;
using System.Globalization;
using System.IO;
using System.Windows.Data;
using System.Windows.Media.Imaging;
 
namespace SchoolClient.Converters
{
  public class StringToImageSourceConverter : IValueConverter
  {
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
      string path = (string)value;
      if (!string.IsNullOrEmpty(path))
      {
        return GetImage(path);
      }
      else
      {
        return null;
      }
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
      return null;
    }

    private BitmapImage GetImage(string imagePath)
    {
      BitmapImage bi = new BitmapImage();
      if (File.Exists(imagePath))
      {
        bi.BeginInit();
        bi.CacheOption = BitmapCacheOption.OnLoad;
        using (Stream ms = new MemoryStream(File.ReadAllBytes(imagePath)))
        {
          bi.StreamSource = ms;
          bi.EndInit();
          bi.Freeze();
        }
      }
      return bi;
    }
  }
}

页面中

xml 复制代码
<Window xmlns:cv="clr-namespace:SchoolClient.Converters">
  <Window.Resources>
    <cv:StringToImageSourceConverter x:Key="StringToImageSourceConverter" />
  </Window.Resources>
  <Image
         Source="{Binding Path=filepath, Converter={StaticResource StringToImageSourceConverter}}"
         Stretch="Uniform" />
</Window>
局部变量代替成员变量

代码中能用局部变量的就不要用对象的成员变量。

  • 局部变量 (Local Variable) :分配在栈 (Stack) 上,或者直接映射到 CPU 寄存器 (Registers)。CPU 访问寄存器的速度比访问内存快数百倍。
  • 成员变量 (Member Variable / Field) :分配在堆 (Heap) 上。访问它需要通过 this 指针进行一次解引用(寻址),涉及内存访问。
csharp 复制代码
// 反面模式:滥用成员变量
// _sum 只是为了辅助计算,却被定义成了成员变量。
public class Calculator
{
    private int _sum; // 成员变量 (Field)

    public void ProcessData(int[] numbers)
    {
        _sum = 0;
        foreach (var n in numbers)
        {
            _sum += n; // 频繁读写堆内存
        }
        Console.WriteLine(_sum);
    }
}
// 推荐模式:使用局部变量
public class Calculator
{
    // 类中没有不必要的状态维护

    public void ProcessData(int[] numbers)
    {
        int localSum = 0; // 局部变量 (Local Variable)
        foreach (var n in numbers)
        {
            localSum += n; // 极速读写栈/寄存器
        }
        Console.WriteLine(localSum);
    }
}
窗口弱引用

在 WPF 中,使用 WeakReference 可以实现弱引用窗口,避免窗口被强引用而导致内存泄漏。

csharp 复制代码
public class MainWindowViewModel
{
    public static readonly WeakReference<MainWindow> mainWindow;

    public MainWindowViewModel(MainWindow mainWindow)
    {
        this.mainWindow = new WeakReference<MainWindow>(mainWindow);
    }

    public void ShowMessage()
    {
        if (mainWindow.TryGetTarget(out MainWindow target))
        {
            target.ShowMessage("Hello, World!");
        }
    }
}

public partial class MainWindow : Window
{
    private MainWindowViewModel viewModel;

    public MainWindow()
    {
        InitializeComponent();
        viewModel = new MainWindowViewModel(this);
    }

    public void ShowMessage(string message)
    {
        MessageBox.Show(message);
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        viewModel.ShowMessage();
    }
}

MainWindowViewModel 中,我们创建了 WeakReference<MainWindow> 类型的字段 mainWindow,它指向主窗口。

ShowMessage 方法中,我们使用 TryGetTarget 方法判断主窗口是否还存在,如果存在,则调用它的 ShowMessage 方法。

MainWindow 中,我们创建了一个 MainWindowViewModel 类型的字段 viewModel,并在构造函数中传入了 this,即主窗口的实例。

当用户点击窗口中的按钮时,我们调用 viewModel.ShowMessage() 方法,间接调用了主窗口的 ShowMessage 方法。这样即可实现弱引用窗口,避免内存泄漏。

DataContext

当我们使用MVVM模式绑定DataContext或是直接给列表控件绑定数据源的情况下,关闭窗体时,最好将绑定属性赋一个空值

csharp 复制代码
protected override void OnClosed(EventArgs e)
{
    base.OnClosed(e);
    this.DataContext = null;
}

将DataContext设置为null实际上是清除控件与数据模型的绑定关系,这有以下两个好处:

  1. 释放资源:当不再需要控件与数据模型的绑定关系时,将DataContext设置为null可以释放对应的资源,避免对内存的浪费。
  2. 取消绑定:有时候需要在运行时取消控件与数据模型的绑定关系,例如当窗口或页面关闭时,需要取消与该窗口或页面相关的所有绑定关系,此时将DataContext设置为null可以轻松实现此功能。
类与类之间尽量不要互相引用

类与类之间尽量不要互相引用,如果相互引用了要手动设置里面的引用为空,不然 会导致内存泄漏

csharp 复制代码
Class1 class1 =new Class1();
Class2 class2 = new Class2();
class1.Class2 = class2;
class2.Class1 = class1;

清除引用:

csharp 复制代码
class2.Class1 = null;
class2 = null;
class1.Class2 = null;
class1 =null;
静态变量

页面关闭时静态变量要设置为空

参考垃圾收集器的工作原理。 基本思想是GC遍历所有GC Root对象并将其标记为"不可收集"。 然后,GC转到它们引用的所有对象,并将它们也标记为"不可收集"。 最后,GC收集剩下的所有内容。

那么什么会被认为是一个GC Root?

  • 正在运行的线程的实时堆栈。
  • 静态变量。
  • 通过interop传递到COM对象的托管对象(内存回收将通过引用计数来完成)。

GC Root的特征

它只会引⽤其他对象,⽽不会被其他对象引⽤,例如:栈中的本地变量、⽅法区中的静态变量、本地⽅法栈中的变量、正在运⾏的线程等可以作为gc root。

这意味着静态变量及其引用的所有内容都不会被垃圾回收。

csharp 复制代码
public class MyClass
{
    static List<MyClass> _instances = new List<MyClass>();
    public MyClass()
    {
        _instances.Add(this);
    }
}

任何MyClass的实例将永远留在内存中,从而导致内存泄漏。

解绑事件

WPF中xaml中绑定的事件会不会在关闭时自动释放

WPF中事件订阅会在控件或对象被创建时创建,直到明确地取消订阅或者控件或对象被 GC 回收时才会被释放。如果不注意在控件或对象被销毁时取消订阅事件,就有可能导致内存泄漏。

因此,在使用 WPF 中 XAML 中绑定的事件时,应该注意及时取消订阅事件。在控件或对象被销毁之前,需要确保取消订阅事件,以避免内存泄漏的问题。常见的取消订阅事件的方法包括使用 -= 操作符或者手动取消绑定事件处理程序。

在WPF中,页面关闭时可以使用以下两种方式来自动解绑页面绑定的所有事件:

A.手动解绑事件

手动解绑事件: 在页面的Closing事件中手动解绑所有已经绑定的事件。例如,如果你在页面上有一个名为MyButton的按钮,并且你绑定了一个Click事件到这个按钮上,你可以在页面的Closing事件中添加以下代码来解绑这个事件:

csharp 复制代码
private void MyWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    MyButton.Click -= MyButtonClickEventHandler;
}

在上述代码中,MyButtonClickEventHandler是你之前绑定到MyButton.Click事件上的处理方法。

B.使用弱事件

使用弱事件: 使用弱事件机制可以让你不必手动解绑事件,因为在页面关闭时,弱事件机制会自动将所有已经绑定的事件解绑。

弱事件机制是一种在WPF中常用的事件处理方式,它能够避免由于事件未能正确解绑而导致的内存泄漏。

WeakEventManager

xaml

xml 复制代码
<Button Name="MyBtn" Content="点击" />

cs

csharp 复制代码
public partial class TestWin : Window
{
    public TestWin()
    {
        InitializeComponent();

        WeakEventManager<Button, RoutedEventArgs>.AddHandler(MyBtn, "Click", Button_Click);
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        Console.WriteLine(@"Button_Click");
    }
}
自定义类

在WPF中,weak通常用于实现弱事件(weak event)。弱事件实际上是一种事件机制,用于解决在事件处理器未释放时导致的内存泄漏问题。

WPF中的弱事件可以通过使用WeakEventManager来实现。该类对于事件发行者和事件接收者都是弱引用,因此即使事件接收者没有显式地从事件发行者取消注册,也不会导致内存泄漏问题。

下面是一个简单的WPF弱事件示例,假设有一个MyButton类,该类在按钮的单击事件被触发时会引发一个WeakEvent。代码如下:

csharp 复制代码
public class MyButton : Button
{
    public static readonly RoutedEvent ClickEvent = EventManager.RegisterRoutedEvent(
        "Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MyButton));

    private static readonly WeakEventManager _clickEventManager = new WeakEventManager();

    public event RoutedEventHandler Click
    {
        add { _clickEventManager.AddEventHandler(this, value); }
        remove { _clickEventManager.RemoveEventHandler(this, value); }
    }

    protected override void OnClick()
    {
        base.OnClick();

        _clickEventManager.HandleEvent(this, EventArgs.Empty, ClickEvent);
    }
}

可以看到,MyButton类定义了ClickEvent事件,并在该事件被触发时调用了clickEventManager对象的HandleEvent()方法*。这里的* clickEventManager实际上就是我们前面提到的WeakEventManager类的实例。

在Click事件的add和remove访问器中,我们调用了_clickEventManagerAddEventHandler()RemoveEventHandler()方法来注册和取消注册事件处理器。这里的事件处理器是以弱引用的形式存储在_clickEventManager中,因此即使事件处理器没有被显式地取消注册,也不会导致内存泄漏问题。

使用该类创建按钮时,可以像下面这样注册和取消注册事件处理器:

csharp 复制代码
var button = new MyButton();
button.Click += new RoutedEventHandler(Button_Click);
button.Click -= new RoutedEventHandler(Button_Click);

当然,也可以使用lambda表达式来注册事件处理器:

csharp 复制代码
var button = new MyButton();
button.Click += (s, e) =>
{
    // 处理Click事件
};

总之,WPF中的weak通常用于实现弱事件机制,以解决内存泄漏问题。

匿名方法中不要捕获类成员

虽然可以很明显地看出事件机制需要引用一个对象,但是引用对象这个事情在匿名方法中捕获类成员时却不明显了。

csharp 复制代码
public partial class UserControl1 : UserControl
{
    public UserControl1()
    {
        InitializeComponent();

        Application.Current.MainWindow.SizeChanged += (s, e) =>
        {
            Debug.WriteLine($"{e.NewSize.Width - this.Width}");

        };

    }
}

这里,由于UserControl1的成员Width被匿名函数捕获,结果导致整个UserControl1的实例也被MainWindow所引用,从而产生内存泄漏。

这类泄漏的解决办法可能很简单------使用局部变量代替对象的成员

csharp 复制代码
public partial class UserControl1 : UserControl
{
    public UserControl1()
    {
        InitializeComponent();
        var w = this.Width;
        Application.Current.MainWindow.SizeChanged += (s, e) =>
        {
            Debug.WriteLine($"{e.NewSize.Width - w}");
        };
    }
}

通过将值分配给局部变量,不会有任何内容被捕获,并且避免了潜在的内存泄漏。

WPF绑定优化

WPF绑定实际上可能会导致内存泄漏。

经验法则是始终绑定到DependencyObject或INotifyPropertyChanged对象。

如果你不这样做,WPF将创建从静态变量到绑定源(即ViewModel)的强引用,从而导致内存泄漏。

INotifyPropertyChanged

xml 复制代码
 <UserControl x:Class="WpfApp.MyControl"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
     <TextBlock Text="{Binding SomeText}"/>
 </UserControl>

这个ViewModel将永远留在内存中:

csharp 复制代码
public class MyViewModel
{
    public string _someText = "内存泄漏";
    public string SomeText
    {
        get { return _someText; }
        set
        {
            _someText = value;
        }
    }
}

而这个ViewModel不会导致内存泄漏:

csharp 复制代码
public class MyViewModel : INotifyPropertyChanged
{
    public string _someText = "无内存泄漏";

    public string SomeText
    {
        get { return _someText; }
        set
        {
            _someText = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SomeText)));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

是否调用PropertyChanged实际上并不重要,重要的是该类是从INotifyPropertyChanged派生的,会告诉WPF不要创建强引用。

另一个和WPF有关的内存泄漏问题会发生在绑定到集合时。 如果该集合未实现INotifyCollectionChanged接口,则会发生内存泄漏。你可以通过使用实现该接口的ObservableCollection来避免此问题。

永不终止的线程

我们已经讨论过了GC的工作方式以及GC root。 我提到过实时堆栈会被视为GC root。 实时堆栈包括正在运行的线程中的所有局部变量和调用堆栈的成员。

如果出于某种原因,你要创建一个永远运行的不执行任何操作并且具有对对象引用的线程,那么这将会导致内存泄漏。

这种情况很容易发生的一个例子是使用Timer。考虑以下代码:

csharp 复制代码
public class MyClass
{
    public MyClass()
    {
        Timer timer = new Timer(HandleTick);
        timer.Change(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
    }

    private void HandleTick(object state)
    {
        // do something
    }
}

如果你并没有真正的停止这个timer,那么它会在一个单独的线程中运行,并且由于引用了一个MyClass的实例,因此会阻止该实例被收集。

非托管资源

到目前为止,我们仅仅谈论了托管内存,也就是由垃圾收集器管理的内存。

非托管内存是完全不同的问题,你将需要显式地回收内存,而不仅仅是避免不必要的引用。

调用Dispose
csharp 复制代码
public class SomeClass
{
    private IntPtr _buffer;

    public SomeClass()
    {
        _buffer = Marshal.AllocHGlobal(1000);
    }

    // do stuff without freeing the buffer memory
}

在上述方法中,通过Marshal.AllocHGlobal方法,它分配了非托管内存缓冲区。

在这背后,AllocHGlobal会调用Kernel32.dll中的LocalAlloc函数。

如果没有使用Marshal.FreeHGlobal显式地释放句柄,则该缓冲区内存将被视为占用了进程的内存堆,从而导致内存泄漏。

要解决此类问题,你可以添加一个Dispose方法,以释放所有非托管资源,如下所示:

csharp 复制代码
public class SomeClass : IDisposable
{
    private IntPtr _buffer;

    public SomeClass()
    {
        _buffer = Marshal.AllocHGlobal(1000);
        // do stuff without freeing the buffer memory
    }

    public void Dispose()
    {
        Marshal.FreeHGlobal(_buffer);
    }
}

由于内存碎片问题,非托管内存泄漏比托管内存泄漏更严重。

垃圾回收器可以移动托管内存,从而为其他对象腾出空间。但是,非托管内存将永远卡在它的位置。

谁申请谁释放,基本上这点能保证的话,内存基本上就能释放干净了。

在示例中,通过Dispose方法以释放所有非托管资源。

也可以在C#中使用using语句:

csharp 复制代码
 using (var instance = new MyClass())
 {
     // ...
 }

这适用于实现了IDisposable接口的类,并且编译器会将其转化为下面的形式:

csharp 复制代码
 MyClass instance = new MyClass();
 try
 {
     // ...
 }
 finally
 {
     if (instance != null)
         ((IDisposable)instance).Dispose();
 }

即使抛出异常,也会调用Dispose。

使用析构函数

官方示例:https://docs.microsoft.com/zh-cn/dotnet/api/system.idisposable.dispose

析构函数 (destructor) 与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数

析构函数用于在对象实例被销毁或销毁前被调用以释放资源和执行清理操作

在C#中,析构函数是一个没有参数和返回值的方法 ,其名称与类名相同,并以"~"开头。

csharp 复制代码
 public class MyClass
 {
     ~MyClass()
     {
        // 做一些清理工作
     }
 }

当对象的实例被销毁时(例如在Scope结束时),析构函数将被调用。

需要注意的是:

C#提供了垃圾回收机制,因此通常不需要显式地释放内存。然而,在处理非托管资源时可能需要使用析构函数。同时,也可以考虑实现IDisposable接口,使用using语句自动调用对象销毁前的清理操作。

下面的示例演示了这种情况:

csharp 复制代码
 public class MyClass : IDisposable
 {
     private IntPtr _bufferPtr;
     public int BUFFER_SIZE = 1024 * 1024; // 1MB
     private bool _disposed = false;
 
     public MyClass()
     {
         _bufferPtr =  Marshal.AllocHGlobal(BUFFER_SIZE);
     }

     protected virtual void Dispose(bool disposing)
     {
         if (_disposed)
             return;

         if (disposing)
         {
             // Free any other managed objects here.
         }

         // Free any unmanaged objects here.
         Marshal.FreeHGlobal(_bufferPtr);
         _disposed = true;
     }

     public void Dispose()
     {
         Dispose(true);
         //加上这句后,则如果已经被disposed,则在回收时不需要调用析构器(调用析构器对性能有一定影响)
         GC.SuppressFinalize(this);
     }

     ~MyClass()
     {
         Dispose(false);
     }
 
 }

这种模式可确保即使没有调用Dispose,Dispose也将在实例被垃圾回收时被调用。

另一方面,如果调用了Dispose,则finalizer将被抑制(SuppressFinalize)。 抑制finalizer很重要,因为finalizer开销很大并且会导致性能问题。

然而,这不是万无一失的。 如果从未调用Dispose并且由于托管内存泄漏而导致你的类没有被垃圾回收,那么非托管资源也将不会被释放。

易混淆

注意

网上有一些防止内存泄漏的观点,但是这些观点是不对的。

❌静态方法使用out

静态方法返回诸如List<>等变量的,请使用out

csharp 复制代码
public static List<String> myMothod(){}
// 改成
public static void myMothod(out List<String> result){}

为什么说静态方法不会造成内存泄漏呢?

要想造成内存泄漏,你的工具类对象本身要持有指向传入对象的引用才行!但是当你的业务方法调用工具类的静态方法时,会生产一个称为方法栈帧的东西(每次方法调用,都会生成一个方法栈帧),当方法调用结束返回的时候,当前方法栈帧就已经被弹出了并且被释放掉了。 整个过程结束时,工具类对象本身并不会持有传入对象的引用。

❌慎用隐式类型var的弱引用

这个本来应该感觉没什么问题的,可在实践中,发现大量采用var与使用类型声明的弱引用对比,总是产生一些不能正确回收的WeakRefrense(这点有待探讨,因为开销不是很大,可能存在一些手工编程的问题)

因为推断类型在编译后会变成实际的类型,所以跟直接写具体的类型没有什么差别,它只会稍微影响编译速度。

2.3.增大可用内存

这种方式提高了内存的使用上限,作用比较明显,但是不是合理的做法,这样只是延迟了内存不足而导致的奔溃。

为什么 32 位程序只能使用最大 2GB 内存?

32 位寻址空间只有 4GB 大小,于是 32 位应用程序进程最大只能用到 4GB 的内存。然而,除了应用程序本身要用内存,操作系统内核也需要使用。应用程序使用的内存空间分为用户空间和内核空间,每个 32 位程序的用户空间可独享前 2GB 空间(指针值为正数),而内核空间为所有进程共享 2GB 空间(指针值为负数)。所以,32 位应用程序实际能够访问的内存地址空间最多只有 2GB。

那么怎样让程序使用更多的内存呢?

编辑一个程序使之声明支持大于 2GB 内存的命令是:

复制代码
 editbin /largeaddressaware xhschool.exe

其中,xhschool.exe 是我们准备修改的程序,可以使用相对路径或绝对路径(如果路径中出现空格记得带引号)。

验证这个程序是否改好了的命令是:

复制代码
 dumpbin /headers xhschool.exe | more

注意到 FILE HEADER VALUES 块的倒数第二行多出了 Application can handle large (>2GB) addresses,就说明成功了。

找到安装路径,在VS的安装路径下搜索editbin.exe添加到环境变量中,重启VS

那么怎么让程序生成时自动进行上面的操作呢?

复制代码
 项目右键属性`=>`生成事件`=>`生成后事件命令行

添加如下命令

复制代码
 editbin.exe /largeaddressaware xhschool.exe

2.4.节省内存

DependencyObject(依赖属性)

通过依赖属性和普通的CLR属性相比为什么会节约内存?

其实依赖属性的声明,在这里或者用注册来形容更贴切,只是一个入口点。也就是我们平常常说的单例模式。

属性的值其实都放在依赖对象的一个哈希表里面。

所以依赖属性正在节约内存就在于这儿的依赖属性是一个static readonly属性。

所以不需要在对象每次实例化的时候都分配相关属性的内存空间,而是提供一个入口点。

csharp 复制代码
public class Student
{
    public string Name { get; set; }
    public double Height { get; set; }
}

替换为

csharp 复制代码
 public class Student : DependencyObject
 {
     public string Name
     {
         get
         {
             return (string)GetValue(NameProperty);
         }
         set
         {
             SetValue(NameProperty, value);
         }
     }
     public double Height
     {
         get
         {
             return (double)GetValue(HeightProperty);
         }
         set
         {
             SetValue(HeightProperty, value);
         }
     }
     public static readonly DependencyProperty NameProperty = DependencyProperty.Register(
         "Name",
         typeof(string),
         typeof(Student),
         new PropertyMetadata("")
     );
     public static readonly DependencyProperty HeightProperty = DependencyProperty.Register(
         "Height",
         typeof(double),
         typeof(Student),
         new PropertyMetadata((double)0.0)
     );
 }
字符串拼接
csharp 复制代码
// 不推荐
string ConcatString(params string[] items)
{
    string result = "";
    foreach (string item in items)
    {
        result += item;
    }
    return result;
}

// 推荐
string ConcatString2(params string[] items)
{
    StringBuilder result = new StringBuilder();
    for(int i=0, count = items.Count(); i<count; i++)
    {
        result.Append(items[i]);
    }
    return result.ToString();
}

建议在需要对string进行多次更改时(循环赋值、连接之类的),使用StringBuilder

项目中频繁且大量改动string的操作全部换成StringBuilder,用ANTS Memory Profiler分析效果显著,不仅提升了性能,而且垃圾也少了。

WPF共享样式模板

共享的方式最简单不过的就是建立一个类库项目,把样式、图片、笔刷什么的,都扔进去,样式引用最好使用StaticResource,开销最小,但这样就导致了一些编程时的麻烦,即未定义样式,就不能引用样式,哪怕定义在后,引用在前都不行。

注意

在自定义控件,尽量不要在控件的ResourceDictionary定义资源,而应该放在Window或者Application级。

  1. 因为放在控件中会使每个实例都保留一份资源的拷贝。
  2. 尽量使用Static Resources。
不同组件内存占用
  1. 布局时候能用Canvas尽量用Canvas。Gird,StackPanel内存开销相对Canvas大。
  2. 自定义控件尽量不要在控件ResourceDictionary定义资源,应该放在Window或者Application级。

把Label(标签)元素的ContentProperty和一个字符串(String)绑定的效率要比把字符串和TextBlock的Text属性绑定 的效率低。

Label在更新字符串是会丢弃原来的字符串,全部重新显示内容。

  1. 如果字符串不需要更新,用Label就无所谓性能问题。
少用透明窗口

WPF设置窗口透明只需要设置

csharp 复制代码
AllowTransparency="True"
WindowStyle="None"
Background="Transparent"

有个文章介绍了:https://gandalfliang.github.io/2018/02/16/transparent_4k_window.translate/

GeometryDrawing实现简单图片

较简单或可循环平铺的图片用GeometryDrawing实现

一个图片跟几行代码相比,哪个开销更少肯定不用多说了,而且这几行代码还可以BaseOn进行重用。

xml 复制代码
<DrawingGroup x:Key="Diagonal_50px">
    <DrawingGroup.Children>
        <GeometryDrawing Brush="#FF2A2A2A" Geometry="F1 M 0,0L 50,0L 50,50L 0,50 Z"/>
        <GeometryDrawing Brush="#FF262626" Geometry="F1 M 50,0L 0,50L 0,25L 25,0L 50,0 Z"/>
        <GeometryDrawing Brush="#FF262626" Geometry="F1 M 50,25L 50,50L 25,50L 50,25 Z"/>
    </DrawingGroup.Children>
</DrawingGroup>

这边是重用

csharp 复制代码
<DrawingBrush
              x:Key="FrameListMenuArea_Brush"
              Stretch="Fill"
              TileMode="Tile"
              Viewport="0,0,50,50"
              ViewportUnits="Absolute"
              Drawing="{StaticResource Diagonal_50px}"/>

2.5.定时GC

csharp 复制代码
 private static bool _appIsRun = true;
 
 /// <summary>
 /// 内存释放.
 /// </summary>
 /// <param name="sleepSpan">
 /// 周期
 /// </param>
 public static void CrackerOnlyGC(int sleepSpan = 30)
 {
     void ThreadStart(object s)
     {
         while (_appIsRun)
             try
             {
                 GC.Collect();
                 GC.WaitForPendingFinalizers();
                 Thread.Sleep(TimeSpan.FromSeconds(sleepSpan));
             }
             catch (Exception)
             {
                 // ignored
             }
     }
     new Thread(ThreadStart) { IsBackground = true }.Start();
 }
 
 protected override void OnExit(ExitEventArgs e)
 {
     // 在应用程序退出之前执行必要的操作
     _appIsRun = false;
     Console.WriteLine(@"----------------------------------------");
     Console.WriteLine(@"应用退出");
     base.OnExit(e);
     Environment.Exit(0);
 }

2.6.使用虚拟内存(不建议)

本质上以下的两种方式都是把内存的占用放在了虚拟内存中,所以内存占用并没有少,只是在任务管理器中少了,并且SetProcessWorkingSetSize方法缓存到硬盘上的数据,很快又会被读出来,还增加了程序的开销,不建议使用。

csharp 复制代码
 using System;
 using System.Runtime.InteropServices;
 using System.Threading;
 
 namespace MyUtils
 {
     internal class MemUtils
     {
         [DllImport("kernel32.dll")]
         private static extern bool SetProcessWorkingSetSize(IntPtr proc, int min, int max);
 
         /// <summary>
         /// 释放占用内存并重新分配,将暂时不需要的内容放进虚拟内存
         /// 当应用程序重新激活时,会将虚拟内存的内容重新加载到内存。
         /// 不宜过度频繁的调用该方法,频繁调用会降低使使用性能。
         /// 可在Close、Hide、最小化页面时调用此方法,
         /// </summary>
         public static void FlushMemory()
         {
             GC.Collect();
             // GC还提供了WaitForPendingFinalizers方法。
             GC.WaitForPendingFinalizers();
             if (Environment.OSVersion.Platform == PlatformID.Win32NT)
             {
                 SetProcessWorkingSetSize(System.Diagnostics.Process.GetCurrentProcess().Handle, -1, -1);
             }
         }
 
         public static void CrackerOnlyGC(int sleepSpan = 30)
         {
             new Thread(
                 s =>
                 {
                     while (true)
                     {
                         try
                         {
                             FlushMemory();
                             Thread.Sleep(TimeSpan.FromSeconds((double)sleepSpan));
                         }
                         catch (Exception)
                         {
                         }
                     }
                 })
             { IsBackground = true }.Start();
         }
     }
 }

其中

csharp 复制代码
GC.WaitForPendingFinalizers();

这个方法简单的挂起执行线程,直到Freachable队列中的清空之后,执行完所有队列中的Finalize方法之后才继续执行。

**用法:**只需要在你希望释放的时候调用

csharp 复制代码
 MemUtils.FlushMemory();
 // 或者
 MemUtils.CrackerOnlyGC(60);

事实上,使用该函数并不能提高什么性能,也不会真的节省内存。

因为他只是暂时的将应用程序占用的内存移至虚拟内存,一旦,应用程序被激活或者有操作请求时,这些内存又会被重新占用。

如果你强制使用该方法来 设置程序占用的内存,那么可能在一定程度上反而会降低系统性能,因为系统需要频繁的进行内存和硬盘间的页面交换。

csharp 复制代码
 BOOL SetProcessWorkingSetSize(
   HANDLE hProcess,
   SIZE_T dwMinimumWorkingSetSize,
   SIZE_T dwMaximumWorkingSetSize
 );

将 2个 SIZE_T 参数设置为 -1 ,即可以使进程使用的内存交换到虚拟内存,只保留一小部分内存占用。

因为使用了定时器,不停的进行该操作,所以性能可想而知,虽然换来了小内存的假象,对系统来说确实灾难。

当然,该函数也并非无一是处:

  1. 当我们的应用程序刚刚加载完成时,可以使用该操作一次,来将加载过程不需要的代码放到虚拟内存,这样,程序加载完毕后,保持较大的可用内存。
  2. 程序运行到一定时间后或程序将要被闲置时,可以使用该命令来交换占用的内存到虚拟内存。

注意

这种方式为缓兵之计,物理内存中的数据转移到了虚拟内存中,当内存达到一定额度后还是会崩溃。

3.界面与数据优化

WPF应用程序中常见的性能瓶颈。WPF应用通常具有丰富的图形界面和动态效果,这使得它们在视觉上给人留下深刻印象的同时,也可能导致性能问题。例如,复杂的XAML布局、大量的动画效果、不当的事件处理以及过度的UI更新等,都可能导致应用变得迟钝。因此,为了提高响应速度,我们需要采取一系列措施来优化这些方面。

  • 减少布局计算:扁平化 Grid、RenderTransform 替代 LayoutTransform
  • 降低绑定开销:精准 PropertyChanged、批量 ObservableCollection、UI 虚拟化
  • 优化渲染性能:Freezable 对象冻结、轻量级 DrawingVisual
  • 线程优化防止UI阻塞:异步后台操作、批量 Dispatcher 调用
  • 防止内存泄漏:及时取消事件订阅、合理管理资源引用

3.1.视觉树

视觉树描述了用户界面的层次结构。复杂的视觉树结构会增加渲染的复杂性,影响应用程序的性能。

减少视觉树的深度:避免过深的控件嵌套层级,这样可以减少渲染负担。使用简单的布局和控件结构。

使用UIElement.ClipToBounds:将ClipToBounds设置为true,可以限制控件的绘制区域,从而提高性能。

xml 复制代码
<Button ClipToBounds="True" Content="Click Me"/>

当生成树时应避免使用UIElements作为子或嵌套控件,最好的例子是FlowDocument,我们经常在FlowDocument中使用TextBlock元素。

xml 复制代码
<FlowDocument>
   <Paragraph>
       <TextBlock> some text </TextBlock>
   </Paragraph>
</FlowDocument>

除了上面这样写外,我们还可以象下面这样重写XAML内容,Run元素不是UIElement,渲染时系统开销更小。

xml 复制代码
<FlowDocument>
    <Paragraph>
        <Run> some text </Run>
    </Paragraph>
</FlowDocument>

类似的例子是使用Label控件的Content属性,如果在其生命周期内内容不止更新一次,并且是个字符串 ,这个数据绑定过程可能会阻碍程序的性能,由于内容是一个字符串,在数据绑定期间它会被丢弃,并重新创建。在这种情况下使用TextBlock将数据绑定到Text属性更有效。

在可视化树中出现不必要的元素也会降低WPF程序的速度,最好结合布局优化默认的控件模板。

3.2.减少界面元素的复杂度

简化界面设计

减少控件数量,避免使用过多的复杂控件。例如,使用ItemsControl而不是ListBox,当不需要滚动功能时。

xml 复制代码
<!--过度设计的 ListBox(重型)-->
<ListBox ItemsSource="{Binding DeviceStatuses}">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Border Background="Gray" Margin="5">
                <TextBlock Text="{Binding Name}"/>
            </Border>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

<!--精简的 ItemsControl(轻量)-->
<ItemsControl ItemsSource="{Binding DeviceStatuses}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Border Background="Gray" Margin="5">
                <TextBlock Text="{Binding Name}"/>
            </Border>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

通过移除不必要的嵌套控件,简化了布局结构,减少了布局引擎的工作量。

xml 复制代码
<!--优化前-->
<Grid>
    <StackPanel>
        <Border Background="Red">
            <Grid>
                <StackPanel>
                    <TextBlock Text="Hello, World!" />
                </StackPanel>
            </Grid>
        </Border>
    </StackPanel>
</Grid>
<!--优化后-->
<Grid>
    <Border Background="Red">
        <TextBlock Text="Hello, World!" />
    </Border>
</Grid>

Simplify your Visual Tree(StackOverflow)

A common source of performance issues is a deep and complex layout. Keep your XAML markup as simple and shallow as possible. When UI elements are drawn onscreen, a "layout pass" is called twice for each element (a measure pass and an arrange pass). The layout pass is a mathematically-intensive process---the larger the number of children in the element, the greater the number of calculations required.

使用虚拟化

UI 虚拟化仅渲染可视区域的元素,而非全量渲染:

一个组合框绑定大量数据行时,会使组合框中项目的展现变得非常慢,是因为在这种情况下,程序需要计算每个项目的具体显示位置,使用WPF时,你可以延迟这个行为,这就叫做UI虚拟化,它只会在其可见范围内生产项目显示需要的容器。

要实现这种效果,你需要将相应控件的IsVirtualizing属性设为True,例如,Listbox经常用来绑定大型数据集,它是UI虚拟化的重要候选者,其它适宜UI虚拟化的控件包括Combobox,ListView和TreeView。

虚拟化是提高性能的重要技巧。对于包含大量数据的控件,如ListBox或DataGrid,启用虚拟化可以显著提高性能。

xml 复制代码
<!-- 启用虚拟化的 ListBox(支持10万+数据) -->
<ListBox ItemsSource="{Binding LargeDataList}"
         VirtualizingStackPanel.VirtualizationMode="Recycling">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <!-- 核心:VirtualizingStackPanel 替代默认 StackPanel -->
            <VirtualizingStackPanel/>
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <!-- 可选:固定高度(避免虚拟化失效) -->
    <ListBox.Height>400</ListBox.Height>
</ListBox>

<ListBox ItemsSource="{Binding Items}" VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Name}" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

UI 虚拟化生效条件:列表必须设置固定高度(如 Height="400"),避免嵌套 ScrollViewer(会导致虚拟化失效)

ViruatlizationMode

通过回收执行虚拟化的容器来提高性能,下面的代码片段将ViruatlizationMode设为Recycling,它让你可以获得更好的性能。当用户滚动或抵达另一个项目时,它强制重复使用容器对象。

csharp 复制代码
VirtualizingStackPanel.VirtualizationMode = "Recycling"

Virtualize your ItemsControls(StackOverflow)

As mentioned earlier, a complex and deep visual tree results in a larger memory footprint and slower performance. ItemsControls usually increase performance problems with deep visual trees because they are not virtualized. This means they are constantly being created and destroyed for each item in the control. Instead, use the VirtualizingStackPanel as the items host and make use of the VirtualizingStackPanel.IsVirtualizing and set the VirtualizationMode to Recycling in order to reuse item containers instead of creating new ones each time.

Rendering Tier(渲染层级)

软件渲染的优先级顺序为:

  1. DisableHWAcceleration registry keyDisableHWAcceleration 注册表项
  2. ProcessRenderMode 进程渲染模式
  3. RenderMode (per-target) 渲染模式(按目标)

根据硬件配置的不同,WPF 使用不同的 Rendering Tier 进行图形渲染:

  • Tier 0: 无硬件加速,完全由 CPU 渲染。
  • Tier 1: 部分硬件加速,如位图、基本形状等。
  • Tier 2: 完全硬件加速,支持复杂的视觉效果。

注意:某些情况下即使处于 Rendering Tier 2,也不会启用硬件加速(例如使用某些特效或透明度),具体请查阅 SDK 文档。

使用RenderCapability.Tier属性确定机器是支持硬件加速,还是部分硬件加速,疑惑没有硬件加速,下面的代码显示了你要如何检查Tier。

csharp 复制代码
int  displayTier = (System.Windows.Media.RenderCapability.Tier > 16)
if(displayTier == 0)
{
   // no hardware acceleration
}
else if(displayTier == 1)
{
   // partial hardware acceleration
}
else
{
   // supports hardware acceleration
}
RenderOptions

检查RenderOptions设置:配置RenderOptions来启用硬件加速,确保应用程序能够利用GPU加速图形渲染。

xml 复制代码
<Window
    ...
    RenderOptions.BitmapScalingMode="HighQuality"
    RenderOptions.ClearTypeHint="Enabled">
</Window>

避免不必要的透明度:使用透明度会影响渲染性能。尽量减少透明度的使用,尤其是在复杂的界面中。

布局和设计

1、尽量多使用 Canvas 等简单布局元素,少使用 GridStackPanel 等复杂布局控件,越复杂性能开销越大。

2、在开发逻辑树或视觉树时,遵循 Top-Down(自上而下) 的原则,避免频繁重排布。

布局优化(减少 Measure/Arrange 开销)

WPF 的布局系统通过 Measure(测量)和 Arrange(排列)计算元素位置,嵌套层级越深、容器越复杂,性能开销越大。

WPF 布局开销核心排序(从低到高):

Canvas < DockPanel/WrapPanel < StackPanel < UniformGrid < Grid

扁平化布局(减少嵌套层级)

使用轻量级容器(Grid/Canvas 代替多层 StackPanel);

优先用 RenderTransform 替代 LayoutTransform(不触发布局刷新);

避免不必要的布局刷新(冻结元素、控制尺寸变化)。

使用自定义更轻量级的Panel控件

1.扁平化布局(替代嵌套 StackPanel)
xml 复制代码
<!-- 低效:多层 StackPanel 嵌套,触发多次 Measure/Arrange -->
<StackPanel>
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="姓名:"/>
        <TextBox Text="{Binding Name}"/>
    </StackPanel>
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="年龄:"/>
        <TextBox Text="{Binding Age}"/>
    </StackPanel>
</StackPanel>

<!-- 优化:扁平化 Grid,仅1次布局计算 -->
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <TextBlock Grid.Row="0" Grid.Column="0" Text="姓名:"/>
    <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Name}"/>
    <TextBlock Grid.Row="1" Grid.Column="0" Text="年龄:"/>
    <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Age}"/>
</Grid>
2. RenderTransform 替代 LayoutTransform

LayoutTransform 会触发 Measure/Arrange,RenderTransform 仅在渲染阶段变换,不影响布局:

xml 复制代码
<!-- 低效:LayoutTransform 触发布局刷新 -->
<Rectangle Width="100" Height="100" Fill="Red">
    <Rectangle.LayoutTransform>
        <RotateTransform Angle="45"/>
    </Rectangle.LayoutTransform>
</Rectangle>

<!-- 优化:RenderTransform 无布局开销 -->
<Rectangle Width="100" Height="100" Fill="Red">
    <Rectangle.RenderTransform>
        <!-- CenterX/Y 避免元素偏移 -->
        <RotateTransform Angle="45" CenterX="50" CenterY="50"/>
    </Rectangle.RenderTransform>
</Rectangle>
延迟滚动

可滚动的DataGrid或ListBox,它们往往会降低整个应用程序的性能,因为在滚动时会强制连续更新,这是默认的行为,在这种情况下,我们可以使用控件的延迟滚动(Deferred Scrolling)属性增强用户体验 。你需要做的仅仅是将IsDeferredScrollingEnabled附加属性设为True。

图像优化
  1. Image 做动画处理(如缩放)时,使用以下代码可提高性能:
csharp 复制代码
RenderOptions.SetBitmapScalingMode(MyImage, BitmapScalingMode.LowQuality);
  1. 使用 TileBrush 时,添加 CachingHint 可以提升渲染效率。

  2. 降低Bitmapscalingmode,加速图像渲染

当你的WPF程序中包含有动画时,你可以使用RenderOptions对象的BitmapScalingMode属性降低资源消耗,需要将BitMapScalingMode属性的值设为LowQuality,这样就会使用加速算法 处理图像,而不是默认的高质量图像重采样算法。

csharp 复制代码
RenderOptions.SetBitmapScalingMode(imageObject,BitmapScalingMode.LowQuality);

3.3.优化数据绑定

WPF的强大数据绑定功能在处理大量数据时可能会导致性能问题。如果绑定的更新频率过高,会影响应用程序的响应速度。

数据绑定是 WPF 高频操作,不当使用会导致频繁的 PropertyChanged/CollectionChanged 通知,引发 UI 频繁刷新。

  • 精准触发 INotifyPropertyChanged(对变化的值和原来的值进行比较)
  • 启用 UI 虚拟化(VirtualizingStackPanel)
  • 批量更新集合(减少 CollectionChanged 通知)
  • 优先用 OneWay/OneTime 替代 TwoWay。
  • 数据虚拟化(分页加载数据)
精准触发 PropertyChanged(避免冗余通知)
csharp 复制代码
using System.ComponentModel;
using System.Runtime.CompilerServices;

public class UserViewModel : INotifyPropertyChanged
{
    private string _name;
    private int _age;

    // ✅ 仅在属性真正变化时触发通知
    public string Name
    {
        get => _name;
        set
        {
            if (_name == value) return;
            _name = value;
            OnPropertyChanged(); // CallerMemberName 自动获取属性名
        }
    }

    public int Age
    {
        get => _age;
        set
        {
            if (_age == value) return;
            _age = value;
            OnPropertyChanged();
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string propName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
    }
}
批量更新 ObservableCollection(减少通知频率)

默认 ObservableCollection 每添加1项触发1次 CollectionChanged,批量更新可大幅优化

csharp 复制代码
/// <summary>
/// 支持批量添加的 ObservableCollection
/// </summary>
public class BatchObservableCollection<T> : ObservableCollection<T>
{
    private bool _suppressNotification;

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (!_suppressNotification) base.OnCollectionChanged(e);
    }

    /// <summary>
    /// 批量添加元素(仅触发1次通知)
    /// </summary>
    public void AddRange(IEnumerable<T> items)
    {
        _suppressNotification = true;
        try
        {
            foreach (var item in items) Add(item);
        }
        finally
        {
            _suppressNotification = false;
            // 手动触发1次重置通知
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }
    }
}

// 使用示例
public void Load10WData()
{
    var batchList = new BatchObservableCollection<string>();
    // 模拟10万条数据
    var tempData = Enumerable.Range(0, 100000).Select(i => $"Item {i}").ToList();
    // 批量添加(仅1次通知)
    batchList.AddRange(tempData);
    LargeDataList = batchList;
}

尽量避免频繁更新绑定数据,尤其是在大型数据集合中。可以使用BindingOperations.EnableCollectionSynchronization来优化集合的绑定。

对象行为

1、访问 CLR 对象和 CLR 属性的效率高于访问 DependencyObject / DependencyProperty。但要注意的是,这里仅指"访问"操作,并不包括数据绑定。

2、虽然 DependencyProperty 访问效率略低,但它具有诸多优点,如支持数据绑定、样式、继承等,适合用于 UI 元素。

应用程序资源

1、自定义控件中应避免在控件的 ResourceDictionary 中定义资源,推荐将资源放在 WindowApplication 级别,避免每个控件实例都保留一份资源副本。 2、尽量使用 StaticResource,但不能盲目使用。动态需求应使用 DynamicResource

文本处理

1、显示文字较少时使用 TextBlockLabel;内容较多时使用 FlowDocument

2、若不需要 TextFlow 的高级特性,优先使用 TextBlock,因其效率更高。

3、在 FlowDocument 中尽量避免使用 UIElement(如 TextBlock),应使用轻量级的 TextElement(如 Run)。

4、在 TextBlock 中显式使用 Run 标签比直接写字符串效率更高。

5、将 Label.Content 绑定到字符串的效率低于将 TextBlock.Text 绑定到字符串,因为 Label 在更新内容时会丢弃旧内容重新加载。如果内容不变,不影响性能。

6、在 TextBlock 中使用多个超链接时,组合在一起效率更高。

7、显示超链接时,尽量只在 IsMouseOverTrue 时显示下划线,持续显示下划线性能消耗更大。

8、尽量避免不必要的字符串拼接操作。

数据绑定优化
  • 1、数据绑定的效率取决于数据源实现机制:
    • A、使用 TypeDescriptor/PropertyChanged 实现通知 → 效率最低(反射)
    • B、使用 INotifyPropertyChanged 实现通知 → 效率稍高
    • C、使用 DependencyObject + DependencyProperty → 效率最高(无需反射)
  • 2、如果一个 CLR 对象包含大量属性(如 1000 个),建议将其拆分为多个小型对象(如 1000 个单属性对象),提升绑定效率。
  • 3、在列表控件(如 ListBox)中展示数据时,若希望动态反映数据变化,应使用 ObservableCollection<T> 而不是直接更新 ItemsSource
  • 4、尽量绑定 IList 而非 IEnumerableItemsControl,提高访问效率。

Bind ItemsControls to IList instead of IEnumerable(StackOverflow)

When data binding an ItemsControl to an IEnumerable, WPF will create a wrapper of type IList which negatively impacts performance with the creation of a second object. Instead, bind the ItemsControl directly to an IList to avoid the overhead of the wrapper object.

StaticResource优于DynamicResource

StaticResource 替代 DynamicResource(减少资源查找开销)

StaticResources通过查找对已定义资源的引用来为任何XAML属性属性提供值。该资源的查找行为与编译时查找相同。DynamicResources将创建一个临时表达式,并延迟对资源的查找,直到需要请求的资源值为止。该资源的查找行为与运行时查找相同,这会对性能产生影响。尽可能始终使用StaticResource。

xml 复制代码
<!-- 推荐:StaticResource(仅1次查找,性能高) -->
<Button Background="{StaticResource FrozenGreenBrush}" Content="静态资源"/>

<!-- 不推荐:DynamicResource(每次访问都查找,仅资源动态变更时用) -->
<Button Background="{DynamicResource NonFrozenBrush}" Content="动态资源"/>

Favor StaticResources Over DynamicResources(SatckOverflow)

StaticResources provide values for any XAML property attribute by looking up a reference to an already defined resource. Lookup behavior for that resource is the same as a compile-time lookup. DynamicResources will create a temporary expression and defer lookup for resources until the requested resource value is required. Lookup behavior for that resource is the same as a run-time lookup, which imposes a performance impact. Always use a StaticResource whenever possible.

Opacity on Brushes Instead of Elements(SatckOverflow)

If you use a Brush to set the Fill or Stroke of an element, it is better to set the Opacity on the Brush rather than setting the element's Opacity property. When you modify an element's Opacity property, it can cause WPF to create temporary surfaces which results in a performance hit.

修改控件 Opacity 时,建议修改 Brush 的透明度并用其填充控件,而不是直接修改控件的 Opacity,后者会导致系统创建临时 Surface。

Favor StreamGeometries over PathGeometries(SatckOverflow)

The StreamGeometry object is a very lightweight alternative to a PathGeometry. StreamGeometry is optimized for handling many PathGeometry objects. It consumes less memory and performs much better when compared to using many PathGeometry objects.

3.4.UI 渲染优化(减少 GPU/CPU 开销)

WPF 渲染依赖 DirectX,不合理的渲染设置会导致 CPU/GPU 占用过高。

  • 冻结 Freezable 对象(如 Brush、Pen、Geometry);
  • 使用轻量级 DrawingVisual 替代 FrameworkElement;
  • 减少透明/阴影等耗时特效;
  • 配置硬件加速(按需启用/禁用)。
  • 渲染图片时可选择先渲染缩略图,按需渲染原图。
冻结 Freezable 对象(减少内存拷贝)

Freezable 对象冻结后不可修改,但能避免 WPF 内部的线程安全拷贝,提升渲染性能:

csharp 复制代码
// 代码冻结 Brush
var blueBrush = new SolidColorBrush(Colors.Blue);
if (!blueBrush.IsFrozen)
{
    blueBrush.Freeze(); // 冻结后不可修改,性能提升30%+
}
myRectangle.Fill = blueBrush;

XAML 冻结资源(推荐)

xml 复制代码
<Window.Resources>
    <!-- ✅ x:Freeze="True" 冻结资源 -->
    <SolidColorBrush x:Key="FrozenGreenBrush" Color="Green" x:Freeze="True"/>
</Window.Resources>
<Rectangle Fill="{StaticResource FrozenGreenBrush}" Width="100" Height="100"/>

注意:

Freezable 冻结后不可修改:若需动态修改 Brush/Pen,不要冻结;

Use and Freeze Freezables(StackOverflow)

A Freezable is a special type of object that has two states: unfrozen and frozen. When you freeze an object such as a Brush or Geometry, it can no longer be modified. Freezing objects whenever possible improves the performance of your application and reduces its memory consumption.

轻量级 DrawingVisual(替代 FrameworkElement)

DrawingVisual是无布局的轻量级视觉元素,比Button/Rectangle等FrameworkElement性能高10倍+

csharp 复制代码
using System.Windows;
using System.Windows.Media;

/// <summary>
/// 自定义轻量级视觉控件
/// </summary>
public class LightweightVisualControl : FrameworkElement
{
    private readonly DrawingVisual _visual;

    public LightweightVisualControl()
    {
        _visual = new DrawingVisual();
        // 绘制内容(仅1次绘制,无布局开销)
        using (var dc = _visual.RenderOpen())
        {
            dc.DrawRectangle(
                Brushes.Orange,
                new Pen(Brushes.Black, 2),
                new Rect(0, 0, 100, 100));
            dc.DrawText(
                new FormattedText("轻量级元素", CultureInfo.CurrentCulture,
                FlowDirection.LeftToRight, new Typeface("Arial"), 12, Brushes.Black),
                new Point(10, 40));
        }
        AddVisualChild(_visual);
    }

    // 重写视觉子元素计数
    protected override int VisualChildrenCount => 1;

    // 重写获取视觉子元素
    protected override Visual GetVisualChild(int index)
    {
        if (index != 0) throw new ArgumentOutOfRangeException();
        return _visual;
    }
}
卸载事件卸载不必要的动画

动画肯定会占用一定的资源,如果处置方式不当,将会消耗更多的资源,如果你认为它们无用时,你应该考虑如何处理他们,如果不这样做,就要等到可爱的垃圾回收器先生来回收资源。

假设要删除一个StoryBorad,在Unload事件中使用StoryBorad的Remove方法.

xml 复制代码
<EventTrigger RoutedEvent = "Page.Unloaded">
   <EventTrigger.Actions>
       <RemoveStoryboard BeginStoryboardName = "myBeginStoryboard"/>
   </EventTrigger.Actions>
</EventTrigger>
字体缓存

使用字体缓存服务提高启动时间

WPF应用程序之间可以共享字体数据,它是通过一个叫做PresentationFontCache Service的Windows服务实现的,它会随Windows自动启动。

可以在控制面板的"服务"中找到这个服务(或在"运行"框中输入Services.msc),确保这个服务已经启动。

3.5.线程处理优化(避免 UI 阻塞)

WPF 主线程(STA线程)负责 UI 渲染,耗时操作(如数据查询、文件读写)必须放在后台线程,避免界面卡顿。

  • 耗时操作用 Task.Run 放在后台线程;
  • 批量更新 UI(减少 Dispatcher 调用);
  • 避免频繁 Dispatcher.Invoke(合并更新),建议使用 Dispatcher.InvokeAsync。
  • 可以起多个STA线程加快耗时UI操作的渲染。
调用线程必须为 STA

不同 STA 线程的 UI 元素无法直接跨线程访问,必须通过目标线程的Dispatcher进行调度

csharp 复制代码
namespace WpfSTAThreadDemo
{
    public partial class MainWindow : Window
    {
        // 保存新 STA 线程的 Dispatcher(用于跨线程调度)
        private Dispatcher _secondaryThreadDispatcher;

        public MainWindow()
        {
            InitializeComponent();
        }

        // 创建新 STA 线程
        private void CreateNewSTAThread_Click(object sender, RoutedEventArgs e)
        {
            Thread newSTAThread = new Thread(STAThreadEntry);
            newSTAThread.SetApartmentState(ApartmentState.STA);
            newSTAThread.IsBackground = true;
            newSTAThread.Start();
        }

        private void STAThreadEntry()
        {
            SecondaryWindow secondaryWindow = new SecondaryWindow();
            secondaryWindow.Title = "新 STA 线程创建的窗口";
            secondaryWindow.Width = 400;
            secondaryWindow.Height = 300;

            // 保存该线程的 Dispatcher(供主线程调用)
            _secondaryThreadDispatcher = Dispatcher.CurrentDispatcher;

            secondaryWindow.Show();
            Dispatcher.Run();
        }

        // 主线程按钮点击:修改新 STA 线程的 UI 内容(同步)
        private void UpdateSecondaryWindowUI_Click(object sender, RoutedEventArgs e)
        {
            if (_secondaryThreadDispatcher != null && !_secondaryThreadDispatcher.HasShutdownStarted)
            {
                // 关键:通过目标线程的 Dispatcher 调度UI操作,避免跨线程异常
                _secondaryThreadDispatcher.Invoke(() =>
                {
                    // 此处操作新 STA 线程的 UI 元素(安全)
                    SecondaryWindow secondaryWindow = (SecondaryWindow)Application.Current.Windows
                        .FirstOrDefault(w => w.Title == "新 STA 线程创建的窗口");
                    if (secondaryWindow != null)
                    {
                        secondaryWindow.Content = new TextBlock
                        {
                            Text = $"主线程于 {DateTime.Now:HH:mm:ss} 修改了该窗口内容",
                            FontSize = 16
                        };
                    }
                });
            }
        }
    }
}
耗时逻辑处理

通过使用Task.Run方法,将耗时操作放到一个新的线程中执行,这样就不会阻塞UI线程了。

csharp 复制代码
// 优化前:
private void Button_Click(object sender, RoutedEventArgs e)
{

    // 假设这是一个耗时的操作
    Thread.Sleep(5000);
}

// 优化后:
private void Button_Click(object sender, RoutedEventArgs e)
{

    Task.Run(() =>
    {

        // 执行耗时操作
        Thread.Sleep(5000);
    });
}
异步操作

避免在UI线程中执行长时间操作:将长时间操作移到后台线程中,确保UI线程的流畅性。这对于数据加载和复杂计算尤其重要。

csharp 复制代码
private async void LoadData()
{
    var data = await GetDataAsync();
    // 更新UI
}

public async Task<string> GetDataAsync()
{
    // 模拟长时间操作
    await Task.Delay(2000);
    return "Data loaded";
}

3.6.其他性能建议

  • 使用 NavigationWindow 时,推荐通过对象更新客户端区域,而非使用 URI。
  • 尽量避免使用 ScrollBarVisibility=Auto,它可能引发额外的布局计算。
  • 使用缩小的图像尺寸

如果您的应用要求显示较小的缩略图,请考虑创建缩小尺寸的图像。默认情况下,WPF将加载并解码图像到其完整尺寸。如果要加载完整的图像并将它们缩小到ItemsControl等控件中的缩略图大小,这可能是许多性能问题的根源。如果可能,将所有图像合并为一个图像,例如由多个图像组成的胶片。

  • 降低BitMapScalingMode

默认情况下,WPF使用高质量的图像重采样算法,该算法有时会消耗系统资源,这会导致帧速率下降并导致动画停顿。而是将BitMapScalingMode设置为LowQuality,以从"质量优化"算法切换为"速度优化"算法。

  • 在单独的线程上加载数据

性能问题,UI冻结以及停止响应的应用程序的一个非常常见的来源是如何加载数据。确保您在一个单独的线程上异步加载数据,以免UI线程超载。在UI线程上加载数据将导致非常差的性能以及最终用户的整体体验。每个WPF开发人员都应在其应用程序中使用多线程。

3.7.总结

WPF 提供了强大的功能,但也对开发者提出了更高的性能优化要求。通过对 渲染层级、布局结构、图像处理、资源管理、文本操作、数据绑定 等方面的合理优化,可以显著提升应用的响应速度与运行效率。

在开发过程中,建议结合实际场景选择合适的优化策略,同时借助性能分析工具(如 Visual Studio 的诊断工具、Perforator、WPF Performance Suite)进行监控和调优。

4.硬件加速

WPF使用硬件加速来提升图形渲染性能。但在某些情况下,硬件加速可能会引起性能问题,特别是在处理复杂的动画或图形时。
硬件加速兼容性:部分老旧显卡/远程桌面场景,硬件加速可能导致闪烁,可局部禁用

硬件加速,可以设置针对全局、窗口、控件配置硬件加速

csharp 复制代码
//1.全局硬件加速
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        // 配置全局硬件加速模式
        // 选项1:启用硬件加速(默认值,显卡支持时自动启用全加速)
        RenderOptions.ProcessRenderMode = RenderMode.Default;

        // 选项2:强制启用硬件加速(忽略部分显卡兼容性问题,不推荐随意使用)
        // RenderOptions.ProcessRenderMode = RenderMode.HardwareAccelerated;

        // 选项3:禁用硬件加速(全局使用软件渲染)
        // RenderOptions.ProcessRenderMode = RenderMode.SoftwareOnly;
    }
}

窗口/某个控件:

csharp 复制代码
public MainWindow()
{
    InitializeComponent();

    // 配置当前窗口的硬件加速模式
    // 禁用硬件加速
    RenderOptions.SetRenderMode(this, RenderMode.SoftwareOnly);

    // 启用硬件加速
    // RenderOptions.SetRenderMode(this, RenderMode.HardwareAccelerated);
}

// 或者,可以订阅窗口的 SourceInitialized 事件并执行相同的操作。
// 禁用硬件加速
public partial class MyWindow : Window
{
    public MyWindow()
        : base()
    {
        InitializeComponent();
    }

    protected override void OnSourceInitialized(EventArgs e)
    {
        var hwndSource = PresentationSource.FromVisual(this) as HwndSource;

        if (hwndSource != null)
            hwndSource.CompositionTarget.RenderMode = RenderMode.SoftwareOnly;

        base.OnSourceInitialized(e);
    }
}

xaml

xml 复制代码
<Window x:Class="WpfHardwareAccelerationDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="WPF硬件加速演示" Height="450" Width="800"
       <!-- 禁用当前窗口的硬件加速(软件渲染) -->
        RenderOptions.RenderMode="SoftwareOnly">
    <Grid>
        <!-- 针对单个Image元素禁用硬件加速 -->
        <Image Source="test.png" Width="300" Height="300"
               RenderOptions.RenderMode="SoftwareOnly" />

        <!-- 针对单个Canvas元素启用硬件加速 -->
        <Canvas Width="200" Height="200" Background="LightBlue"
                RenderOptions.RenderMode="HardwareAccelerated" />
    </Grid>
</Window>

5.设计优化

规划应用程序性能

实现绩效目标的成功取决于您制定绩效策略的能力。规划是开发任何产品的第一步。本主题描述了制定良好性能策略的一些非常简单的规则。

在开始开发任何WPF应用程序之前,必须先制定性能策略。应用程序启动时间,每帧动画频率速率,窗口中允许的最大工作集都是可以提前计划和制定战略的一些方案。

一旦提出了方案,就可以定义目标并以循环/迭代的方式评估,研究,改进应用程序的性能。

文章推荐

优化WPF应用程序性能

6.待机优化

全局禁用硬件渲染(会影响性能)

在app.xaml.cs添加以下代码即可:

csharp 复制代码
protected override void OnStartup(StartupEventArgs e)
{
    RenderOptions.ProcessRenderMode = System.Windows.Interop.RenderMode.SoftwareOnly;
}

WPF materialDesign 锁屏后界面卡死问题解决

如果使用了materialDesign,很容易出现花屏或卡死现象:

因为默认使用了提高渲染性能

xml 复制代码
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"

materialDesign的一个缓存机制问题,在xaml文件中Window属性添加一行:

xml 复制代码
materialDesign:ShadowAssist.CacheMode="{x:Null}"

易混淆

窗口关闭内存不能完全释放

在WPF中,当关闭窗口时,窗口对象及其关联的资源应该被自动释放。然而,如果您注意到关闭窗口后内存没有被完全释放,可能是由于以下几个原因:

  1. 事件处理程序的内存泄漏:如果您在窗口中订阅了事件,但在关闭窗口之前没有取消订阅它们,这可能会导致内存泄漏。在窗口关闭事件处理程序中,确保取消订阅所有事件。
  2. 对象引用的内存泄漏:在某些情况下,您可能会保留对窗口对象的引用,即使窗口已经关闭。这会阻止垃圾回收器将窗口对象及其相关资源释放,从而导致内存泄漏。在关闭窗口时,确保删除所有对窗口对象的引用。
  3. 静态资源的内存泄漏:如果您在窗口中使用了大量的静态资源,这些资源可能会在窗口关闭后仍然存在于内存中。这可能是由于WPF的静态资源缓存机制。您可以尝试手动清除静态资源缓存,以便在关闭窗口时释放内存。
  4. 未处理的非托管资源:如果您在窗口中使用了非托管资源(如文件句柄或数据库连接),可能需要手动释放这些资源。否则,这些资源可能会继续存在于内存中,即使窗口已经关闭。

如果遇到这种情况,可以使用.NET内存分析工具,如.NET Memory Profiler,来分析内存泄漏的原因。

列表中相同URL的图片会重复占用内存吗

在WPF中,如果多个控件使用相同的图片地址来显示图片,那么这些控件会共享同一个图片实例,而不会重复占用内存。

具体来说,WPF会将图片数据缓存到内存中,并为每个图片创建一个唯一的标识符。当多个控件使用相同的图片地址时,WPF会查找缓存中是否已经存在相应的图片实例,如果存在,就会直接使用该实例。这样,不同控件之间就可以共享同一个图片实例,避免了重复占用内存的问题。

需要注意的是:

如果多个控件使用相同的图片地址,但是图片实例被手动缓存或者持有引用导致无法释放,就可能会导致内存泄漏问题。
因此,在使用图片时,最好不要手动缓存图片实例,并且在不需要使用图片时,及时将图片引用设置为null,以便让GC回收不再使用的图片实例,避免内存泄漏问题的发生。

在析构函数中释放按钮事件合理吗?

在WPF窗口的析构函数中释放按钮事件监听器可以解决内存泄漏问题,但是也有一定的风险。

主要有以下几点需要注意:

  1. 析构函数是对象被销毁时系统自动调用的,因此如果在运行过程中窗口没有正常关闭,析构函数也不会执行,那么事件监听器仍然无法被释放,内存泄漏问题仍然存在。

假如窗口在ShowDialog()后关闭,它的析构函数实际上会在窗口关闭后立即执行。

  1. 这时如果还未来得及解除按钮的事件监听,会导致InvalidOperationException。
  2. 依赖属性(如Command)的事件绑定在窗口关闭后会自动解除,无需手动释放。但直触发的事件(如Click)需要手动 -= 解除,所以只在析构函数中释放直触发的事件。
  3. 在调用Close()或Hide()方法后,窗口transition会导致按钮失效,所以应在这些方法中先解除事件,然后再关闭窗口。而在析构函数中解除事件为时已晚。

优先在窗口的Closing事件中解除按钮事件监听器。

csharp 复制代码
void Window_Closing(object sender, CancelEventArgs e)
{
    button1.Click -= Button_Click;
}
  1. 其次在Close()或Hide()方法调用前解除事件监听器。
  2. 最后,作为备份,在析构函数中再做一次事件监听器的解除以防漏掉。

对依赖属性的事件绑定不需要在以上三处释放,系统会自动解除。所以总结来说,在WPF窗口的析构函数中释放事件监听器可以作为一种备份手段,但最佳的方式还是在Closing事件或Close()/Hide()方法中释放,这两个时机都先于析构函数,可以更好地防止InvalidOperationException的发生。二者结合可以达到最佳的内存管理效果。

using & var

using

using关键字有两个主要用途:

  • 作为指令,用于为命名空间创建别名或导入其他命名空间中定义的类型。
  • 作为语句,用于定义一个范围,在此范围的末尾将释放对象。

作为语句

using 语句允许程序员指定使用资源的对象应当何时释放资源。using 语句中使用的对象必须实现 IDisposable 接口。此接口提供了 Dispose 方法,该方法将释放此对象的资源。

使用规则

  1. using只能用于实现了IDisposable接口的类型,禁止为不支持IDisposable接口的类型使用using语句,否则会出现编译错误
  2. using语句支持初始化多个变量,但前提是这些变量的类型必须相同
  3. 针对初始化多个不同类型的变量时,可以都声明为IDisposable类型

using实质

在程序编译阶段,编译器会自动将using语句生成为try-finally语句,并在finally块中调用对象的Dispose方法,来清理资源。所以,using语句等效于try-finally语句

var

var 关键字是C# 3.0新增的特性,称为推断类型。也就是说 var 可以替代所有类型,因为编译器会推断出你这里应该使用的类型,但是需要注意的是:

  1. var 的所修饰的变量必须是局部变量
  2. var 修改的变量必须在定义的时候初始化
  3. 一旦 var 修饰的变量初始化完成,就不能再给变量赋予跟初始值不同的值。
csharp 复制代码
// 错误示范
var a;  //隐式类型的局部变量必须已经初始化
var b = {1,2,3};  //1、无法用数组的初始值 初始化隐式类型的局部变量
//2、只能使用数组初始值表达式为数组类型赋值,尝试使用new表达式。
 
var e = null; //无法将null赋予隐式局部变量
// 改正
var a= 2020;
var b = new int[]{1,2,3};
var e = "广东";
相关推荐
wangnaisheng8 小时前
【WPF】路由事件详细使用
wpf
广州服务器托管8 小时前
[2026.4.27]WIN10.1809.17763.8647[PIIS]中简优化版LTSC2019 丝滑流畅 老爷机续命系统
运维·人工智能·windows·计算机网络·可信计算技术
Maydaycxc8 小时前
影刀RPA锁屏失败排查:从错误码看Windows会话机制
windows·rpa
yqcoder17 小时前
前端性能优化:如何减少重绘与重排?
前端·性能优化
brucelee18618 小时前
Claude Code 安装教程(Windows / Linux / macOS)
linux·windows·macos
wltx168818 小时前
外贸独立站+GEO优化需要多久维护一次?
性能优化
前端百草阁20 小时前
【前端性能优化全链路指南】从开发编写到构建运行的多维度实践
前端·性能优化
卷Java1 天前
GPTQ vs AWQ vs GGUF:模型量化工具横向测评
开发语言·windows·python
Wect1 天前
React 性能优化精讲
前端·react.js·性能优化