轉自:http://www.cnblogs.com/LastPropose/archive/2011/08/01/2124359.html
一直以來用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 )
這是官方給的說明,看來在樣式和數據綁定部分下了點工夫啊:
- 運行一個包含樣式或模板,請參閱通過使用 StaticResource 標記擴展或 DynamicResource 標記擴展應用程序資源的 WPF 應用程序。 創建使用這些樣式或模板的多個控件。 但是,這些控件不使用引用的資源。 在這種情況的一些內存WeakReference對象和空間泄漏的控股數組后,垃圾回收釋放該控件。
- 運行一個包含的控件的屬性是數據綁定到的 WPF 應用程序DependencyObject對象。 該對象的生存期是超過控件的生存期。 許多控件時創建,一些內存WeakReference對象和容納數組空格被泄漏后垃圾回收釋放該控件。
- 運行使用樹視圖控件或控件派生於的 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進行輸出就可以了。