WPF内存优化及性能优化

WPF内存优化及性能优化

内存优化

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

  1. 防止内存泄漏

  2. 增大可用内存

  3. 节省内存

  4. 定时GC

  5. 使用虚拟内存

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

防止内存泄漏

内存泄露原因

内存泄露主要原因分析:

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

  • 非托管代码资源未Dispose

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

DataContext

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

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

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

清除引用:

BitmapImage

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

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

修改加载方式

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

释放

注意

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

静态变量

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

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

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

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

  • 正在运行的线程的实时堆栈。

  • 静态变量。

  • 通过interop传递到COM对象的托管对象(内存回收将通过引用计数来完成)。

GC Root的特征

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

这意味着静态变量及其引用的所有内容都不会被垃圾回收。 这里是一个例子:

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

事件/Events

.NET中的Events因导致内存泄漏而臭名昭著。

原因很简单:订阅事件后,该对象将保留对你的类的引用。 除非你使用不捕获类成员的匿名方法。

解决方法

代码中手动订阅和取消事件

示例1

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

注销

示例2

假设wifiManager的寿命超过MyClass,那么你就已经造成了内存泄漏。 wifiManager会引用MyClass的任何实例,并且垃圾回收器永远不会回收它们。

Event确实很危险

所以,你可以做什么呢? 有几种很好的模式可以防止和Event有关的内存泄漏。 无需详细说明,其中一些是:

  • 注销订阅事件。

  • 使用弱句柄(weak-handler)模式。

  • 如果可能,请使用匿名函数进行订阅,并且不要捕获任何类成员。

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

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

这里是一个例子:

在代码中,类成员_id是在匿名方法中被捕获的,因此该实例也会被引用。

这意味着,尽管JobQueue存在并已经引用了job委托,但它还将引用一个MyClass的实例。

解决方案可能非常简单——分配局部变量:

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

错误的WPF绑定

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

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

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

INotifyPropertyChanged

这里是一个例子:

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

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

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

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

DependencyObject

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

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

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

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

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

替换为

注意

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

永不终止的线程

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

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

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

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

非托管资源

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

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

这里有一个简单的例子。

在上述方法中,我们使用了 Marshal.AllocHGlobal 方法,它分配了非托管内存缓冲区。

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

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

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

由于内存碎片问题,非托管内存泄漏比托管内存泄漏更严重。 垃圾回收器可以移动托管内存,从而为其他对象腾出空间。 但是,非托管内存将永远卡在它的位置。

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

在最后一个示例中,我们添加了Dispose方法以释放所有非托管资源。 这很棒,但是当有人使用了该类却没有调用Dispose时会发生什么呢?

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

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

这非常有用,因为即使抛出异常,也会调用Dispose。

使用析构函数

官方示例:IDisposable.Dispose 方法 (System) | Microsoft Docs

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

析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存)。

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

这种模式可确保即使没有调用Dispose,Dispose也将在实例被垃圾回收时被调用。 另一方面,如果调用了Dispose,则finalizer将被抑制(SuppressFinalize)。 抑制finalizer很重要,因为finalizer开销很大并且会导致性能问题。

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

错误的观点

注意

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

静态方法使用out

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

比如

请改成

这个观点是不对的

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

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

慎用隐式类型var的弱引用

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

这个观点是不对的

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

增大可用内存

注意

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

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

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

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

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

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

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

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

注意到 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

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

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

添加如下命令

节省内存

字符串拼接

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

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

WPF样式模板请共享

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

注意:

  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设置窗口透明只需要设置

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

有个文章介绍了:【翻译】关于 WPF 透明窗口的内存占用 - gandalfliang的个人博客

GeometryDrawing实现简单图片

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

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

这边是重用

检查冗余的代码

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

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

定时GC

使用虚拟内存(不建议)

注意

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

代码

其中

作用:

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

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

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

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

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

  1. 当我们的应用程序刚刚加载完成时,可以使用该操作一次,来将加载过程不需要的代码放到虚拟内存,这样,程序加载完毕后,保持较大的可用内存。

  2. 程序运行到一定时间后或程序将要被闲置时,可以使用该命令来交换占用的内存到虚拟内存。

注意

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

小知识

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

错误示范

改正

内存监测软件

Ants Memory Profiler

下载地址:百度网盘 请输入提取码提取码: phsy

使用方法:WPF性能调试系列 – 内存监测 - want - 博客园

dotMemory

dotMemory: a Memory Profiler & Unit-Testing Framework for .NET by JetBrains

snoop

官网:Chocolatey Software | Snoop 4.0.1

安装choco

使用管理员权限打开 Powershell

安装snoop

升级

卸载

性能优化

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

官方

优化应用性能 - WPF .NET Framework | Microsoft Docs

元素

  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. 尽量少的使用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. 使用元素 TextFlow TextBlock 时,如果不需要 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 ),确保这个服务已经启动。

来源: blog.csdn.net/qq_283680

发布于 2022-09-28 09:42