曾經有人問過我,在進行頁面導航的時候,使用 Xamarin.Forms 的專案樣板精靈做出來的專案,並且使用 call behind 後置程式碼的方式,呼叫
Navigation.PushAsync
方法進行頁面導航與使用 Prism 專案樣板精靈做出來的 Xamarin.Forms for Prism 專案,使用 ViewModel 的 INavigationService
來進行頁面導航;其中一個會造成頁面的記憶體洩漏,講白話一點,就是在進行頁面導航操作過程中,頁面無法被記憶體回收程序 Garbage Collection 進行回收,一直殘留在系統裡;若反覆進行這樣的操作,將會造成這個應用程式耗用大量的記憶體,最後終究會使得產生記憶體不足 Memory Overflow 的例外異常問題。
想要知道是否會造成這樣的現象,最簡單的方式,那就是寫個測試專案,並且來跑看看,不過,在 .NET 運作環境下,一個應用程式是無法自己手動來釋放掉變數持有的記憶體空間,而且,沒有被使用到的物件,何時會被釋放掉這些記憶體,也不是程式設計可以來決定的;若你想要更加清楚的瞭解這些機制如何運作,你需要深入去了解 .NET 通用語言執行階段 CLR Common Language Runtime 這個元件的核心運作機制,不過,我們在這裡並不會去介紹這些功能。
準備建立兩個測試專案
好的,在這裡,我們使用 Visual Studio 2017 建立兩個專案
- Xamarin.Forms 跨平台專案檔案 > 新增 > 專案 > Visual C# > Cross-Platform > Cross Platrorm App (Xamarin)這裡所有的頁面導航等商業邏輯,都會使用 Call Behind 的方式來撰寫
- Xamarin.Forms for Prism 專案檔案 > 新增 > 專案 > Visual C# > Prism > Prism Unity App (Xamarin.Forms)這裡所有的頁面導航等商業邏輯,都會使用 MVVM 的方式,在 ViewModel 來撰寫
在這兩個專案內,都會具有底下的頁面導航方式
MainPage > Pg1Page > Pg2Page
其中,在 Pg1Page 與 Pg2Page 這兩個頁面,都提供了導航工具列與頁面按鈕的返回上一頁功能。
下圖是這兩個專案所使用的首頁頁面,其中,GC 按鈕將會驅使 .NET CLR 進行記憶體沒有被參考到的物件之回收作業,而 RESET 按鈕,則是會使用絕對導航的方式,重新設定新的 MainPage 為這個應用程式的第一個首頁頁面。
底下的程式碼為
Xamarin.Forms 跨平台專案
的 MainPage 的 Call Behind 程式碼。public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
private void GotoPage1_Clicked(object sender, EventArgs e)
{
Navigation.PushAsync(new Pg1Page());
}
private void NeedGC_Clicked(object sender, EventArgs e)
{
GC.Collect();
}
private void Reset_Clicked(object sender, EventArgs e)
{
((Application)App.Current).MainPage = new NavigationPage(new App1.MainPage());
}
}
底下的程式碼為
Xamarin.Forms for Prism 專案
的 MainPage 頁面會使用到的 ViewModel 程式碼。public class MainPageViewModel : INotifyPropertyChanged, INavigationAware
{
public event PropertyChangedEventHandler PropertyChanged;
private readonly INavigationService _navigationService;
public DelegateCommand GotoPage1Command { get; set; }
public DelegateCommand GCCommand { get; set; }
public DelegateCommand ResetCommand { get; set; }
public MainPageViewModel(INavigationService navigationService)
{
_navigationService = navigationService;
GotoPage1Command = new DelegateCommand(() =>
{
_navigationService.NavigateAsync("Pg1Page");
});
ResetCommand = new DelegateCommand(() =>
{
_navigationService.NavigateAsync("xf:///NavigationPage/MainPage?title=Hello%20from%20Xamarin.Forms");
});
GCCommand = new DelegateCommand(() =>
{
GC.Collect();
});
}
public void OnNavigatedFrom(NavigationParameters parameters)
{
}
public void OnNavigatingTo(NavigationParameters parameters)
{
}
public void OnNavigatedTo(NavigationParameters parameters)
{
}
}
為了要能夠知道所這些葉面是否有被 CLR GC 將其物件所使用的記憶體回收,我們將會在 Pg1Page / Pg2Pag2 這兩個頁面類別內,分別使用解構函式,顯示出一段訊息,這個訊息,將會於 CLR 要釋放這個頁面物件之前,顯示出來,並且於顯示出來之後,該頁面物件就會隨即於記憶體釋放掉(相關運作方式與原理,請參考通用語言執行階段的相關文件)。
在底下的程式碼,為 Pg1Page 頁面的 Call Behind 的程式碼,我們在這裡宣告一個屬性 Index,該 Index 的屬性值將會於建構式中進行初始化,用來標示出這是第幾個產生的頁面指標。
public partial class Pg1Page : ContentPage
{
public int Index { get; set; }
~Pg1Page()
{
Debug.WriteLine($"----------------- Release Pg1Page [{Index}]");
}
public Pg1Page()
{
InitializeComponent();
Index = GlobalMember.Pg1Count;
GlobalMember.Pg1Count++;
}
}
而
GlobalMember.Pg1Count
這是個靜態變數,定義於底下 GlobalMember 類別中。class GlobalMember
{
public static int Pg1Count { get; set; } = 1;
public static int Pg2Count { get; set; } = 901;
}
進行 Xamarin.Forms 跨平台專案
測試
這時,我們執行
Xamarin.Forms 跨平台專案
專案,並且依照底下流程進行頁面切換
MainPage > Pg1Page > Pg2Page > Pg1Page > MainPage
當操作完成之後,Visual Studio 的輸出視窗內並沒有任何解構函式的輸出內容,此時,我們點選 GC 按鈕,這個時候,就會出現底下訊息;你會看到 Pg2Page頁面物件已經從記憶體中移除了。
----------------- Release Pg2Page [901]
現在,讓我們再度進行同樣的頁面切換
MainPage > Pg1Page > Pg2Page > Pg1Page > MainPage
接著,按下 GC 按鈕,會看到底下內容,這個時候,您將會看到第一次進行頁面切換的 Pg1Page 頁面物件因為沒有物件參考到他,所以,他被記憶體回收了,當然,Pg2Page頁面物件同樣的也被回收了,
----------------- Release Pg2Page [902]
----------------- Release Pg1Page [1]
現在,讓我們再度進行同樣的頁面切換
MainPage > Pg1Page > Pg2Page > Pg1Page > MainPage
接著,按下 RESET 按鈕,接著按下 GC 按鈕,會看到底下內容,此時,所有的子頁面物件就都全部被 GC 釋放掉了
----------------- Release Pg1Page [3]
----------------- Release Pg2Page [903]
----------------- Release Pg1Page [2]
進行 Xamarin.Forms for Prism 專案
測試
這時,我們執行
Xamarin.Forms for Prism 專案
專案,並且依照上面的操作過程,您將會得到一樣的結果,這表示您使用 Prims 框架來建立起來的跨平台行動應用程式,確實會將梅有被參考到的頁面物件予以釋放掉。而最前面所質疑的問題,就獲得解答了。