Xamarin を使えば iPhone 端末が要らない、ワケがない

今年も Advent Calendar 1番手、よろしくおねがいします。

さて、Windows + Visual Studio 2017 を使った iOS アプリの開発でも、

などの登場で、「おっ、これなら iPhone 端末なしで(Mac なしで) iOS アプリ開発できるんじゃね?」 という風説がチラホラ聞かれます。

私は「んなことはない」と考えているのですが、実際どの辺が「ほら iPhone 実機必要でしょ?」なのかを検証してみようと思います。

そして、 iPhone 実機が必要だということは Mac も必要だということになりますからね。Mac がないとアプリをビルドして実機に転送できないので。

今回は、 「iPhone 実機は必須」 → 「iPhone 実機がないと困ったことが起きる可能性がある」 → 「iPhone 実機でしか発生しないトラブルがある」 という観点で、「iPhone 実機でしか発生しないトラブル」は本当に実在するのかを調査しましょう。

Xamarin.iOS アプリの動作の仕組み

Xamarin.iOS のアプリ開発について、最初に知っておくべきことは、 「iPhone 実機向けと、iOS シミュレータ向けで、配布されるアプリケーションはまったく違う」 ということです。

iPhone 実機向けのビルドでは、 AOT(Ahead-Of-Time) コンパイラによって、 事前に プログラムをマシン語に変換されたものを配布します。Apple の制約で iOS 端末上では、動的なプログラムの実行が許可されていないためです。

一方、 iOS シミュレータ向けのビルドでは、AOT ではなく JIT でアプリが動作します。なので AOT で生じる特有の「制約」が iOS シミュレータでは適用されないのです。

しょせん、simulation は "ニセモノ、まがい物"、本物とは違うのです。 普通の思考なら、この時点で 「あっ、iPhone 実機要るわ」 となります、はい終了ー。

Xamarin.iOS - AOT の制約を体験してみる

これで終わってもアレなので、もうちょっと突っ込んでみましょう。 じゃあ AOT で生じる特有の「制約」って具体的には何よ?ということで、それを体験してみましょう。

AOT での制限事項は次に書いてあります。

ここに書いてある制限事項をトレースしてみましょう。

Generic Subclasses of NSObjects are limited → 実機だけで発生するか?=:question:

えーと、「NSObjects を基底クラスにして Generic クラスを作るのには制限がある」と書いてありますかね。 ん? 続いて "While generic subclasses of NSObjects are possible, there are a few limitations. " とあるので、なんかできるようになったっぽいです。わずかな制約はまだあるので油断は禁物、とも。

試しにここに書いてある class Foo<T> : UIView { } というクラスを作って使ってみたのですが、実機でも特に問題なかったです。でも few limitations を確認してないので "実機だけで発生するか?" の評価は :question: で。

P/Invokes in Generic Types - 実機だけで発生するか?=:x:

「Generic なクラス内で P/Invoke は使えない」と書いてありますね。

Xamarin.iOS プロジェクトで、

class GenericType<T> {
    [DllImport ("System")]
    public static extern int getpid ();
}

というクラスを定義すると、

The DllImport attribute cannot be applied to a method that is generic or contained in a generic type. (CS7042)

というエラーがビルド時に出て進めません。これはシミュレータ向けでも実機向けでも同じです。また PCL プロジェクトでは DllImport 自体が使用できません。

よって、この件について「実機だけで発生するか?」の問いには "No" の回答、:x: です。

Property.SetInfo on a Nullable Type is not supported - 評価できず

Nullable Type どころか、Xamarin.iOS どころか、Xamarin.Android でも、普通のオブジェクトに対しても Property.SetInfo を機能させることができませんでしたー。ちょっと時間切れで割愛。

Value types as Dictionary Keys - 実機だけで発生するか?= :question:

「値型を Dictionary のキーにすると実機でクラッシュする」と書いてあるように見えます、ほうほう。

ValueType から直接派生させる方法を知らないので、次のような struct を作って Dictionary のキーに突っ込んでみました。

public struct MyKey 
{
    public string Id { get; set; }
}

[Register("AppDelegate")]
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
    public override bool FinishedLaunching(UIApplication app, NSDictionary options)
    {
        var dic = new Dictionary<MyKey, string>();
        dic.Add(new MyKey { Id = "a" }, "aaa");
        dic.Add(new MyKey { Id = "b" }, "bbb");
        dic.Add(new MyKey { Id = "c" }, "ccc");

        if (dic.ContainsKey(new MyKey { Id = "b" }))
        {
            dic.Remove(new MyKey { Id = "b" });
        }

        Console.WriteLine(dic.Count());
        <以下省略>

特にクラッシュはしないですね、シミュレータでも実機でも。 しかしこんな簡単なケースだったらもっと大事になってる気がするし、この症状の再現方法に不安があるので :question: で。

**2017/12/01 PM 追記 **

この制限、もうないそうですw

No Dynamic Code Generation

本題っぽくなってきました。 Reflection.Emit とか動的言語ランタイム(DLR)、Remoting は動きませんよー、と書いてあります。Remoting は調査してませんのであしからず。

Refrection.Emit - 実機だけで発生するか?= :x:

実は使ったことないので、精鋭が書いたネットのコピペを元にやってみたんですが、どうも必要なクラスやメンバが Xamarin.iOS では公開されてない(あるいは実装されてない)っぽくて、シミュレータでも実機でも動作しませんでした。

dynamic 型 - 実機だけで発生するか?= :x:

DLR が提供する代表的な機能である dynamic 型はどうでしょう?

DLR を使用するには Microsoft.CSharp を nuget や参照の追加で Xamarin.iOS プロジェクトに追加する必要があります。

その上で、次のような("C# でぐぐれ!"の人のサイトからの)コードを実行してみました。

static dynamic GetX(dynamic obj)
{
    return obj.X;
}

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
    var ret = GetX(new System.Drawing.Point { X = 999 });
    Console.WriteLine(ret.ToString());

    <以下省略>

このコードも、シミュレータでは当然動作しますが、なんと実機で動作します。どうなっとるんじゃい。 というわけでこれも :x:

これについては、2014年にリリースされた Mono 3.2.7 で、

System.Core now has an interpreter for LINQ expressions and dynamic statements that can be used by FullAOT runtimes.

と書かれているので、それの恩恵かなと推測されます。 また、最近でも、

という話題があり、 Reflection.Emit もイケちゃうかもみたいな記述もあります。この辺りが好きな方は是非「AOT技術 Advent Calendar 2017」へ投稿をお願いします。 :pray:

Reverse Callbacks

よくわからないので、割愛 :bow:

ここまでのまとめ

さて Limitations - Xamarin を辿ってきましたが、明確に 「iPhone実機でのみ動作しないという制限」 は確認できませんでした。

しかし、制限そのものが確認できなかったり、細かい制約はあるので、安心して「実機のみ動作しないことはない」とは言い切れません。

リンカーのお仕事 - みなさんが「AOT のせい」と普段言ってるのはたぶんこっち

とは言え、iPhone 実機でのみ動作しないコードに遭遇するという体験は確かにあります。

例えば次のコード

var typeName = "System.Net.WebClient, System, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e";
var type = Type.GetType(typeName);
var client = Activator.CreateInstance(type);

Debug.WriteLine(client.GetType().AssemblyQualifiedName);

厳密名で指定してるから長いけど、要は「型を示す文字列からインスタンスを生成する」という処理。できる限り実装を外部から注入できるようにするとどこかで使うやつですね。

このコード、iOSシミュレータでは動作しますが、iPhone 実機では動作しません(typenull、つまりそんな型はない、と言われます)。

.NETフレームワークのクラスのひとつである「System.Net.WebClient」は確かに存在しているはずなのに、「ない」と言われる、なぜか? そこで気にすべき存在が「リンカー」です。

「Xamarin.iOS におけるリンカー」とはなにか?

リンカー(linker)とは、ソースコードを最終的に実行可能プログラムにするまでに登場するツールの一つですが、その辺のはなしは「compiler, linker」などとググればたくさん出てくるのでそちらで。

Xamarin.iOS の文脈での「リンカー」のお仕事は、

  • アプリケーションには不要な(=使用されないと判断した)クラスやメンバーを削除すること

だと思っておけばよいでしょう。 Android だと ProGuard、.NET だと DotFuscator などの「難読化ツール」が持つ副次的な機能に「不要コードの除去」がありますが、それと同じようなものだと理解できます。

つまり、先のコードに出てきた System.Net.WebClient は、静的なソースコード解析によって「不要だと判断されて」削除されてしまいました。文字列の中に記述されているとはリンカーは知る由もありません。

では、なぜ iOSシミュレータでは動作するのでしょう? とここで Xamarin.iOS プロジェクトの設定を見てみましょう。

実機向けビルドの設定

image.png

iOSシミュレータ向けビルドの設定

image.png

はい、このように

  • 実機向けの場合は「フレームワークSDKのみをリンクする」
  • シミュレータ向けの場合は「リンクしない」

となっていました。つまりシミュレータ向けのアプリは「不要なコードを削除する処理が行われていない」ということになり、すべてのクラスが使用できていました。

逆に言えば、 シミュレータ向けのビルド設定を「フレームワークSDKのみをリンクする」にすれば、実機と同じように上記コードは動作しなくなります

シミュレータでは AOT は使用されないのに、実機と同じ症状を発生させられるのだから、これは 「AOT のせい」ではないことがわかります。

おそらく一般的に「AOT のせいで iOS 実機で動作しない」と言われている事象のほとんどは「実はリンカーのせい」だと思われます。

例えばこれ↓とか

書いたのオマエかよw

なので、このトラブルは、iOSシミュレータ向けのビルド設定を「リンクする」に変更することで発見可能です。あれ、ここでも iPhone 実機必要なくなっちゃった。:sweat_smile:

(実機向けのビルド設定を「リンクしない」にしても同様に動作しますが、配布する .ipa にデータサイズが巨大になってしまうので現実解ではないです。)

リンカーに消されないようにするには?

次の3つの方法があります。

  1. どこか1箇所でいいから、型を明示的にソースコード中に書く
  2. 消されたくないクラスに Preserve 属性を付ける
  3. ビルド設定で「除外するアセンブリ」を指定する

1 が一番よく行う方法です。アプリの static コンストラクタに

static AppDelegate()
{
    var dummy = new System.Net.WebClient();
}

などと書いておくだけでOKです。デメリットは、使用する実装クラスが予めわかっている必要があること、インスタンス生成のコストが発生することです。

ライブラリでも、「とりあえずアプリ起動時に MyLib.Init() を呼べ」みたいなものがありますが、それもリンカー対策のために必須としていると思ってもよいでしょう。

2 はライブラリ開発者側の話で、これは、消されるとまずいぞとわかっているクラスやメンバに [Preserve] という属性を付けておくと、リンカーさんは、それがついているクラスを無視してくれるというものです。

デメリットは、ライブラリ開発者しか使えないということです。ライブラリ使用者つまりアプリ開発者は、問題が発生したライブラリに対して、この方法は適用できません。

3 はビルド設定で、リンカーの対象外にするアセンブリを指定する、というものです。

デメリットというか、どこに何を設定すれば機能するのか分からないんですよね、IDEでは。。。

さいごのまとめ

  • AOT の制限 -> 実機でのみ再現する事象は確認できなかった(未確認な制限あり)
  • リンカーによるトラブル -> iOSシミュレータでもビルド設定を「リンクする」に変えれば未然に防げる

あれ? iOSシミュレータだけでも結構イケる???

ままま、まあシミュレータは所詮シミュレータなんで!いつも実機で開発してないと、いざデプロイするときに絶対問題起こるんで!!:cold_sweat::sweat_drops:

しかし会社の方針?とかでiPhone実機が提供されず、シミュレータしか使えない環境の人は、せめてビルド設定だけは変えておきましょう。

ただし「Xamarin Live Playerだけ使えばOK」その考えはダメ。なぜならこれは「あなたのコードを、iOS アプリっぽい画面にインタラクティブに表示させるアプリ」だからです。

そこで Visual Studio App Center ですよ(ですか?)

Visual Studio Mobile Center から名称変更して正式リリースとなった Visual Studio App Center は、 Xamarin Test cloud のサブセット?を内包しており、雲の向こう側にある「実際の端末」を使ってテストができます。継続的に実機でテストが実施できるとしたら、それはとても恵まれた環境でしょう。けど、開発時に常時使うもんじゃないよね。