WPF内存优化

内存监测软件

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

安装choco

使用管理员权限打开 Powershell

1
2
Set-ExecutionPolicy RemoteSigned
iwr https://chocolatey.org/install.ps1 -UseBasicParsing | iex

安装snoop

1choco install snoop –pre

升级

1choco upgrade snoop –pre

卸载

1choco uninstall snoop –pre

内存泄露原因

内存泄露主要原因分析:

  • 静态引用
  • 未注销的事件绑定
  • 非托管代码资源使用等

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

内存泄漏注意点

MVVM

如果用MVVM模式,View里面有图片,ViewModel里面有View引用,要把ViewModel里面的View设置为空,View里面的DataContext设置为空,不然有可能导致内存泄漏

清除引用:

1
2
this.Page.DataContext = null;
this.Page = 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;

Image、BitMapSource

自定义控件里面有Image、BitMapSource属性值之类或者引用类属性时,要手动删除并设置为空

1
2
3
4
CustomControl cc = new CustomControl();
BitMapSource bms = new BitMapSource();
bms.UriSource = xxx;
cc.Image = new Image(){ Source= bms };

清除引用:

1
2
cc.Image= null;
bms = null;

INotifyPropertyChanged

MVVM模式里面继承INotifyPropertyChanged的ViewModel里面的命令(用CommandManager.RegisterClassCommandBinding)有可能导致内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protected ICommand CreateCommand(
Action<ExecutedRoutedEventArgs> executed,
Action<CanExecuteRoutedEventArgs> canExecute
)
{
var rCommand = new RoutedCommand();
var cBinding = new CommandBinding(rCommand);
CommandManager.RegisterClassCommandBinding(typeof(UIElement), cBinding);

cBinding.CanExecute += (s, e) =>
{
if (canExecute != null)
canExecute(e);
else
e.CanExecute = true;
};

cBinding.Executed += (s, e) =>
{
executed(e);
};
return rCommand;
}

修改成:

1
2
3
4
5
6
7
8
9
protected ICommand CreateCommand(Action<object> execute)
{
return new RelayCommand(execute);
}

public class RelayCommand : Icommand
{
// …………
}

线程

页面关闭时没结束的线程要结束线程

静态变量

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

事件

使用事件时,如果是一个类的事件在另一个类里面被注册(委托方法在这个类里面),要注销事件

1Window1.w2.TextBox1.TextChanged += new TextChangedEventHandler(this.TextBox1_TextChanged);

注销

1Window1.w2.TextBox1.TextChanged -= new TextChangedEventHandler(this.TextBox1_TextChanged);

静态事件

用静态事件时要注销事件

BitmapImage

加载项目内图片

在Image里面使用BitMapImage时要用

1
2
3
4
5
6
7
8
9
BitmapImage bi = new BitmapImage();
if (File.Exists(imagePath))
{
bi.BeginInit();
bi.CacheOption = BitmapCacheOption.OnLoad;
bi.UriSource = new Uri(path);
bi.EndInit();
bi.Freeze();
}

加载本地绝对路径图片

使用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 bitmap = new BitmapImage();
if (File.Exists(imagePath))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
using (Stream ms = new MemoryStream(File.ReadAllBytes(imagePath)))
{
bitmap.StreamSource = ms;
bitmap.EndInit();
bitmap.Freeze();
}
}
return bitmap;
}

使用时直接通过调用此方法获得Image后立马释放掉资源

1
2
3
ImageBrush berriesBrush = new ImageBrush();
berriesBrush.ImageSource = GetImage(path); //path为图片的路径
this.Background = berriesBrush;

注意

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

调用垃圾回收

1
2
3
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

INotifyPropertyChanged

如果绑定的数据源没有实现INotifyPropertyChanged,可能导致内存泄漏。

1public class CustomCollectionClass : INotifyPropertyChanged {}

在 WPF 中,不标记为 OneTime 必须侦听属性的一个数据绑定操作从源对象 (对象 X) 更改通知。

WPF 从 INotifyPropertyChanged 界面使用 DependencyProperties 类的内置通知。

如果 DependencyProperties 类和 INotifyPropertyChanged 接口都不可用,WPF 使用 ValueChanged 事件。

此行为涉及到与属性 P 相对应的 PropertyDescriptor 对象上调用 PropertyDescriptor.AddValueChanged 方法。

遗憾的是,此操作会导致公共语言运行库 (CLR) 可以创建从此 PropertyDescriptor 对象 X 的强引用。

CLR 还保留全局表中的 PropertyDescriptor 对象的引用。

DataContext

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

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

优化内存占用的方式

使用依赖属性

我们通过依赖属性和普通的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)175.0)
);
}

WPF样式模板请共享

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

img

慎用隐式类型var的弱引用

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

Dispose

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

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

我是这么做的:

接口约束

1
2
3
4
5
6
7
8
9
10
11
12
interface IUIElement : IDisposable
{
/// <summary>
/// 注册事件
/// </summary>
void EventsRegistion();

/// <summary>
/// 解除事件注册
/// </summary>
void EventDeregistration();
}

在实现上可以这样:

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
public void EventsRegistion()
{
this.traineeReport.SelectionChanged += new SelectionChangedEventHandler(traineeReport_SelectionChanged);
}

public void EventDeregistration()
{
this.traineeReport.SelectionChanged -= new SelectionChangedEventHandler(traineeReport_SelectionChanged);
}

~TraineePaymentMgr()
{
ConsoleEx.Log(“{0}被销毁”, this);
Dispose(false);
}

public void Dispose()
{
ConsoleEx.Log(“{0}被手动销毁”, this);
Dispose(true);
GC.SuppressFinalize(this);
}

private bool disposed;
protected void Dispose(bool disposing)
{
ConsoleEx.Log(“{0}被自动销毁”, this);
if(!disposed)
{
if(disposing)
{
//托管资源释放
((IDisposable)traineeReport).Dispose();
((IDisposable)traineePayment).Dispose();
}
//非托管资源释放
// …
}
disposed = true;
}

比如写一个UserControl或是一个Page时,可以参考以上代码,实现这样接口,有利于资源释放。

定时回收垃圾

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
DispatcherTimer GCTimer = new DispatcherTimer();
public MainWindow()
{
InitializeComponent();
//垃圾释放定时器 我定为每十分钟释放一次,大家可根据需要修改
this.GCTimer.Interval = TimeSpan.FromMinutes(10);
this.GCTimer.start();
// 注册事件
this.EventsRegistion();
}

public void EventsRegistion()
{
this.GCTimer.Tick += new EventHandler(OnGarbageCollection);
}

public void EventDeregistration()
{
this.GCTimer.Tick -= new EventHandler(OnGarbageCollection);
}

void OnGarbageCollection(object sender, EventArgs e)
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}

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}”/>

使用out

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

比如

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

请改成

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

string拼接

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分析效果显著,不仅提升了性能,而且垃圾也少了。

检查冗余的代码

使用Blend做样式的时候,一定要检查冗余的代码

众所周知,Blend定义样式时,产生的垃圾代码还是比较多的,如果使用Blend,一定要检查生成的代码。

打微软补丁

NET4的内存泄露补丁地址,下载点这里 (QFE: Hotfix request to implement hotfix KB981107 in .NET 4.0 )

这是官方给的说明,看来在样式和数据绑定部分下了点工夫啊:

  1. 运行一个包含样式或模板,请参阅通过使用 StaticResource 标记扩展或 DynamicResource 标记扩展应用程序资源的 WPF 应用程序。 创建使用这些样式或模板的多个控件。 但是,这些控件不使用引用的资源。 在这种情况的一些内存WeakReference对象和空间泄漏的控股数组后,垃圾回收释放该控件。
  2. 运行一个包含的控件的属性是数据绑定到的 WPF 应用程序DependencyObject对象。 该对象的生存期是超过控件的生存期。 许多控件时创建,一些内存WeakReference对象和容纳数组空格被泄漏后垃圾回收释放该控件。
  3. 运行使用树视图控件或控件派生于的 WPF 应用程序,选择器类。 将控件注册为控制中的键盘焦点的内部通知在KeyboardNavigation类。 该应用程序创建这些控件的很多。 例如对于您添加并删除这些控件。 在本例中为某些内存WeakReference对象和容纳数组空格被泄漏后垃圾回收释放该控件。

后续更新的三个补丁,详细的请百度:

都是NET4的补丁,在发布程序的时候,把这些补丁全给客户安装了会好的多。

日志输出

对于调试信息的输出,我的做法是在窗体应用程序中附带一个控制台窗口,输出调试信息,给一个类,方便大家:

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace Trainee.UI.UIHelper
{
public struct COORD
{
public ushort X;
public ushort Y;
};

public struct CONSOLE_FONT
{
public uint index;
public COORD dim;
};

public static class ConsoleEx
{
[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport(“kernel32”, CharSet = CharSet.Auto)]
internal static extern bool AllocConsole();

[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport(“kernel32”, CharSet = CharSet.Auto)]
internal static extern bool SetConsoleFont(IntPtr consoleFont, uint index);

[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport(“kernel32”, CharSet = CharSet.Auto)]
internal static extern bool GetConsoleFontInfo(IntPtr hOutput, byte bMaximize, uint count, [In, Out] CONSOLE_FONT[] consoleFont);

[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport(“kernel32”, CharSet = CharSet.Auto)]
internal static extern uint GetNumberOfConsoleFonts();

[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport(“kernel32”, CharSet = CharSet.Auto)]
internal static extern COORD GetConsoleFontSize(IntPtr HANDLE, uint DWORD);

[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport(“kernel32.dll “)]
internal static extern IntPtr GetStdHandle(int nStdHandle);

[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport(“kernel32.dll”, CharSet = CharSet.Auto, SetLastError = true)]
internal static extern int GetConsoleTitle(String sb, int capacity);

[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport(“user32.dll”, EntryPoint = “UpdateWindow”)]
internal static extern int UpdateWindow(IntPtr hwnd);

[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport(“user32.dll”)]
internal static extern IntPtr FindWindow(String sClassName, String sAppName);

public static void OpenConsole()
{
var consoleTitle = “> Debug Console”;
AllocConsole();


Console.BackgroundColor = ConsoleColor.Black;
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WindowWidth = 80;
Console.CursorVisible = false;
Console.Title = consoleTitle;
Console.WriteLine(“DEBUG CONSOLE WAIT OUTPUTING…{0} {1}\n”, DateTime.Now.ToLongTimeString());
}

public static void Log(String format, params object[] args)
{
Console.WriteLine(“[” + DateTime.Now.ToLongTimeString() + “] ” + format, args);
}
public static void Log(Object arg)
{
Console.WriteLine(arg);
}
}
}

在程序启动时,可以用ConsoleEx.OpenConsole()打开控制台,用ConsoleEx.Log(.....)或者干脆用Console.WriteLine进行输出就可以了。

使用虚拟内存

内存重新分配

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[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);
}
}

其中

1GC.WaitForPendingFinalizers();

作用:

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

用法:只需要在你希望释放的时候调用FlushMemory()即可

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

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

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

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

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

注意

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

Lierda.WPFHelper

输入下面代码,安装 Lierda.WPFHelper

1Install-Package Lierda.WPFHelper -Version 1.0.3

安装完成后在程序启动的地方加入下在代码

1
2
3
4
5
6
7
8
9
public partial class App : Application
{
LierdaCracker cracker = new LierdaCracker();
protected override void OnStartup(StartupEventArgs e)
{
cracker.Cracker();
base.OnStartup(e);
}
}

这个设置可以让程序隔一段时间自动回收内存

增大可用内存

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

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

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

image-20200818084950845

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

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

1editbin /largeaddressaware xhschool.exe

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

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

1dumpbin /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

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

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

添加如下命令

1editbin.exe /largeaddressaware xhschool.exe

优化 WPF 应用程序性能

WPF程序性能由很多因素造成,以下是简单地总结:

官方

https://docs.microsoft.com/zh-cn/dotnet/framework/wpf/advanced/optimizing-wpf-application-performance

元素

  1. 减少需要显示的元素数量:去除不需要或者冗余的XAML元素代码. 通过移出不必要的元素,合并layout panels,简化templates来减少可视化树的层次。这可以保证低内存使用,而改变渲染性能。
  2. UI虚拟化:只显示当前需要显示的元素.
  3. 不要把不要显示的自定义控件隐藏在主界面中:虽然它们不会显示出来,但是程序启动时还是会去计算自定义控件所需的空间和位置.
  4. VirtualizingStackPanel:对Item类型控件重写时,使用VirtualizingStackPanel作为ItemPanel,这样列表资源可以只渲染当前需要的内容。不过如果设置CanContextScrol=”True”会阻止虚拟化,另外使用VirtualizingStackPanel时,可以设置VirtualizingStackPanel.VirtualizationMode="Recycling", 这样已经显示过的列表不会被重复创建和释放掉。
  5. 冻结可以冻结的控件:通过在代码中调用Freeze()或者在Xmal中设定PresentationOptions:Freeze=”true”来冻结可以冻结的控件。由于这样系统不必监听该控件的变化,所以可以带来性能的提升.
  6. 尽可能使用StreamGeometries 代替PathGeometries:因为它可以降低内存占用,更高效.
  7. 尽量多使用Canvas等简单的布局元素:少使用Grid或者StackPanel等复杂的,越复杂性能开销越大
  8. 尽量不要使用ScrollBarVisibility=Auto
  9. 如果需要修改元素的Opacity属性,最后修改一个Brush的属性,然后用这个Brush来填充元素。因为直接修改元素的Opacity会迫使系统创建一个临时的Surface
  10. 使用延迟滚动增强用户体验:如果你还记得可滚动的DataGrid或ListBox,它们往往会降低整个应用程序的性能,因为在滚动时会强制连续更新,这是默认的行为,在这种情况下,我们可以使用控件的延迟滚动(Deferred Scrolling)属性增强用户体验。你需要做的仅仅是将IsDeferredScrollingEnabled附加属性设为True
  11. 使用容器回收提高性能: 你可以通过回收执行虚拟化的容器来提高性能,将ViruatlizationMode设为Recycling,它让你可以获得更好的性能。当用户滚动或抵达另一个项目时,它强制重复使用容器对象。

线程

  1. 耗时操作放在放在非UI线程上处理,保持UI的顺畅:处理完成后如果需要在UI上展示,调用Dispatcher.BeginInoke()方法

绑定

  1. Mode:关于Data Binding,根据实际情况对Binding指定不同的Mode,性能比较:OneTime>OneWay>TwoWay
  2. 修正系统中Binding错误:在Visual Studio的输出日志中查找System.Windows.Data Error。
  3. 在使用数据绑定的过程中,如果绑定的数据源是一个CLR对象,属性也是一个CLR属性,那么在绑定的时候对象CLR对象所实现的机制不同,绑定的效率也不同。
  4. 访问CLR对象和CLR属性的效率会比访问DependencyObject/DependencyProperty高。注意这里指的是访问,不要和前面的绑定混淆了。但是,把属性注册为DependencyProperty会有很多的优点:比如继承、数据绑定和Style。
  5. 数据源是一个CLR对象,属性也是一个CLR属性。对象通过TypeDescriptor/PropertyChanged模式实现通知功能。此时绑定引擎用TypeDescriptor来反射源对象。效率最低。
  6. 数据源是一个CLR对象,属性也是一个CLR属性。对象通过INotifyPropertyChanged实现通知功能。此时绑定引擎直接反射源对象。效率稍微提高。
  7. 数据源是一个DependencyObject,而且属性是一个DependencyProperty。此时不需要反射,直接绑定。效率最高。
  8. 当一个CLR对象很大时,比如有1000个属性时,尽量把这个对象分解成很多很小的CLR对象。比如分成1000个只有一个属性的CLR对象。
  9. 当我们在列表(比如ListBox)显示了一个CLR对象列表(比如List)时,如果想在修改List对象后,ListBox也动态的反映这种变化。此时,我们应该使用动态的ObservableCollection对象绑定。而不是直接的更新ItemSource。两者的区别在于直接更新ItemSource会使WPF抛弃ListBox已有的所有数据,然后全部重新从List加载。而使用ObservableCollection可以避免这种先全部删除再重载的过程,效率更高。
  10. 尽量绑定IList而不是IEnumerable到ItemsControl。

资源

  1. 通常情况下我们会把样式资源都统一到App.xaml中,这是很好的,便于资源的管理。
  2. 尽量把多次重复用到的资源放到App.xaml中。例如某些页面的资源只会在当前页面中使用到,那么可以把资源定义在当前页面, 因为放在控件中会使每个实例都保留一份资源的拷贝。
  3. 如非必要,不要使用DynaicResource,使用StaticResource即可;

即:

1<Window.Resources></Window.Resources>

中的页面通用样式放在

1<Application.Resources></Application.Resources>

动画

  1. 尽量少的使用Animation:程序启动时,Animation渲染时会占用一些CPU资源。
  2. 降低动画的帧率:大多数动画不需要高帧率,而系统默认为60frames/sec,所以可以设定Storyboard.DesiredFrameRate 为更低值。
  3. 使用卸载事件卸载不必要的动画:动画肯定会占用一定的资源,如果处置方式不当,将会消耗更多的资源,如果你认为它们无用时,你应该考虑如何处理他们,如果不这样做,就要等到可爱的垃圾回收器先生来回收资源。

图像

  1. 对Image做动画处理的时候(如调整大小等),可以使用这条语句RenderOptions.SetBitmapScalingMode(MyImage,BitmapScalingMode.LowQuality),以改善性能。
  2. TileBrush的时候,可以CachingHint
  3. 预测图像绘制能力:根据硬件配置的不同,WPF采用不同的Rendering Tier做渲染。下列情况请特别注意,因为在这些情况下,即使是处于Rendering Tier 2的情况下也不会硬件加速。

文本

  1. 文字少的时候用TextBlock或者label,长的时候用FlowDocument.
  2. 使用元素TextFlowTextBlock时,如果不需要TextFlow的某些特性,就应该考虑使用TextBlock,因为它的效率更高。
  3. 在TextFlow中使用UIElement(比如TextBlock)所需的代价要比使用TextElement(比如Run)的代价高.在FlowDocument中尽量避免使用TextBlock,要用Run替代。
  4. 在TextBlock中显式的使用Run命令比不使用Run命名的代价要高。
  5. 把Label(标签)元素的ContentProperty和一个字符串(String)绑定的效率要比把字符串和TextBlock的Text属性绑定的效率低。因为Label在更新字符串是会丢弃原来的字符串,全部重新显示内容。如果字符串不需要更新,用Label就无所谓性能问题。
  6. 在TextBlock块使用HyperLinks时,把多个HyperLinks组合在一起效率会更高。
  7. 显示超链接的时候,尽量只在IsMouseOver为True的时候显示下划线,一直显示下划线的代码高很多
  8. 尽量不使用不必要的字符串连接。
  9. 使用字体缓存服务提高启动时间:WPF应用程序之间可以共享字体数据,它是通过一个叫做PresentationFontCache Service的Windows服务实现的,它会随Windows自动启动。你可以在控制面板的“服务”中找到这个服务(或在“运行”框中输入services.msc),确保这个服务已经启动。

其他

  1. 用NavigationWindow的时候,尽量Update the client area by object,而不是URI
  2. 建立逻辑树或者视觉树的时候,遵循Top-Down的原则
  3. 使用WPF分析工具分析WPF程序:分析WPF程序是理解其行为很重要的一步,市场上有大量现成的WPF程序分析工具,如SnoopWPFPerfPerforatorVisual Profiler,其中Perforator和Visual Profiler是WPF Performance Suite的一部分,要了解这些工具的用法,请去它们的项目主页。
技术交流Q群: 1012481075 群内有各种流行书籍资料
文章后续会在公众号更新,微信搜索 OneByOneDotNet 即可关注。
你的一分鼓励,我的十分动力,点赞免费,感恩回馈。喜欢就点赞评论吧,双击6666~
本网站(网站地址)刊载的所有内容,包括文字、图片、音频、视频、软件、程序、以及网页版式设计等均在网上搜集。
访问者可将本网站提供的内容或服务用于个人学习、研究或欣赏,以及其他非商业性或非盈利性用途,但同时应遵守著作权法及其他相关法律的规定,不得侵犯本网站及相关权利人的合法权利。
除此以外,将本网站任何内容或服务用于其他用途时,须征得本网站及相关权利人的书面许可,并支付报酬.
本网站内容原作者如不愿意在本网站刊登内容,请及时通知本站,予以删除.
转载请注明出处
作者昵称:OneByOneDotNet
作者链接:https://onebyone.icu/user/1
网站标题:OneByOne DotNet
网站地址:https://onebyone.icu
文章标题:WPF内存优化
文章链接:https://onebyone.icu/archives/2146
原文出处:https://www.psvmc.cn/article/2020-08-17-wpf-memory.html
原文链接:https://www.psvmc.cn/article/2020-08-17-wpf-memory.html
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

发表评论

邮箱地址不会被公开。 必填项已用*标注

WeChat
WeChat
QQ
QQ
返回顶部