XamarinForms 系列課程

特別說明

2017/09/29

Xamarin.Forms 頁面導航是否會造成頁面物件記憶體洩漏 Memory Leak

曾經有人問過我,在進行頁面導航的時候,使用 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 為這個應用程式的第一個首頁頁面。
NaviMemLeak1
底下的程式碼為 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 框架來建立起來的跨平台行動應用程式,確實會將梅有被參考到的頁面物件予以釋放掉。而最前面所質疑的問題,就獲得解答了。

測試範例專案

沒有留言:

張貼留言