zoukankan      html  css  js  c++  java
  • WPF中解决内存泄露的几点提示与解决方法

    一直以来用WPF做一个项目,但是开发中途发现内存开销太大,用ANTS Memory Profiler分析时,发现在来回点几次载入页面的操作中,使得非托管内存部分开销从起始的43.59M一直到150M,而托管部分的开销也一直持高不下,即每次申请的内存在结束后不能完全释放。在网上找了不少资料,甚受益,现在修改后,再也不会出现这种现象了(或者说,即使有也不吓人),写下几个小心得:

    1. 慎用WPF样式模板合并

      我发现不采用合并时,非托管内存占用率较小,只是代码的理解能力较差了,不过我们还有文档大纲可以维护。

    2. WPF样式模板请共享

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

    3. 慎用隐式类型var的弱引用

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

    4. 写一个接口约束一下

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

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

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

    在实现上可以这样:

     1 #region IUIElement 成员
    2 public void EventsRegistion()
    3 {
    4 this.traineeReport.SelectionChanged += new SelectionChangedEventHandler(traineeReport_SelectionChanged);
    5 }
    6
    7 public void EventDeregistration()
    8 {
    9 this.traineeReport.SelectionChanged -= new SelectionChangedEventHandler(traineeReport_SelectionChanged);
    10 }
    11
    12 private bool disposed;
    13
    14 ~TraineePaymentMgr()
    15 {
    16 ConsoleEx.Log("{0}被销毁", this);
    17 Dispose(false);
    18 }
    19
    20 public void Dispose()
    21 {
    22 ConsoleEx.Log("{0}被手动销毁", this);
    23 Dispose(true);
    24 GC.SuppressFinalize(this);
    25 }
    26
    27 protected void Dispose(bool disposing)
    28 {
    29 ConsoleEx.Log("{0}被自动销毁", this);
    30 if(!disposed)
    31 {
    32 if(disposing)
    33 {
    34 //托管资源释放
    35 ((IDisposable)traineeReport).Dispose();
    36 ((IDisposable)traineePayment).Dispose();
    37 }
    38 //非托管资源释放
    39 }
    40 disposed = true;
    41 }
    42 #endregion

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

    5. 定时回收垃圾

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

    6. 较简单或可循环平铺的图片用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}"/>

    上面几行代码相当于这个:

    7. 使用Blend做样式的时候,一定要检查完成的代码

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

     

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

    比如

    public static List<String> myMothod()

    {...}

    请改成

    public static myMothod(out List<String> result)

    {...}

     

    9. 打针对此问题的微软补丁

    3.5的应该都有了吧,这里附上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对象和容纳数组空格被泄漏后垃圾回收释放该控件。

    继续更新有关的三个8月补丁,详细的请百度:KB2487367  KB2539634  KB2539636,都是NET4的补丁,在发布程序的时候,把这些补丁全给客户安装了会好的多。

    10.  对string怎么使用的建议

    这个要解释话就长了,下面仅给个例子说明一下,具体的大家去找找MSDN

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

     

    11. 其它用上的技术暂时还没想到,再补充...

     

    如果严格按以上操作进行的话,可以得到一个满意的结果:

    运行了三十分钟,不断的切换功能,然后休息5分钟,回头一看,结果才17M左右内存开销,效果显著吧。

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

    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());

    try
    {
    //这里是改控制台字体大小的,可能会导致异常,在我这个项目中我懒得弄了,如果需要的的话把注释去掉就行了
    //IntPtr hwnd = FindWindow(null, consoleTitle);
    //IntPtr hOut = GetStdHandle(-11);

    //const uint MAX_FONTS = 40;
    //uint num_fonts = GetNumberOfConsoleFonts();
    //if (num_fonts > MAX_FONTS) num_fonts = MAX_FONTS;
    //CONSOLE_FONT[] fonts = new CONSOLE_FONT[MAX_FONTS];
    //GetConsoleFontInfo(hOut, 0, num_fonts, fonts);
    //for (var n = 7; n < num_fonts; ++n)
    //{
    // //fonts[n].dim = GetConsoleFontSize(hOut, fonts[n].index);
    // //if (fonts[n].dim.X == 106 && fonts[n].dim.Y == 33)
    // //{
    // SetConsoleFont(hOut, fonts[n].index);
    // UpdateWindow(hwnd);
    // return;
    // //}
    //}
    }
    catch
    {

    }
    }

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

  • 相关阅读:
    封装成帧、帧定界、帧同步、透明传输(字符计数法、字符串的首尾填充法、零比特填充的首尾标志法、违规编码法)
    计算机网络之数据链路层的基本概念和功能概述
    物理层设备(中继器、集线器)
    计算机网络之传输介质(双绞线、同轴电缆、光纤、无线电缆、微波、激光、红外线)
    计算机网络之编码与调制
    0953. Verifying an Alien Dictionary (E)
    1704. Determine if String Halves Are Alike (E)
    1551. Minimum Operations to Make Array Equal (M)
    0775. Global and Local Inversions (M)
    0622. Design Circular Queue (M)
  • 原文地址:https://www.cnblogs.com/LastPropose/p/2124359.html
Copyright © 2011-2022 走看看