前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >WPF内存优化

WPF内存优化

作者头像
码客说
发布2020-08-19 11:30:20
1.4K0
发布2020-08-19 11:30:20
举报
文章被收录于专栏:码客码客码客

内存泄露原因

内存泄露主要原因分析:

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

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

内存监测

Ants Memory Profiler

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

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

内存泄漏注意点

MVVM

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

清除引用:

this.Page.DataContext = null;
this.Page = null;

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

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

Class1 class1 =new Class1();
Class2 class2 = new Class2();
class1.Class2 = class2;
class2.Class1 = class1;

清除引用:

class2.Class1 = null;
class2 = null;
class1.Class2 = null;
class1 =null;

Image、BitMapSource

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

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

清除引用:

cc.Image= null;
bms = null;

INotifyPropertyChanged

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

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;

}

修改成:

protected ICommand CreateCommand(Action<object> execute)
{
    return new RelayCommand(execute);
}

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

线程

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

静态变量

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

事件

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

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

注销

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

静态事件

用静态事件时要注销事件

BitmapImage

在Image里面使用BitMapImage时要用

BitmapImage bi = new BitmapImage();
bi.BeginInit();
bi.CacheOption = BitmapCacheOption.OnLoad;
bi.UriSource = new Uri(path);
bi.EndInit();
bi.Freeze();

调用垃圾回收

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

INotifyPropertyChanged

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

public class CustomCollectionClass : INotifyPropertyChanged {}

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

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

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

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

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

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

优化内存占用的方式

使用依赖属性

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

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

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

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

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

public class Student
{
    public string Name { get; set; }
    public double Height { get; set; }
}

替换为

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
img

慎用隐式类型var的弱引用

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

Dispose

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

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

我是这么做的:

接口约束

interface IUIElement : IDisposable
{
    /// <summary>
    /// 注册事件
    /// </summary>
    void EventsRegistion();

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

在实现上可以这样:

public void EventsRegistion()
{
    this.traineeReport.SelectionChanged += new SelectionChangedEventHandler(traineeReport_SelectionChanged);
}

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

private bool disposed;

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

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

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

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

定时回收垃圾

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进行重用。

<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>

这边是重用

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

使用out

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

比如

public static List<String> myMothod(){}

请改成

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

检查冗余的代码

使用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的补丁,在发布程序的时候,把这些补丁全给客户安装了会好的多。

string拼接

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

日志输出

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

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进行输出就可以了。

内存重新分配

代码

[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);
    }
}

其中

GC.WaitForPendingFinalizers();

作用:

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

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

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

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

BOOL SetProcessWorkingSetSize(
  HANDLE hProcess,
  SIZE_T dwMinimumWorkingSetSize,
  SIZE_T dwMaximumWorkingSetSize
);

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

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

注意

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

图片的释放

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

修改加载方式

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后立马释放掉资源

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

注意

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-08-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 内存泄露原因
  • 内存监测
  • 内存泄漏注意点
    • MVVM
      • 类与类之间尽量不要互相引用
        • Image、BitMapSource
          • INotifyPropertyChanged
            • 线程
              • 静态变量
                • 事件
                  • 静态事件
                    • BitmapImage
                      • 调用垃圾回收
                        • INotifyPropertyChanged
                        • 优化内存占用的方式
                          • 使用依赖属性
                            • WPF样式模板请共享
                              • 慎用隐式类型var的弱引用
                                • Dispose
                                  • 定时回收垃圾
                                    • GeometryDrawing实现简单图片
                                      • 使用out
                                        • 检查冗余的代码
                                          • 打微软补丁
                                            • string拼接
                                              • 日志输出
                                              • 内存重新分配
                                              • 图片的释放
                                              领券
                                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档