内存泄露主要原因分析:
对于静态对象尽量小或者不用,非托管资源可通过手动Dispose来释放。
Ants Memory Profiler
下载地址: https://pan.baidu.com/s/1nLF6njntaVgrXVdIaT1mOw 提取码: phsy
使用方法:https://www.cnblogs.com/jingridong/p/6385661.html
如果用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属性值之类或者引用类属性时,要手动删除并设置为空
CustomControl cc = new CustomControl();
BitMapSource bms = new BitMapSource();
bms.UriSource = xxx;
cc.Image = new Image(){ Source= bms };
清除引用:
cc.Image= null;
bms = null;
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);
用静态事件时要注销事件
在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,可能导致内存泄漏。
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)
);
}
共享的方式最简单不过的就是建立一个类库项目,把样式、图片、笔刷什么的,都扔进去,样式引用最好使用StaticResource
,开销最小,但这样就导致了一些编程时的麻烦,即未定义样式,就不能引用样式,哪怕定义在后,引用在前都不行。
这个本来应该感觉没什么问题的,可是不明的是,在实践中,发现大量采用var与老老实实的使用类型声明的弱引用对比,总是产生一些不能正确回收的WeakRefrense(这点有待探讨,因为开销不是很大,可能存在一些手工编程的问题)
官方示例: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实现
一个图片跟几行代码相比,哪个开销更少肯定不用多说了,而且这几行代码还可以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}"/>
静态方法返回诸如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 )
这是官方给的说明,看来在样式和数据绑定部分下了点工夫啊:
后续更新的三个补丁,详细的请百度:
都是NET4的补丁,在发布程序的时候,把这些补丁全给客户安装了会好的多。
// 不推荐
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
,即可以使进程使用的内存交换到虚拟内存,只保留一小部分内存占用。
因为使用了定时器,不停的进行该操作,所以性能可想而知,虽然换来了小内存的假象,对系统来说确实灾难。
当然,该函数也并非无一是处:
注意
这种方式为缓兵之计,物理内存中的数据转移到了虚拟内存中,当内存达到一定额度后还是会崩溃。
使用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 缓存选项保留对流的访问,直至需要位图并且垃圾回收器执行清理为止。