個人的なメモ

Tomohiro Suzuki @hiro128_777 のブログです。Xamarin に関する事を中心に書いています。 Microsoft MVP for Development Technologies 2017- 本ブログと所属組織の公式見解は関係ございません。

Xamarin.Forms, Uno Platform と Mobile Blazor Bindings の比較

はじめに


こんにちは、@hiro128_777です。

C# / .NET 系のクロスプラットフォームフレームワークもいつも間にか、Xamarin.Forms, Uno.Platform と Mobile Blazor Bindings(まだ Experimental ですが) と3種類も登場しています。「さあ、これから C#クロスプラットフォームやるぞ」という方は、いったいどれから始めればいいのか迷ってしまうと思いますので、それぞれ、どんな違いがあるのかを簡単に整理してみました。



Xamarin.Forms, Uno Platform と Mobile Blazor Bindings のアーキテクチャの比較


Xamarin.Forms

f:id:hiro128:20200224235858p:plain



Uno Platform

f:id:hiro128:20200224235914p:plain



Mobile Blazor Bindings (実験段階)

f:id:hiro128:20200224235926p:plain


Xamarin.Forms, Uno Platform と Mobile Blazor Bindings それぞれの特長

Xamarin.Forms Uno Platform Mobile Blazor Bindings (実験段階)
UI コントロール 各プラットフォーム独自UIを尊重 UWP UI の再現を重視 各プラットフォーム独自UIを尊重
DSL XAML (Xamarin.Fomrs独自) XAML (UWP Based) Razor
iOS
Android
UWP ×
Web × ×
カスタマイズ難易度 × ?(まだ不明)




まとめ


Mobile Blazor Bindings はまだ Experimental ですが、Xamarin.Forms と Uno Platform は問題なくアプリ開発ができます。

Xamarin.Forms の方が設計的に、iOS, Android の最終的なアプリのカスタマイズが考慮されており、iOS, Android らしさについて細かい調整ができます。

Uno Platform は、カスタマイズはあまり考慮されていませんが、各プラットフォームでの見た目の差異が少ないので、UI にこだわりがないツール的なアプリが本当に簡単に作成できます。

Mobile Blazor Bindings は、今の所、Razor で Xamarin.Forms のUI が書けるという感じです。

実際にサンプルアプリをビルドしてみるとその違いがよくわかりますので、ぜひ試してみてください。

Visual Studio for Mac が Install additional required components に対応していました

Visual Studio for Mac は裏で、Xcode を利用していますが、Xcode をインストールまたはアップデートして、一度も起動してないと、iOS, Mac アプリのビルド時にエラーを吐いてビルドできない問題がありました。


これは、Visual Studio for Mac が悪いのではなく、Xcode がインストールまたはアップデート時に、Xcode command-line tools をインストールしてくれず、Xcode の初回起動時にダイアログが出て、インストールを確認する仕様になっているためです。


これは、初学者は自分で気づくことがかなり難しく Xamarin.iOS を使うときの大きなハマりどころでした。


ですが、いつの間にか、Install additional required components が実行されていない場合、Visual Studio for Mac が教えてくれて、そのままインストールも可能なようになっていました。


インストールしていないと、下記のように教えてくれます。

f:id:hiro128:20200119213950p:plain


あとは、Install Xcode command-line tools ボタンを押すと下記のように、Xcode の初回起動時のインストールが始まります。

f:id:hiro128:20200119214052p:plain


これは、とても良い改善です!


マイクロソフトは、Visual Studio for Mac に本気で取り組んでくれています。ぜひ、まだ使っていない方は使ってみてください。

visualstudio.microsoft.com

Xamarin.iOS Deep Dive その5 User type のオブジェクトが破棄される状況を理解する

はじめに



こんにちは、@hiro128_777です。
iOSDC で 以下の通り Xamarin.iOS の仕組みについてお話させていただきました。


www.slideshare.net
www.youtube.com



ですが、時間が足りなくて話しきれなかったことが多いので、blog でもう少し詳しく説明していこうと思います。


前回のお話はこちらです。

hiro128.hatenablog.jp


前回は、Wrapper type と User type の話でした。今回は User type の挙動について深く見ていきます。

User type でマネージ側の参照が解放される状況


前回、User type のオブジェクトは GC の判断で自由に開放することはできないというお話をしました。では、User type オブジェクトはいつ解放されるのでしょうか。

解放される状況は実質以下の2つです。

  1. 他からの参照がない状況で Dispose が明示的に呼び出されたとき
  2. ネイティブオブジェクトへの参照が対応するマネージオブジェクトのみになり GC がオブジェクトを解放したとき


他からの参照がない状況で Dispose が明示的に呼び出されたとき

C#
var button = new MyButton();

button?.Dispose();


上記のように、buttonを保持している他のマネージオブジェクトが何もないときに、ユーザーが明示的にDisposeをコールすると、マネージオブジェクトの参照が外れ、ネイティブオブジェクトとマネージオブジェクトの間のリンクが切断されて、GC がそのオブジェクトを回収できるようになります。

この時、ユーザーがDisposeをコールした後、ネイティブコードが何らかの理由でそのオブジェクトを使用しようとすると、Xamarin.iOS は対応するマネージオブジェクトが存在しないことを検出し、それを再作成しようとします。ですがこれは失敗し、プロセスを終了させる例外がスローされてしまいます。

その問題を解決するため、ユーザーがDisposeを呼び出したとき、Xamarin.iOS は以下のような挙動をします。

  1. マネージオブジェクトの参照を外す。(まだGCに回収はされていない)
  2. ただし、ネイティブオブジェクトの参照カウンタが0に達するまで、ネイティブオブジェクトとマネージオブジェクトの間のリンクは切断しない。(よってGCは回収できない)

これにより、ネイティブオブジェクトが存続している限り、マネージオブジェクトを引き続き参照することができるようにしています


ネイティブオブジェクトへの参照が対応するマネージピアオブジェクトになり GC がオブジェクトを解放したとき

C#
UIButton button;

public override void ViewDidLoad()
{             
    base.ViewDidLoad(); 

    button = new MyButton();
    View.AddSubView(button);
}

public override void ViewDidDisappear(bool animated)
{             
    base.ViewDidDisappear(animated); 

    button?.RemoveFromSuperview();

    button?.Dispose();
}


上記のような場合、button?.RemoveFromSuperview()が実行されると、ネイティブオブジェクトの参照カウンタが1になります。さらに、マネージオブジェクトからの参照が唯一の参照であり、ネイティブコードは当該オブジェクトを再び使用しないと安全に仮定できます。よって、ネイティブオブジェクトとマネージオブジェクトの間のリンクを解除し、ネイティブオブジェクトを解放して、GC がマネージオブジェクトを回収できるようになります。


この2つの状態が発生した時、User type のオブジェクトは破棄されます。


今回は以上となります。


次回は、イベントのアタッチによってメモリーリークするメカニズムを手順を追ってご説明します。

iOS で Uno Platform と Xamarin.Forms の registar.m を比べてみました。

はじめに


こんにちは、@hiro128_777です。


この記事は「Xamarin Advent Calendar 2019」の22日目になります。

最近何かと話題の Uno Platform ですが、iOS, Android に関しましては内部的には Xamarin を利用していると言うことですので、気になっているのでちょっと触ってみました。

私は、Xamarin.iOS が大好きなので、Xamarin.iOS との関係、特に registar.m の生成が Xamarin.Forms の iOS アプリとどう違うかを調べてみました。


作ってみたアプリ


registar.m を調べるにはとにかく実機ビルドしなければいけませんので、超簡単なサンプルを作って早速ビルドしてみました。

f:id:hiro128:20191222174415p:plain

そして同じアプリを Xamarin.Forms でも作ってビルドしました。


registar.m の比較


Uno Platform と Xamarin.Forms で生成された registar.m を比較してみましょう。


registar.m についてはレジストラーを使えるようにするための仕組みですのでこちらの公式ドキュメントも是非ご参照ください。
docs.microsoft.com


起動時に読み込むアセンブリの比較


比較しやすいように実際の registar.m と順番を変えています。


Uno Platform

static const char *__xamarin_registration_assemblies []= {
    "UnoSampleApp", 
    "mscorlib", 
    "Uno",
    "Uno.Core", 
    "Uno.Foundation", 
    "Uno.UI",
    "Uno.UI.Toolkit"
    "Uno.Xaml", 
    "Xamarin.iOS", 
    "Mono.Security", 
    "System", 
    "System.Xml", 
    "System.Numerics", 
    "System.Core", 
    "System.Net.Http", 
    "System.Drawing.Common", 
    "System.Data", 
    "System.Runtime.Serialization", 
    "System.Runtime.CompilerServices.Unsafe", 
    "System.ServiceModel.Internals", 
    "System.Web.Services", 
    "System.Xml.Linq", 
    "System.Collections.Immutable", 
    "Microsoft.Practices.ServiceLocation", 
    "Microsoft.Extensions.Logging.Abstractions", 
    "Microsoft.Extensions.Logging", 
    "Microsoft.Extensions.DependencyInjection.Abstractions", 
    "Microsoft.Extensions.Logging.Filter", 
    "Microsoft.Extensions.Primitives", 
    "Microsoft.Extensions.Logging.Console", 
    "Microsoft.Extensions.Configuration.Abstractions", 
};


Xamarin.Forms

static const char *__xamarin_registration_assemblies []= {
    "XFSampleApp.iOS", 
    "XFSampleApp", 
    "mscorlib", 
    "Xamarin.Forms.Platform", 
    "Xamarin.Forms.Platform.iOS", 
    "Xamarin.Forms.Core",
    "Xamarin.Forms.Xaml"
    "Xamarin.iOS", 
    "Mono.Security", 
    "System", 
    "System.Xml", 
    "System.Numerics", 
    "System.Core", 
    "System.Net.Http", 
    "System.Drawing.Common", 
    "System.Data", 
    "System.Runtime.Serialization", 
    "System.ServiceModel.Internals", 
    "System.Web.Services", 
    "System.Xml.Linq"
};



比べてみると、Xamarin.Forms の方がだいぶ少ないですね。Uno Platform は、iOS の UI コンポーネントを UWP の UI コンポーネントAPI で利用できるように、かなりの量のコードを書いています。この部分が丸々乗っかって来てるので、起動時に必要なアセンブリが多くなっています。



トランポリンの数の比較


トランポリンは C# 側のハンドラーメソッドを、Objective-C のランタイム側から引数をマーシャリングして呼ぶ仕組みです。
詳しくはこちらをご覧ください。

また、こちらの公式ドキュメントも参考になります。
docs.microsoft.com



Uno Platform : 124
Xamarin.Forms : 163


Uno Platform がかなり少ないですね。トランポリンの分布は調べるのがかなりしんどいので調べていません。



Class Map の 登録


Class Map は C# 側のオブジェクトで、Objective-C のランタイム側からアクセスする必要があるものを起動時に Objective-C のランタイムに登録する仕組みです。


Uno Platform : 486
Xamarin.Forms : 340


Uno Platform がかなり多くなっています。これも、Uno が、iOS の UI コンポーネントを UWP の UI コンポーネントAPI で利用できるように、かなりコードを書いていることに起因しています。



サンプルアプリの ipa のサイズ


Uno Platform : 13.5MB
Xamarin.Forms : 8.7MB


f:id:hiro128:20191222231906p:plain


上記のようになりました。 Uno Platform の方がアセンブリの数が多いので、順当な結果です。実際に大きいアプリになればフレームワークの容量はあまり関係ないので、そこまで気にするほどではないと思います。


まとめ


Uno Platform と Xamarin.Forms を、registar.m から比較してみましたが、最終的にどちらも Xamarin.iOS を利用しているので、その観点からは大きな差はないことがわかりました。もう少し何か出るかと思って調べてみましたが、単に使っている UI コンポーネントのライブラリが違うだけと言うそのままの結果でした。


Xamarin.iOS の上に、Uno Platform または、 Xamarin.Forms が乗っているという構成は全く同じですので、UWP の XAML を使いたいのか Xamarin.Forms の XAML を使いたいのか、あとは SPA も一緒に生成したいかでどちらを使うか決めればよいと思います。


@tan-yさんも仰っていましたが、Uno Platform は、UI コンポーネントの定義とレンダリングが分離されていません。よってカスタムでレンダリングしたいときに Xamarin.Forms のように比較的戦略的に機能を追加するのは難しく、あまり自分でカスタマイズして使う前提ではないのかも知れません。Uno Platform の FAQ にも、 UWP で提供されている全ての API を 様々なプラットフォーム上に展開するのがゴールだとあるので、カスタマイズは方向性として違うのだと思います。


Uno Platform は、UWP の各デバイスへの展開
Xamarin.Forms は、各デバイスの UI コンポーネントを統合して抽象化
なので、同じ XAML を使っていても、そもそもの思想が違うようです。


ですが、Uno Platform が Xamarin.iOS, Xamarin.Android を使用しているのも、Xamarin.iOS, Xamarin.Android がプラットフォームのブリッジとして、とてもよくできているからだと思っています。Xamarin.Forms がそれ自体の実装に、iOS, Android のブリッジを持たず、Xamarin.iOS, Xamarin.Android の上に乗せる形になっているのも、とても素晴らしい設計です。


Uno Platform と Xamarin.Forms は方向性が違うので今後の発展がとても楽しみです。また時間をとって、もっと色々調べてみたいです。


今回生成した、registar.m こちらなのでよろしければご覧くださいませ。


Uno Platform
Xamarin.Forms


以上です。

Xamarin.iOS Deep Dive その4 Wrapper type と User type

はじめに



こんにちは、@hiro128_777です。
iOSDC で 以下の通り Xamarin.iOS の仕組みについてお話させていただきました。


www.slideshare.net
www.youtube.com


ですが、時間が足りなくて話しきれなかったことが多いので、blog でもう少し詳しく説明していこうと思います。


前回のお話はこちらです。

hiro128.hatenablog.jp



前回は、ネイティブとマネージドの話でした。そして、ネイティブオブジェクトにはさらに2種類のオブジェクトがあります。今回はその説明となります。


Wrapper type と User type


Xamarin.iOS で、取扱いに気をつけなくてはいけないのは、ネイティブオブジェクトですが、ネイティブオブジェクトにはさらに、Wrapper type と User type という2つの型があります。Xamarin.iOS における GC の挙動を考える上で、この2つの型の区別はとても重要になります。

Wrapper type


UIView や UIButton のような Objective-C の型をラップした型です。
モリーリークはしません。

public override void ViewDidLoad()
{             
    base.ViewDidLoad(); 

    var button = new UIButton();
}

Wrapper type の特徴


f:id:hiro128:20190930193636p:plain

  • マネージドの世界では、ネイティブオブジェクトのインスタンスへのハンドルだけのみを持ちます。(マネージドな世界だけで保持されているステートは持ちません)
  • Wrapper type のインスタンスの寿命は、ネイティブオブジェクトの寿命とは無関係です。
  • Wrapper type のインスタンスは、マネージドな世界にネイティブオブジェクトへのハンドル以外には何も保持していないので、GC の判断によっていつでも自由に解放できます。
  • もし、後で再びそのオブジェクトが必要になった場合には Wrapper type のインスタンスが再度作成されるます。
  • Wrapper type としてインスタンス生成されても、イベントのアタッチなどでマネージドな世界だけで管理されているステートを持ったタイミングで、User type にアップグレードされます。



User type


f:id:hiro128:20190930193654p:plain

下記の MyButton のように、UIView や UIButton のような Wrapper type を継承し派生した型で Objective-C に対応する型が無いものです。
いつマネージ側の参照が解放され。ネイティブオブジェクトが破棄されるかがポイントです。(メモリーリークのリスクがあります)

public override void ViewDidLoad()
{             
    base.ViewDidLoad(); 

    var button = new MyButton();
}

public class MyButton : UIButton 
{
    public string Id { get; set; }  
}

User type の特徴

  • マネージドの世界では、ネイティブオブジェクトのインスタンスへのハンドルの他に、マネージドな世界だけで管理されているステートを持ちます。
  • User type のインスタンスの寿命は、ネイティブオブジェクトの寿命と関連があります。
  • User type のインスタンスは マネージドな世界だけで管理されているステートを持つ可能性があるため GC の判断によって自由に解放することはできません。
  • Xamarin.iOS はマネージオブジェクトを必要な間はずっと生かし続けることを保証ために GCHandle を作成し、回収されないようにします。


今回は以上となります。


次回はこちらです。
hiro128.hatenablog.jp

Xamarin.iOS Deep Dive その3 ネイティブとマネージド

はじめに


こんにちは、@hiro128_777です。
iOSDC で 以下の通り Xamarin.iOS の仕組みについてお話させていただきました。


www.slideshare.net
www.youtube.com


ですが、時間が足りなくて話しきれなかったことが多いので、blog でもう少し詳しく説明していこうと思います。


前回のお話はこちらです。

hiro128.hatenablog.jp



今回から、Xamarin.iOS がメモリーリークするメカニズムについての説明に入っていきます。


今回のお話に役に立つ iOS アプリのアーキテクチャーについてはこちらの公式ドキュメントも是非ご参照ください。
docs.microsoft.com

Xamarin.iOS で大変なのは、プラットフォーム固有の部分だけ

まず、最初に大事なことを確認しますと、Xamarin.iOS で、メモリリークやイベントの消失など「クセ」があり大変なのは、プラットフォーム固有の部分だけです。.NET の世界で閉じていることの多いロジック部分は、センサー類などの機種依存コードのDI部分を除けばとても安定しています。



f:id:hiro128:20190927204923p:plain



そして、プラットフォーム固有の部分とは、具体的には以下の2つです。

NSObject を継承したクラス
public class SomeClass : NSObject
{

}

※ NSObject 自体も、 INativeObject を実装しています。

INativeObject を実装するクラス
public class SomeClass : ISomeInterface
{

}

public interface ISomeInterface : INativeObject
{

}

この2つの共通点は、ネイティブオブジェクトつまり Objective-C の世界へのポインター(ハンドル)を持っていることです。ネイティブの世界へのハンドルを持っているため適切に扱わないとからリークします。

Windows の世界でも FileStream など、Win32 API の世界へのポインターを持つものを適切に扱わないとリークしてしまうのと同じことです。

INativeObject の定義
using System;

namespace ObjCRuntime
{
    //
    // 概要:
    //     A simple interface that is used to expose the unmanaged object pointer in various
    //     MonoTouch classes.
    //
    // コメント:
    //     All this interface requires is for a class to expose an IntPtr that points to
    //     the unmanaged pointer to the actual object.
    public interface INativeObject
    {
        //
        // 概要:
        //     Handle (pointer) to the unmanaged object representation.
        //
        // コメント:
        //     This IntPtr is a handle to the underlying unmanaged representation for this object.
        IntPtr Handle { get; }
    }
}



ネイティブとマネージド

ネイティブとマネージドの定義は以下の通りとなります。

ネイティブ

  • Objective-C ランタイムで実行されるコード



マネージド

  • mono ランタイムで実行されるコード



そして、Xamarin.iOS(マネージド)の世界の裏には隠れた Objective-C(ネイティブ)の世界がある事を常に意識してください。この表と裏の世界との関係性によって、メモリーリークが発生します。


f:id:hiro128:20190927204833p:plain




それでは、今回は以上となります。


次回はこちらです。
hiro128.hatenablog.jp

Xamarin.iOS Deep Dive その2 Xamarin.iOS は簡単にメモリーリークしてしまう

はじめに


こんにちは、@hiro128_777です。
iOSDC で 以下の通り Xamarin.iOS の仕組みについてお話させていただきました。


www.slideshare.net
www.youtube.com


ですが、時間が足りなくて話しきれなかったことが多いので、blog でもう少しこの話題を掘り下げて説明していこうと思います。


前回のお話はこちらです。

hiro128.hatenablog.jp



今回は Windows で 開発してきた皆さんが Xamarin.iOS でハマりがちなメモリーリークについてご説明します。

Windows と Xamarin.iOS の簡単なサンプルコードでメモリーリークの状況をそれぞれ確認する。

View 上にボタンがあり、ボタンを押すと View を閉じるサンプルコードです。 Windows と Xamarin.iOS で全く同じことをしています。

実際に動作するサンプルコードは GitHub にアップしてありますので是非動かしてみてください。GitHub にアップしたサンプルではオブジェクトの初期化、破棄、ファイナライズでそれぞれトレースを出力するようにしています。 blog のコードは簡略化してあります。

ますは Windows のコードです。

Windows Forms
void InitializeComponent()
{
    var buttonClose = new Button();
    buttonClose.Click += ButtonClose_Click;
    Controls.Add(buttonClose); 
} 

void ButtonClose_Click(object sender, EventArgs e) => Close();

フルバージョンはこちらです。
github.com


とても簡単なコードですね。当然、メモリーリークなどしません。

次に、Xamarin.iOS で同じことをやってみましょう


Xamarin.iOS
public override void ViewDidLoad()
{
    base.ViewDidLoad();

    var dismissViewButton = new UIButton();
    dismissViewButton.TouchUpInside += DismissViewButton_TouchUpInside;
    View.AddSubview(dismissViewButton); 
} 

void DismissViewButton_TouchUpInside(object sender, EventArgs e)
    => DismissViewController(truenull);

フルバージョンはこちらです。
github.com

はい、メモリーリークしました!

全く同じ意味のコードを書いているのに、Xamarin.iOS だけメモリーリークしてしまいます。

ただし、実はこの現象は、どちらかといえばメモリーリークする Xamarin.iOS の方がプログラムの動きとしてはむしろ当たり前で、メモリーリークしない、Windows がすごいと言った方が正確です。Windows .NETGC はなかなか賢くて、このような単純なパターンであれば本来メモリーリークしてもおかしくないコードでもしっかり GC が回収してくれます。

なぜ、Xamarin.iOS ではメモリーリークするのか


前回も言った通り、Xamarin.iOS は「攻めた設計」であり、.NET と iOS API の相互運用という 「禁断の果実」に手を出しているからメモリーリークしてしまいます。

具体的には

という機能を使えるようにした「副作用」です。

では、次回以降では、Xamarin.iOS でメモリーリークする詳しいメカニズムを見ていきましょう。

今回は以上となります。


次回はこちらです。
hiro128.hatenablog.jp