因為.NET的垃圾回收機制相當完善,通常情況下我們是不需要關心內存泄漏的。問題人一但傻起來,連自己都會害怕,幾個頁面跳啊跳的,內存蹭蹭的往上漲,拉都拉不住。這種時候我們就需要冷靜下來,泡一杯熱巧克力。再打開Visual Studio 2015的Diagnostic Tools,來檢查下到底哪段代碼出了問題。
我們先創建一個簡單的UWP工程,該工程只有2個幾乎為空的Page。MainPage只有兩個按鈕,分別用來跳轉到SecondPage,以及調用GC.Collect()方法。而SecondPage就只有一個Goback用的按鈕,同時在SecondPage的構造函數里創建了一個將近400MB的超大ArrayList。
<Page x:Class="EventMemoryLeak.MainPage" …… > <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" VerticalAlignment="Center"> <Button Click="Button_Click">Go to second page</Button> <Button Click="Button_Click_1">Force GC</Button> </StackPanel> </Page> public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { this.Frame.Navigate(typeof(SecondPage)); } private void Button_Click_1(object sender, RoutedEventArgs e) { GC.Collect(); } } <Page x:Class="EventMemoryLeak.SecondPage" …… > <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Button Click="Button_Click">Go back to main page</Button> </Grid> </Page> public sealed partial class SecondPage : Page { public ArrayList arrayList { get; set; } public SecondPage() { this.InitializeComponent(); arrayList = new ArrayList(100000000); } private void Button_Click(object sender, RoutedEventArgs e) { this.Frame.GoBack(); } }
在Visual Studio 2015中Debug UWP程序時,會自動打開Diagnostic Tools的窗口(沒打開也沒關系,可以通過Debug->Show Diagnostic Tools找到)。
每從MainPage跳轉SecondPage后,內存都會明顯的增加。
在我寫下上面這段話之后,再回到運行中的程序,在MainPage點擊“Force GC“按鈕后,CLR很給面子做了一次徹底的回收,內存占用回到了程序剛打開的狀態。這里需要說明的是,調用GC.Collect方法並不能保證立即回收所有引用計數為0的對象且釋放所有內存。CLR會自己判斷該怎么回收,回收多少,根本就是傲嬌的小公舉。
那是不是說傲嬌的Diagnostic Tools不靠譜呢?非也!首先調用GC.Collect方法,回收是一定會被執行的,必定會有一部分的對象被釋放,這部分變化我們可以通過Snapshot很好的進行觀察(后面會介紹)。其次,如果確實需要進行比較徹底的回收,根據個人經驗,連續調用2到3次GC.Collect方法,效果還是很好的。再傲嬌的小公舉連續收到“在么”的微信,也會回復“呵呵,睡覺了”意思一下的。
接下來我們要故意制造嚴重的內存泄漏,並用Diagnostic Tools來進行觀察。我們增加一個Service層的類,並在SecondPage中監聽Service層的事件。同時我將SecondPage創建的ArraryList從400MB改為40MB,因為我主打輕薄的筆記本性能無法支撐。
public class FakeService { public static FakeService Instance = new FakeService(); public event EventHandler ShowMeTheMoneyEvent; private FakeService() { } } public SecondPage() { this.InitializeComponent(); arrayList = new ArrayList(10000000); FakeService.Instance.ShowMeTheMoneyEvent += Instance_ShowMeTheMoneyEvent; }
這回你會發現,無論你怎么樣GC(怎么感覺這個名字有點污……算了我什么都不知道),內存都不會下降了。這是因為SecondPage被FakeService所引用,FakeService又是靜態的存活於整個APP生命周期的對象,所以SecondPage再也不會被回收釋放了。哎呀我的媽呀……
先別急着叫,用Snapshot在比較一下內存對象,會有更可怕的事情發生。我們重新運行該程序,在第一次運行到MainPage時,做一次Snapshot。反復的打開3次SeconcdPage,再返回MainPage做第二次的SnapShot。
可以看到對象相對於第一次SnapShot僅增加了43個,但Heap Size已經慘不忍睹了。點擊(+43)會打開詳細的對象列表。一般情況下,我會在右上角填寫命名空間來縮小觀察的范圍。我們這里會驚訝的發現SecondPage對象,在3次打開該頁面后,竟然有3份重復的實例存在。
點擊列表中的SecondPage一行,在屏幕下方的窗口中,會顯示Path to Root的相關情況,可以看到SecondPage對象都由EventHandler關聯到了FakeService對象上。
至此,我們通過Diagnostic Tools就找到了內存泄漏的原因,處理方法也很簡單,在離開頁面時,取消對事件的監聽就行了,這里我們可以在頁面的OnNavigateFrom方法里來做。
protected override void OnNavigatedFrom(NavigationEventArgs e) { base.OnNavigatedFrom(e); FakeService.Instance.ShowMeTheMoneyEvent -= Instance_ShowMeTheMoneyEvent; }
本篇我們簡單的討論如何使用Diagnostic Tools來觀察內存對象,並就監聽靜態對象的事件引起的內存泄漏舉例給出了解決方案。希望能夠拋磚引玉,引出許多真知灼見,最不濟您也點個推薦唄。
GayHub:
https://github.com/manupstairs/UWPSamples/tree/master/UWPSamples/EventMemoryLeak