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

WPF的Window的生命周期

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

  1. 构造函数(Constructor):创建窗口的实例对象。在此阶段可以设置窗口的属性和注册事件。

  2. 加载事件(Loaded Event):窗口对象被添加到界面树中,但是还没有完全渲染。在此阶段可以执行一些初始化的操作。

  3. 可见性事件(IsVisibleChanged Event):窗口被设置为可见状态,这时候才可以看到界面上的内容。在此阶段可以进行一些页面加载的操作。

  4. 激活事件(Activated Event):窗口被激活,即成为用户当前的操作窗口,此时窗口获取焦点。

  5. 关闭事件(Closing Event):窗口关闭前的事件,可以在此阶段进行一些保存数据的操作及按钮等注册的事件。

  6. 已关闭事件(Closed Event):窗口已关闭,如果没有绑定事件,窗口将被销毁。

  7. 从界面树移除事件(Unloaded Event):窗口从界面树中移除,但仍然存在于内存中。在此阶段可以进行一些清理和释放资源的操作。

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

内存优化

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

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

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

第5点不建议

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

防止内存泄漏

内存泄露原因

内存泄露主要原因分析:

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

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

图片处理

加载图片

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

1
2
3
Uri uri = new Uri(ImageSavePath, UriKind.Absolute);
BitmapImage bimg = new BitmapImage(uri);
myimage.Source = bitmap;

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

修改加载方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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后立马释放掉资源

1
myimage.Source = GetImage(path); //path为图片的路径

释放

1
myimage.Source = null;

注意

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

列表中加载图片

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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;
}
}
}

页面中

1
2
3
4
5
6
7
8
<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>

局部变量代替成员变量

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

窗口弱引用

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

下面给出一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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或是直接给列表控件绑定数据源的情况下,关闭窗体时,最好将绑定属性赋一个空值

1
2
3
4
5
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
this.DataContext = null;
}

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

  1. 释放资源:当不再需要控件与数据模型的绑定关系时,将DataContext设置为null可以释放对应的资源,避免对内存的浪费。

  2. 取消绑定:有时候需要在运行时取消控件与数据模型的绑定关系,例如当窗口或页面关闭时,需要取消与该窗口或页面相关的所有绑定关系,此时将DataContext设置为null可以轻松实现此功能。

类与类之间尽量不要互相引用

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

1
2
3
4
Class1 class1 =new Class1();
Class2 class2 = new Class2();
class1.Class2 = class2;
class2.Class1 = class1;

清除引用:

1
2
3
4
class2.Class1 = null;
class2 = null;
class1.Class2 = null;
class1 =null;

静态变量

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

我知道有些开发人员认为使用静态变量始终是一种不好的做法。 尽管有些极端,但在谈论内存泄漏时的确需要注意它。

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

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

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

GC Root的特征

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

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

这里是一个例子:

1
2
3
4
5
6
7
8
public class MyClass
{
static List<MyClass> _instances = new List<MyClass>();
public MyClass()
{
_instances.Add(this);
}
}

如果你出于某种原因而决定编写上述代码,那么任何MyClass的实例将永远留在内存中,从而导致内存泄漏。

解绑事件

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

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

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

WPF怎么在页面关闭的时候自动解绑页面绑定的所有事件

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

方式1 手动解绑事件

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

1
2
3
4
private void MyWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
MyButton.Click -= MyButtonClickEventHandler;
}

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

方式2 使用弱事件

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

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

使用WeakEventManager

以下是一个使用弱事件的示例:

页面中

1
<Button Name="MyBtn" Content="点击" />

代码中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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中,因此即使事件处理器没有被显式地取消注册,也不会导致内存泄漏问题。

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

1
2
3
var button = new MyButton();
button.Click += new RoutedEventHandler(Button_Click);
button.Click -= new RoutedEventHandler(Button_Click);

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

1
2
3
4
5
var button = new MyButton();
button.Click += (s, e) =>
{
// 处理Click事件
};

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

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

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

这里是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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所引用,从而产生内存泄漏。

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

1
2
3
4
5
6
7
8
9
10
11
12
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

这里是一个例子:

1
2
3
4
5
<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}"></TextBlock>
</UserControl>

这个View Model将永远留在内存中:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyViewModel
{
public string _someText = "内存泄漏";
public string SomeText
{
get { return _someText; }
set
{
_someText = value;
}
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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。考虑以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

这里有一个简单的例子。

1
2
3
4
5
6
7
8
9
10
11
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方法,以释放所有非托管资源,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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方法以释放所有非托管资源。 这很棒,但是当有人使用了该类却没有调用Dispose时会发生什么呢?

为了避免这种情况,你可以在C#中使用using语句:

1
2
3
4
using (var instance = new MyClass())
{
// ...
}

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

1
2
3
4
5
6
7
8
9
10
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#中,析构函数是一个没有参数和返回值的方法,其名称与类名相同,并以”~”开头。

例如:

1
2
3
4
5
6
7
public class MyClass
{
~MyClass()
{
// 做一些清理工作
}
}

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

需要注意的是:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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

比如

1
public static List<String> myMothod(){}

请改成

1
public static void myMothod(out List<String> result){}

这个观点是不对的

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

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

慎用隐式类型var的弱引用

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

这个观点是不对的

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

增大可用内存

注意

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

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

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

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

image-20200818084950845

安装时注意勾选红框对应的模块

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

1
editbin /largeaddressaware xhschool.exe

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

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

1
dumpbin /headers xhschool.exe | more

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

找到安装路径

在VS的安装路径下搜索editbin.exe

比如我的

D:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\MSVC\14.27.29110\bin\Hostx86\x86

添加到环境变量中

重启VS

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

项目右键属性=>生成事件=>生成后事件命令行

添加如下命令

1
editbin.exe /largeaddressaware xhschool.exe

节省内存

DependencyObject(依赖属性)

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

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

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

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

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

1
2
3
4
5
public class Student
{
public string Name { get; set; }
public double Height { get; set; }
}

替换为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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)
);
}

注意

这个不用自己敲,输入propdp,点Tab键就会自动生成。

字符串拼接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 不推荐
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,开销最小,但这样就导致了一些编程时的麻烦,即未定义样式,就不能引用样式,哪怕定义在后,引用在前都不行。

img

注意:

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

    因为放在控件中会使每个实例都保留一份资源的拷贝。

  2. 尽量使用Static Resources。

不同组件内存占用

  1. 布局时候能用Canvas尽量用Canvas。Gird,StackPanel内存开销相对Canvas大。

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

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

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

    如果字符串不需要更新,用Label就无所谓性能问题。

少用透明窗口

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

1
2
3
AllowTransparency="True" 
WindowStyle="None"
Background="Transparent"

我的电脑是1920*1080,一个还没有全屏大小的窗口,透明比不透明就大了30多M,太恐怖了,所以尽量少用透明窗口。

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

GeometryDrawing实现简单图片

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

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

1
2
3
4
5
6
7
<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>

这边是重用

1
2
3
4
5
6
7
<DrawingBrush 
x:Key="FrameListMenuArea_Brush"
Stretch="Fill"
TileMode="Tile"
Viewport="0,0,50,50"
ViewportUnits="Absolute"
Drawing="{StaticResource Diagonal_50px}"/>

定时GC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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);
}

使用虚拟内存(不建议)

注意

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

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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();
}
}
}

其中

1
GC.WaitForPendingFinalizers();

作用:

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

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

1
2
3
MemUtils.FlushMemory();
// 或者
MemUtils.CrackerOnlyGC(60);

事实上,使用该函数并不能提高什么性能,也不会真的节省内存。
因为他只是暂时的将应用程序占用的内存移至虚拟内存,一旦,应用程序被激活或者有操作请求时,这些内存又会被重新占用。

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

1
2
3
4
5
BOOL SetProcessWorkingSetSize(
HANDLE hProcess,
SIZE_T dwMinimumWorkingSetSize,
SIZE_T dwMaximumWorkingSetSize
);

将 2个 SIZE_T 参数设置为 -1 ,即可以使进程使用的内存交换到虚拟内存,只保留一小部分内存占用。
因为使用了定时器,不停的进行该操作,所以性能可想而知,虽然换来了小内存的假象,对系统来说确实灾难。
当然,该函数也并非无一是处:

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

注意

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

内存监测软件

Ants Memory Profiler

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

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

dotMemory

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

常见问题

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

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

  1. 事件处理程序的内存泄漏:如果您在窗口中订阅了事件,但在关闭窗口之前没有取消订阅它们,这可能会导致内存泄漏。在窗口关闭事件处理程序中,确保取消订阅所有事件。

  2. 对象引用的内存泄漏:在某些情况下,您可能会保留对窗口对象的引用,即使窗口已经关闭。这会阻止垃圾回收器将窗口对象及其相关资源释放,从而导致内存泄漏。在关闭窗口时,确保删除所有对窗口对象的引用。

  3. 静态资源的内存泄漏:如果您在窗口中使用了大量的静态资源,这些资源可能会在窗口关闭后仍然存在于内存中。这可能是由于WPF的静态资源缓存机制。您可以尝试手动清除静态资源缓存,以便在关闭窗口时释放内存。

  4. 未处理的非托管资源:如果您在窗口中使用了非托管资源(如文件句柄或数据库连接),可能需要手动释放这些资源。否则,这些资源可能会继续存在于内存中,即使窗口已经关闭。

如果您遇到这种情况,您可以使用.NET内存分析工具,如.NET Memory Profiler,来分析内存泄漏的原因。这些工具可以帮助您识别和解决内存泄漏问题。

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

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

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

需要注意的是:

如果多个控件使用相同的图片地址,但是图片实例被手动缓存或者持有引用导致无法释放,就可能会导致内存泄漏问题。

因此,在使用图片时,最好不要手动缓存图片实例,并且在不需要使用图片时,及时将图片引用设置为null,以便让GC回收不再使用的图片实例,避免内存泄漏问题的发生。

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

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

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

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

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

    这时如果还未来得及解除按钮的事件监听,会导致InvalidOperationException。

  3. 依赖属性(如Command)的事件绑定在窗口关闭后会自动解除,无需手动释放。但直触发的事件(如Click)需要手动 -= 解除,所以只在析构函数中释放直触发的事件。

  4. 在调用Close()或Hide()方法后,窗口transition会导致按钮失效,所以应在这些方法中先解除事件,然后再关闭窗口。而在析构函数中解除事件为时已晚。

正确的做法是:

  1. 优先在窗口的Closing事件中解除按钮事件监听器。
    如:
1
2
3
4
void Window_Closing(object sender, CancelEventArgs e)
{
button1.Click -= Button_Click;
}
  1. 其次在Close()或Hide()方法调用前解除事件监听器。

  2. 最后,作为备份,在析构函数中再做一次事件监听器的解除以防漏掉。

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

    二者结合可以达到最佳的内存管理效果。

小知识

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 修饰的变量初始化完成,就不能再给变量赋予跟初始值不同的值。

错误示范

1
2
3
4
5
var a;  //隐式类型的局部变量必须已经初始化
var b = {1,2,3}; //1、无法用数组的初始值 初始化隐式类型的局部变量
//2、只能使用数组初始值表达式为数组类型赋值,尝试使用new表达式。

var e = null; //无法将null赋予隐式局部变量

改正

1
2
3
var a= 2020
var b = new int[]{1,2,3};
var e = "广东";