WPF内存优化
内存监测软件
Ants Memory Profiler
下载地址: https://pan.baidu.com/s/1nLF6njntaVgrXVdIaT1mOw 提取码: phsy
使用方法:https://www.cnblogs.com/jingridong/p/6385661.html
dotMemory
snoop
官网:https://chocolatey.org/packages/snoop
安装choco
使用管理员权限打开 Powershell
1 2 | Set-ExecutionPolicy RemoteSigned iwr https://chocolatey.org/install.ps1 -UseBasicParsing | iex |
安装snoop
1 | choco install snoop –pre |
升级
1 | choco upgrade snoop –pre |
卸载
1 | choco 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 { // ………… } |
线程
页面关闭时没结束的线程要结束线程
静态变量
页面关闭时静态变量要设置为空
事件
使用事件时,如果是一个类的事件在另一个类里面被注册(委托方法在这个类里面),要注销事件
1 | Window1.w2.TextBox1.TextChanged += new TextChangedEventHandler(this.TextBox1_TextChanged); |
注销
1 | Window1.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,可能导致内存泄漏。
1 | public 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
,开销最小,但这样就导致了一些编程时的麻烦,即未定义样式,就不能引用样式,哪怕定义在后,引用在前都不行。

慎用隐式类型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
比如
1 | public static List<String> myMothod(){} |
请改成
1 | public 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 )
这是官方给的说明,看来在样式和数据绑定部分下了点工夫啊:
- 运行一个包含样式或模板,请参阅通过使用 StaticResource 标记扩展或 DynamicResource 标记扩展应用程序资源的 WPF 应用程序。 创建使用这些样式或模板的多个控件。 但是,这些控件不使用引用的资源。 在这种情况的一些内存WeakReference对象和空间泄漏的控股数组后,垃圾回收释放该控件。
- 运行一个包含的控件的属性是数据绑定到的 WPF 应用程序DependencyObject对象。 该对象的生存期是超过控件的生存期。 许多控件时创建,一些内存WeakReference对象和容纳数组空格被泄漏后垃圾回收释放该控件。
- 运行使用树视图控件或控件派生于的 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); } } |
其中
1 | GC.WaitForPendingFinalizers(); |
作用:
这个方法简单的挂起执行线程,直到Freachable队列中的清空之后,执行完所有队列中的Finalize方法之后才继续执行。
用法:只需要在你希望释放的时候调用FlushMemory()
即可
事实上,使用该函数并不能提高什么性能,也不会真的节省内存。
因为他只是暂时的将应用程序占用的内存移至虚拟内存,一旦,应用程序被激活或者有操作请求时,这些内存又会被重新占用。
如果你强制使用该方法来 设置程序占用的内存,那么可能在一定程度上反而会降低系统性能,因为系统需要频繁的进行内存和硬盘间的页面交换。
1 2 3 4 5 | BOOL SetProcessWorkingSetSize( HANDLE hProcess, SIZE_T dwMinimumWorkingSetSize, SIZE_T dwMaximumWorkingSetSize ); |
将 2个 SIZE_T
参数设置为 -1
,即可以使进程使用的内存交换到虚拟内存,只保留一小部分内存占用。
因为使用了定时器,不停的进行该操作,所以性能可想而知,虽然换来了小内存的假象,对系统来说确实灾难。
当然,该函数也并非无一是处:
- 当我们的应用程序刚刚加载完成时,可以使用该操作一次,来将加载过程不需要的代码放到虚拟内存,这样,程序加载完毕后,保持较大的可用内存。
- 程序运行到一定时间后或程序将要被闲置时,可以使用该命令来交换占用的内存到虚拟内存。
注意
这种方式为缓兵之计,物理内存中的数据转移到了虚拟内存中,当内存达到一定额度后还是会崩溃。
Lierda.WPFHelper
输入下面代码,安装 Lierda.WPFHelper
1 | Install-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。
那么怎样让程序使用更多的内存呢?

安装时注意勾选红框对应的模块
编辑一个程序使之声明支持大于 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 |
优化 WPF 应用程序性能
WPF程序性能由很多因素造成,以下是简单地总结:
官方
元素
- 减少需要显示的元素数量:去除不需要或者冗余的XAML元素代码. 通过移出不必要的元素,合并layout panels,简化templates来减少可视化树的层次。这可以保证低内存使用,而改变渲染性能。
- UI虚拟化:只显示当前需要显示的元素.
- 不要把不要显示的自定义控件隐藏在主界面中:虽然它们不会显示出来,但是程序启动时还是会去计算自定义控件所需的空间和位置.
- VirtualizingStackPanel:对Item类型控件重写时,使用VirtualizingStackPanel作为ItemPanel,这样列表资源可以只渲染当前需要的内容。不过如果设置CanContextScrol=”True”会阻止虚拟化,另外使用VirtualizingStackPanel时,可以设置
VirtualizingStackPanel.VirtualizationMode="Recycling"
, 这样已经显示过的列表不会被重复创建和释放掉。 - 冻结可以冻结的控件:通过在代码中调用Freeze()或者在Xmal中设定PresentationOptions:Freeze=”true”来冻结可以冻结的控件。由于这样系统不必监听该控件的变化,所以可以带来性能的提升.
- 尽可能使用
StreamGeometries
代替PathGeometries
:因为它可以降低内存占用,更高效. - 尽量多使用Canvas等简单的布局元素:少使用Grid或者StackPanel等复杂的,越复杂性能开销越大
- 尽量不要使用
ScrollBarVisibility=Auto
- 如果需要修改元素的Opacity属性,最后修改一个Brush的属性,然后用这个Brush来填充元素。因为直接修改元素的Opacity会迫使系统创建一个临时的Surface
- 使用延迟滚动增强用户体验:如果你还记得可滚动的DataGrid或ListBox,它们往往会降低整个应用程序的性能,因为在滚动时会强制连续更新,这是默认的行为,在这种情况下,我们可以使用控件的延迟滚动(Deferred Scrolling)属性增强用户体验。你需要做的仅仅是将
IsDeferredScrollingEnabled
附加属性设为True
- 使用容器回收提高性能: 你可以通过回收执行虚拟化的容器来提高性能,将
ViruatlizationMode
设为Recycling
,它让你可以获得更好的性能。当用户滚动或抵达另一个项目时,它强制重复使用容器对象。
线程
- 耗时操作放在放在非UI线程上处理,保持UI的顺畅:处理完成后如果需要在UI上展示,调用
Dispatcher.BeginInoke()
方法
绑定
- Mode:关于Data Binding,根据实际情况对Binding指定不同的Mode,性能比较:
OneTime
>OneWay
>TwoWay
。 - 修正系统中Binding错误:在Visual Studio的输出日志中查找System.Windows.Data Error。
- 在使用数据绑定的过程中,如果绑定的数据源是一个CLR对象,属性也是一个CLR属性,那么在绑定的时候对象CLR对象所实现的机制不同,绑定的效率也不同。
- 访问CLR对象和CLR属性的效率会比访问DependencyObject/DependencyProperty高。注意这里指的是访问,不要和前面的绑定混淆了。但是,把属性注册为DependencyProperty会有很多的优点:比如继承、数据绑定和Style。
- 数据源是一个CLR对象,属性也是一个CLR属性。对象通过TypeDescriptor/PropertyChanged模式实现通知功能。此时绑定引擎用TypeDescriptor来反射源对象。效率最低。
- 数据源是一个CLR对象,属性也是一个CLR属性。对象通过INotifyPropertyChanged实现通知功能。此时绑定引擎直接反射源对象。效率稍微提高。
- 数据源是一个DependencyObject,而且属性是一个DependencyProperty。此时不需要反射,直接绑定。效率最高。
- 当一个CLR对象很大时,比如有1000个属性时,尽量把这个对象分解成很多很小的CLR对象。比如分成1000个只有一个属性的CLR对象。
- 当我们在列表(比如ListBox)显示了一个CLR对象列表(比如List)时,如果想在修改List对象后,ListBox也动态的反映这种变化。此时,我们应该使用动态的ObservableCollection对象绑定。而不是直接的更新ItemSource。两者的区别在于直接更新ItemSource会使WPF抛弃ListBox已有的所有数据,然后全部重新从List加载。而使用ObservableCollection可以避免这种先全部删除再重载的过程,效率更高。
- 尽量绑定IList而不是IEnumerable到ItemsControl。
资源
- 通常情况下我们会把样式资源都统一到
App.xaml
中,这是很好的,便于资源的管理。 - 尽量把多次重复用到的资源放到App.xaml中。例如某些页面的资源只会在当前页面中使用到,那么可以把资源定义在当前页面, 因为放在控件中会使每个实例都保留一份资源的拷贝。
- 如非必要,不要使用
DynaicResource
,使用StaticResource
即可;
即:
1 | <Window.Resources></Window.Resources> |
中的页面通用样式放在
1 | <Application.Resources></Application.Resources> |
动画
- 尽量少的使用Animation:程序启动时,Animation渲染时会占用一些CPU资源。
- 降低动画的帧率:大多数动画不需要高帧率,而系统默认为60frames/sec,所以可以设定Storyboard.DesiredFrameRate 为更低值。
- 使用卸载事件卸载不必要的动画:动画肯定会占用一定的资源,如果处置方式不当,将会消耗更多的资源,如果你认为它们无用时,你应该考虑如何处理他们,如果不这样做,就要等到可爱的垃圾回收器先生来回收资源。
图像
- 对Image做动画处理的时候(如调整大小等),可以使用这条语句
RenderOptions.SetBitmapScalingMode(MyImage,BitmapScalingMode.LowQuality)
,以改善性能。 - 用
TileBrush
的时候,可以CachingHint
。 - 预测图像绘制能力:根据硬件配置的不同,WPF采用不同的Rendering Tier做渲染。下列情况请特别注意,因为在这些情况下,即使是处于Rendering Tier 2的情况下也不会硬件加速。
文本
- 文字少的时候用TextBlock或者label,长的时候用FlowDocument.
- 使用元素
TextFlow
和TextBlock
时,如果不需要TextFlow
的某些特性,就应该考虑使用TextBlock
,因为它的效率更高。 - 在TextFlow中使用UIElement(比如TextBlock)所需的代价要比使用TextElement(比如Run)的代价高.在FlowDocument中尽量避免使用TextBlock,要用Run替代。
- 在TextBlock中显式的使用Run命令比不使用Run命名的代价要高。
- 把Label(标签)元素的ContentProperty和一个字符串(String)绑定的效率要比把字符串和TextBlock的Text属性绑定的效率低。因为Label在更新字符串是会丢弃原来的字符串,全部重新显示内容。如果字符串不需要更新,用Label就无所谓性能问题。
- 在TextBlock块使用HyperLinks时,把多个HyperLinks组合在一起效率会更高。
- 显示超链接的时候,尽量只在IsMouseOver为True的时候显示下划线,一直显示下划线的代码高很多
- 尽量不使用不必要的字符串连接。
- 使用字体缓存服务提高启动时间:WPF应用程序之间可以共享字体数据,它是通过一个叫做PresentationFontCache Service的Windows服务实现的,它会随Windows自动启动。你可以在控制面板的“服务”中找到这个服务(或在“运行”框中输入
services.msc
),确保这个服务已经启动。
其他
- 用NavigationWindow的时候,尽量
Update the client area by object
,而不是URI - 建立逻辑树或者视觉树的时候,遵循Top-Down的原则
- 使用WPF分析工具分析WPF程序:分析WPF程序是理解其行为很重要的一步,市场上有大量现成的WPF程序分析工具,如
Snoop
,WPFPerf
,Perforator
和Visual Profiler
,其中Perforator和Visual Profiler是WPF Performance Suite的一部分,要了解这些工具的用法,请去它们的项目主页。
原文链接:https://www.psvmc.cn/article/2020-08-17-wpf-memory.html