XAML in Xamarin.Forms 基礎篇 電子書

XAML in Xamarin.Forms 基礎篇 電子書
XAML in Xamarin.Forms 基礎篇 電子書

Xamarin.Forms 快速入門 電子書

Xamarin.Forms 快速入門 電子書
Xamarin.Forms 快速入門 電子書

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 框架來建立起來的跨平台行動應用程式,確實會將梅有被參考到的頁面物件予以釋放掉。而最前面所質疑的問題,就獲得解答了。

測試範例專案

在 Visual Studio 2017 上,安裝 .NET Standard 2.0 標準類別庫支援

了解更多關於 [Xamarin.Android] 的使用方式
了解更多關於 [Xamarin.iOS] 的使用方式
了解更多關於 [Xamarin.Forms] 的使用方式
了解更多關於 [Hello, Android:快速入門] 的使用方式
了解更多關於 [Hello, iOS – 快速入門] 的使用方式
了解更多關於 [Xamarin.Forms 快速入門] 的使用方式

  • 首先,請先將您的 Visual Studio 2107 升級到 15.3 以上的版本
  • 請安裝指定 SDK
  • 請依照指示,繼續安裝
    .NET Core 2.0 Install
    .NET Core 2.0 Install
    .NET Core 2.0 Install
  • 完成安裝

進行檢測

  • 開啟 Visual Studio 2017
  • 選擇功能表 檔案 > 新增 > 專案 > 已安裝 > Visual C# > .NET Standard > 類別庫 (.NET Standard)
    建立一個 .NET Standard 標準類別庫
    .NET Core 2.0 Install
  • 滑鼠右擊該專案的節點,選擇 屬性
    點選 目標 Framework 的下拉選單控制項,從這裡,你就可以切換使用 .NET Standard 2.0
    .NET Core 2.0 Install

2017/09/28

利用 UnhandledException 與 UnobservedTaskException 找出 Xamarin 的 "An unhandled exception occured" 錯誤問題

我們在進行 Xamarin.Forms 專案開發的時候,偶而會遇到這樣的例外錯誤訊息 An unhandled exception occured
unhandled exception1

了解更多關於 [Xamarin.Android] 的使用方式
了解更多關於 [Xamarin.iOS] 的使用方式
了解更多關於 [Xamarin.Forms] 的使用方式
了解更多關於 [Hello, Android:快速入門] 的使用方式
了解更多關於 [Hello, iOS – 快速入門] 的使用方式
了解更多關於 [Xamarin.Forms 快速入門] 的使用方式

可是,從這個畫面上,也看不出問題發生在哪裡,若我們點選 複製詳細資料 ,則會得到這樣的內容:An unhandled exception occured. 發生
結果,還是一樣,無法看到何有幫助的訊息。
在我們這裡,可以使用 C# 內建的類別 應用程式定義域 (Application Domain) 裡面的一個事件 UnhandledException;一旦你訂閱了這個事件,並且應用程式發生了沒有捕捉到的任何例外異常,這個訂閱的事件方法,將會被呼叫執行。
這個事件會接收一個客製化的事件引數類別 UnobservedTaskExceptionEventArgs 我們可以從這個參數中,得到真正發生例外異常的情況。
若想要使用這樣的功能,您需要在 Xamarin 原生專案的進入點加入訂閱這個事件,底下為在 Android 平台專案下的 MainActivity.cs的程式碼使用範例。
protected override void OnCreate(Bundle bundle)
{
    AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
    TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
    TabLayoutResource = Resource.Layout.tabs;
    ToolbarResource = Resource.Layout.toolbar;

    base.OnCreate(bundle);

    global::Xamarin.Forms.Forms.Init(this, bundle);
    LoadApplication(new App(new AndroidInitializer()));
}

private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
    throw new NotImplementedException();
}

private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
    throw new NotImplementedException();
}
所以,你可以在 CurrentDomain_UnhandledException 設定一個中斷點,並且執行這個有問題的專案,若看到 An unhandled exception occured 這個錯誤訊息出現,則按下 F5 繼續來執行,直到在你剛剛設定的中斷點停下來;這個時候,你就可以查看 e 這個參數值,看到問題原因說明了,在這個範例中,我們看到的是 Master and Detail must be set before using a MasterDetailPage
unhandled exception1
unhandled exception1
此時,我們開啟 MasterDetailPage.xaml 檔案,您就會知道問題出在哪裡了。
<?xml version="1.0" encoding="utf-8" ?>
<MasterDetailPage xmlns="http://xamarin.com/schemas/2014/forms"
                  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                  xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
                  prism:ViewModelLocator.AutowireViewModel="True"
                  x:Class="PrismUnityApp4.Views.MDPage">

</MasterDetailPage>

2017/09/25

.NET Framework / PCL 可攜式類別庫 / .NET Standard 標準類別庫 之中繼套件與類型轉送 深入探究

在這裡,我們透過 IL 中繼語言的反組譯工具 ILSpy 來查看這三個 .NET 生態環境的內容。

.NET Framework

我們使用 ILSpy 工具,打開 C:\Windows\Microsoft.NET\Framework\v4.0.30319 目錄,找到 mscorlib.dll這個檔案
接著展開其 mscorlib 節點,接著再展開 System.Collections.Generic 節點,就會看到 List<T> 節點,點擊這個節點,就會看到這個類別的原始 C# 原始程式碼。

PCL 可攜式類別庫

在這裡,我們使用 PCL Profile259 這個版本
我們使用 ILSpy 工具,打開 C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.5\Profile\Profile259\ 目錄,這裡就是 Profile 259 會使用到的類別庫組件所在位置,找到 System.Collections.dll 這個檔案
這個位置,可以從 Visual Studio 中,打開任意一個 PCL 可攜式專案,點選 參考 > .NET 節點,從屬性視窗的 路徑 中,就可以查到,如下圖所示
接著展開其 System.Collections 節點,接著再展開 System.Collections.Generic 節點,就會看到 List<T> 節點,點擊這個節點,就會看到這個類別的原始 C# 原始程式碼。
不過,在這裡,似乎你只看到了這個 List 類別的成員定義,而成員的方法似乎都沒有實做出來。
讓我們繼續使用 ILSpy 工具,打開 C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.5\Profile\Profile259\ 目錄,這裡就是 Profile 259 會使用到的類別庫組件所在位置,找到 mscorlib.dll 這個檔案
當你點選剛剛開啟的 mscorlib.dll 的 mscorlib 節點,從右邊分割視窗中,您會看到這個敘述 [assembly: TypeForwardedTo(typeof(List<>))] 這表示,在執行時期,若你的專案中有使用這個 List 的泛型型別,則真正實作的組件,將不會在這個組件內定義,而是使用了 Type Forwarding 類型轉送 技術,需要到其他的組件上來找到這個類別的實作定義。根據微軟官方文件上的描述:型別轉送可讓您將某種型別移到其他組件,而不需重新編譯使用原始組件的應用程式

.NET Standard 標準類別庫

在這裡,請先下載 NETStandard.Library NuGet 套件到本機上,使用 zip 解壓縮工具,就可以解開這個 netstandard.library.2.0.0.nupkg 壓縮檔案。
當你在 Visual Studio 內,打開任意 .NET Standard 標準類別庫,就會看到如下圖
.NET Standard 標準類別庫
我們使用 ILSpy 工具,打開解壓縮 (netstandard.library.2.0.0.nupkg 壓縮檔案) 後的目錄 netstandard.library.2.0.0.nupkg\build\netstandard2.0\ref ,這裡就是 NETStandard.Library 會使用到的類別庫組件所在位置,找到 netstandard.dll 這個檔案
接著展開其 netstandard 節點,接著再展開 System.Collections.Generic 節點,就會看到 List<T> 節點,點擊這個節點,就會看到這個類別的原始 C# 原始程式碼。
不過,在這裡,似乎你只看到了這個 List 類別的成員定義,而成員的方法似乎都沒有實做出來。
讓我們繼續使用 ILSpy 工具,打開解壓縮 (netstandard.library.2.0.0.nupkg 壓縮檔案) 後的目錄 netstandard.library.2.0.0.nupkg\build\netstandard2.0\ref,這裡就是 NETStandard.Library 會使用到的類別庫組件所在位置,找到 mscorlib.dll 這個檔案
當你點選剛剛開啟的 mscorlib.dll 的 mscorlib 節點,從右邊分割視窗中,您會看到這個敘述 [assembly: TypeForwardedTo(typeof(List))] 這表示,在執行時期,若你的專案中有使用這個 List 的泛型型別,則真正實作的組件,將不會在這個組件內定義,而是使用了 Type Forwarding 類型轉送 技術,需要到其他的組件上來找到這個類別的實作定義。根據微軟官方文件上的描述:型別轉送可讓您將某種型別移到其他組件,而不需重新編譯使用原始組件的應用程式

總結

從上面的檢測過程,我們可以知道,不論 PCL 或者 .NET Standard 這兩個,他們使用的核心技術原則上是相同的,只不過對於中繼套件的使用方式與可以使用那些平台的 API 的規劃設定方式不同。
在 .NET Standard,中繼套件 (原始程式碼) 描述定義 (部分) 一個或多個 .NET 標準程式庫版本的程式庫集合,並且,以 NuGet 套件散發並由 NETStandard.Library 中繼套件參考的參考組件;而 PCL 的中繼套件則是存在於本機上的某個目錄中,這是隨著你的 Visual Studio 安裝的同時,也就會安裝進去的。
+

關於最後真正平台要使用各 API 實作,則是使用 Type Forwarding 類型轉送 技術,在執行階段,動態的進行使用真正實作的組件。

參考資料

2017/09/24

了解 .NET 標準類別庫 (.NET Standard) 的 Type Forwarding 運作情形(模擬演練)

在 .NET 標準類別庫 (.NET Standard Class Library) 裡面,使用到一個核心技術,那就是 Type Forwarding。
我們在這裡將會透過實際演練過程,讓您體驗出何謂 Type Forwarding。

準備專案

在這裡,建立一個 .NET Framework 類別庫,我們命名為:MyLibAssemblyA,這個類別庫內,僅有一個類別宣告,如下所示:
namespace TypeForward
{
    public class Foo
    {
    }
}
接著,我們建立一個 .NET Framework 可執行的專案,我們命名為:TypeForward,並且在這個專案內,使用加入參考功能,將類別庫 MyLibAssemblyA 加入到這專案內,這個可執行專案的程式碼如下:
using System;

namespace TypeForward
{
    class Program
    {
        static void Main(string[] args)
        {
            Foo foo = new Foo();
            Console.WriteLine(typeof(Foo).AssemblyQualifiedName);
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();
        }
    }
}

第一次執行

此時,請設定可執行的 .NET Framework 專案,TypeForward,為預設起始專案,並且執行這個專案。
您會看到這個應用程式將會輸出如下的內容,我們從這個內容,可以看到,我們這裡使用的型別, Foo 是位於 MyLibAssemblyA 組件內。
TypeForward.Foo, MyLibAssemblyA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Press any key for continuing...

加入 Type Forwarding

我們準備要把 Foo 這個類別的宣告,搬移到另外一個組件 (Assembly) 中,所以,我們另外建立一個 .NET Framework 類別庫,我們命名為:MyLibAssemblyB,這個類別庫內,僅有一個類別宣告,如下所示:
namespace TypeForward
{
    public class Foo
    {

    }
}
接下來,我們回到 MyLibAssemblyA 專案內,將之前宣告 Foo 類別的 .cs 檔案,修改成為如下的程式碼。
在這裡,我們看到了,我們將類別 Foo 的宣告已經移除了 (此時,這個類別的宣告已經搬移到 MyLibAssemblyB 專案內。
並且,我們在 MyLibAssemblyA 專案內,使用屬性 (Attribute) TypeForwardedTo 來指定目的地型別 Type (Foo) 其他組件(MyLibAssemblyB)中。
最後,我們要在 MyLibAssemblyA 專案中,使用加入參考功能,將 MyLibAssemblyB 專案加入到其參考清單中。
+

現在,我們可以重新建置這個 MyLibAssemblyA 專案。
請不要去建置 .NET Framework 可執行的專案 TypeForward
using System.Runtime.CompilerServices;

[assembly: TypeForwardedTo(typeof(TypeForward.Foo))]
//namespace TypeForward
//{
//    public class Foo
//    {
//    }
//}

第二次執行 (使用 Type Forwarding)

現在,我們到 MyLibAssemblyA 專案目錄下的 TypeForward\MyLibAssemblyA\bin\Debug 內的兩個 .dll 與兩個 .pdb 檔案都複製下來。
接著,將剛剛複製下來的四個檔案,複製到 .NET Framework 可執行的專案 TypeForward 下的 TypeForward\TypeForward\bin\Debug 目錄中。
此時,請執行 TypeForward.exe 檔案,您就會看到底下輸出結果。
這個時候,我們將看到了,類別 Foo 已經從組件 MyLibAssemblyA 轉移到組件 MyLibAssemblyB 了。
TypeForward.Foo, MyLibAssemblyB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Press any key for continuing...