Xamarin バッドノウハウ大全 の補足

はじめに

先日のわんくま横浜で田淵さんの話された Xamarinバッドノウハウ大全 で自分の報告したバグ?が多く採用されてたようなので補足です。

一応自分の立ち位置を言っておくと、Xamarinのトラブルとかバグとかに対してタヒね!と怨嗟の声を日々ツイッターでつぶやいてますが、Xamarinはほかのものに代えがたくXamarin.Formsもとても有効な弾丸だと思ってます。
一言でいうと Xamarin、Xamarin.Formsはいいぞ。F#もいいぞ。 という感じです。
前置きは置いておいて以下補足。

Visual Studio と Xamarin Studio で相互運用す ると .csproj がゴリっと書き換わったりすることがあ るみたいよ。

WindowsのVSとMacのXSでソースをやり取りするとIDEのバージョン?とかなんかいろいろ書き換わります。
それで問題あるかどうかですが、自分のソースではF#部分が昔は全く通らなかったり、この間でもコンパイル通るようになったけど実行すると一発で落ちるなどで実際の相互運用は出来ていないので不明です。
ただその辺の書き換えのせいでVSに戻したらコンパイル通らなくなって、もう二度とするまいと思ったことがあったような気がが。

Xamarin.Forms の Slider は Min を先に指定し ないと落ちるみたい…

最近でどうなってるか試してませんが、前はそれですっ飛んでかつデバッグウィンドウにも有用な情報が出ず少しはまりました。
今はXAMLでのバインディングをせずにC#でViewなどのOnBindingContextにてMinimumにDouble.MinValue、MaximumにDouble.MaxValueをセットしたのちに値セットなりバインディングをしています。

Xamarin.Forms で PushModalAsync 表示をし たときに、Navigation.ModalStack.Count の値が iOSAndroidで違うんだって。

Navigate.PushModalAsyncなどするとNavigation.ModalStackに積まれていくんですが、一枚PushModalAsyncしたときにNavigation.ModalStack.Countの値がAndroidでは2、iOSでは1になります。
何かしようとしたらすぐ気づくと思いますが、最初はあれ?ってなると思います。

Xamarin.Forms の Device.BeginInvokeOnMainThread は、 Android ではそのまま実行されるけど iOS はポス トされてから実行されるので挙動が変わるんだって。

Device.BeginInvokeOnMainThread的なものの使い方として

  • 1.別スレッドからUIスレッドで処理したいものの実行をする。
  • 2.今UIスレッドにいて、今の処理が終わった後で実行してほしいのでPostしておく 

というのが多いのではと思います。
iOSはそのような挙動をするのですが、Androidでは今UIスレッドにいる場合はポストされて後で実行するのではなくそのまま実行してしまいます。
async/awaitでの処理待ちなど絡んだ時に場合によって予期せぬデッドロックを生む可能性もあると思うので注意が必要かと。

Task.Run(()=>Device.BeginInvokeOnMainThread(...));

などとすればAndroidでもUIスレッド上でPostする的な挙動になると思います。

Xamarin.Forms の StackLayout は設定がややこ しいよ。特に XXXandExpand が闇みたいでレイア ウトもバグるんだって。

Xamarin.FormsのStackLayoutはXXXAndExpandというオプションがありGridでの*のような感じで残りを占めるという風にレイアウトできます。
複数個Viewを配置した後、残りをこのViewで占めるなどGridレイアウトでもよく行うと思いますが、GridではRowDefinitionsなどであらかじめ配置する数を決めておかないといけなかったりそもそもRowDefinitionsの定義をしなければいけないなど面倒で、StackLayoutなら思いつくまま追加した後、広げるものにXXXAndExpand等をつければよく追加も簡単なので最初は多用していました。
けれどいろいろとレイアウトしてるうちに何かのタイミングでレイアウトがずれることが多発してどうやらStackLayoutのレイアウトが怪しいのではという結論に至りました。
そもそもXXXAndExpandはおそらく初見では絶対にどうなるか理解できずに間違えると思います。
その点Gridは面倒だけれど挙動が予測できます。
結局自分は怪しいところは全部Gridに戻しました。

Xamarin.Forms の TapGestureRecognizer の 判定範囲が Android 側で広すぎて、スワイプと かぶっておかしくなるらしいよ。

これは自前Carouselを実装してた時だと思いますが、横にスワイプをするもの上にTapGestureRecognizerを実装したViewを配置した時に、Android側でタップにはならないだろうと思うぐらい横に動かしてから指を話すとTapが発火してしまってました。
結局TapGestureRecognizerではやり方が分からなかったのでRendererでAndroidのTouchイベントを拾って自分で動かした範囲を拾って判定するようにしました。
この辺のXamarin.FormsのジェスチャーAndroid,iOSのTouchイベントがScrollViewなどの上にあるビューでどう発火するかなどはプラットフォームごとにだいぶパターンが違うので、もし何か作る場合には調べる必要があります。

Xamarin.Forms の PanGestureRecognizer は iOSAndroid でイベントが発火したり発火しな かったりするんだって。

これがまさにそんな感じでScrollViewの上に配置したViewでPanGestureをとろうとするとAndroidでは発火しなかったのかScrollViewでの期待した動作にならなかったのかいい感じにならず、iOSでは発火して望みの挙動になりました。結局Android側はTouchイベントで処理するようにしてiOS側はそのままPanGestureを使いました。
たしかXamarin.FormsのPanGestureRecognizerとiOSのUIPanGestureRecognizerではまた挙動がいろいろ違った気がします。
逆にあるものがだめでもほかのものでは望みの挙動をするなどの場合もあるのでいろいろ試してみればいいと思います。

Xamarin.Forms で InputTransparent=false にし ても iOSAndroid で下のコントロールにイベント を透過できないことがあるらしいよ。

これはちょっと逆でViewの上に半透明なビューを置いたときに、下のView上にあるボタンが発火しないようにしたくて上の半透明のViewのInputTransparent=falseで行けるのかと思ったのですが、だめで下のボタンが押されてしまったという話です。
これもiOSかAndoirdか、どっちかは下のものにイベントがいかないようになったんですが、もう一方がそうならないという感じだったはず。
結局下にイベントを行かせたくないViewにTapイベントを何も処理しないダミーのTapGestureRecognizerを配置することで下に行かないようになりました。

Xamarin.Forms の Picker も闇らしいよ…

これは話がややこしいんですが、Xamarin.FormsのPickerはちゃんと動きます。
けれどXamarin.FormsのPickerはEntryのような見た目で、今回ほしかったものはボタンの形状をしていて文字列として現在の値がある、ボタンを押すとその時に選択候補を取得し、それをピッカーとして表示して選択するというものでした。
ボタンを押したときに選択候補を取得する必要がなければ、ボタンの上に透明に近いPicker(Opacity=0.011.たしか0.01だとiOSでおかしかったはず)を配置することで触った瞬間にピッカーが出るという風にできました。
がどうしてもボタンを押して選択候補を取得する必要があったのでPickerのWidth/HeightRequestを1に、透明近くにしたうえで上に書いたダミーのTapGestureRecognizerを配置して触られないようにする、ボタンが押されたときに選択候補を取得して、Pickerに値などをセット、その後PickerにFocusをするとピッカーが出るのでこれでいけるかなど色々試行錯誤してました。
が、これで動きそうだとおもったのですが、何かのタイミングでボタンを押してもピッカーが表示されない現象が起き、どうにも解決できず。
ので最終時にボタンを押したときのイベントでネイティブ側で各々のプラットフォームのやり方でピッカーを表示するようにしたらあっさりと動きました。思えば最初からそうしてればよかったと思いますがAndroidをあまり知らなかったのでFormsで何とかしようと思ったのが間違いのもとでした。
困ったらネイティブ処理。大事なのでもう一度言うと困ったらネイティブ処理です(´・ω・`)

Xamarin.Forms の ListView はメモリリークするん だって…

これは自分じゃないかも?けどなんかProfilerで調べたりしてるとリークはしてそうな気はしますが、自分の場合だとそこまでそこで困ってはないです。今回はビューごと破棄することが多いのでその辺で解消されてるのかもしれません。
解決法?CashingStrategyを変えるとかですかね…あとどうしても残ってしまうものはそのListViewがいらなくなった後Cellの中のViewのBindingContextにnullを入れてBindingを断ち切る、ViewCellの上に配置したViewを何かのタイミングで切り離して破棄するなども有効そうな予感です。

Android の View は Mono 側で GC してもしば らく Java 側から握られてたりしてメモリから消える までに時間が掛かるんだって。メモリリークじゃない らしいよ。

えーとMonoのGCをした後もJava側からXamarin.FormsのViewへの参照が残ってていろいろ解放されないなどとなるためメモリリークの調査時は注意が必要です。
Javaの方のGCもやると消えるんですが、自分の経験的には今参照残ってないはずのものをメモリからパージしたい場合にはMonoのGCJavaGCを交互に3回ずつぐらいやった方が確実に消えると思います。
なおしばらく放置してればJavaのほうがつかんでるものも消えます。メモリの使い方にもよると思いますが、2-3分放置してたら消えたことが多かったような。
それとは別になんかメモリに少し残るなぁというものはそれはそれであった気がしますが詳細未確認。
しかしプロファイラーは早く良くなりませんかね…JetBrainが作ってくれてもいいんですよ(/ω・\)チラッ

iOSデバッグできる端末と launch failed でデ バッグできない端末が未だにあるんだって。

これはおそらくXamarinのせいではなく自分の方のiPhone端末のProfileがおかしいせいです。がそれでデプロイできなかったりデプロイはできてもデバッグ実行できなかったりするんですが、その辺の原因がよくわからないのはXamarinのせいじゃないかなー。Xamarin側でもAppleがろくなエラーを出さずに何ともしようがないのかもしれませんが。
いずれにしても何がおかしいのかわからないので放置してデバッグできる端末やシミュレータでしのいでます。
まぁこの辺は去年、おととしに比べたら楽なもんですよえぇ。

※朝起きたら思い出したので追記

F#のsprintfで引数を5個以上にすると実機にデプロイした時だけそこですっ飛ぶ。

これはバグジラにも前から上がってるし治ったよとなってるはずですが、この間もなりましたね。このバグの時はデバッグウィンドウに痕跡を残さずすっ飛ばれるので原因解析にちょと困ります。

解決法…引数を5個以上にしない。

SkiaSharpで文字列のMeasureが変なサイズを返す。

SkiaSharp速いしいいんですが、文字列の表示と合わないサイズを返してきて困ります。解決法ないんですが、ひとつ前のバージョンでだいぶ良くなりました。最新のは組み込んではまだ試してませんが期待。

SkiaSharpを使うとAndroid側のAOTができない。

Xamarin.Forms、なにやらAndroid側のパフォーマンスがよろしくないんですが、AOTすると起動速度やその後の処理速度が改善されます。ものにもよると思いますが、体感できるレベルで速くなるかと。
が、SkiaSharpを組み込むとAndoird側でAOT時に怒られてできなくなります。
バグジラにも上がってて対処中のようですが、予定は未定の予感…

まとめ

とりあえず覚えていたもの羅列しましたが、のど元過ぎたら忘れてる感じでほんとはもっといろいろあったような…
けれどどんな開発環境でもなんだかんだいろいろトラブルあるはずだし、XamarinもXamarin.Formsもバグらなければ大変安定して動きます。
いずれにしてもXamarin.FormsはなんだかんだAndroidのことをほとんど知らないような自分でもクロスプラットフォームな開発ができる使える弾丸です。
基本はVMより上のロジックはすべてのプラットフォームで共通化し、多くのUIもXamarin.Formsで共有化する。
どうしても足りない部分やネイティブで作った方が早そうな部分はRendererやネイティブで実装すればいいだけです。
フレームワークは結局便利な部分とフレームワークでカバーできてないところ隠蔽されてどうにもならないところの利点と欠点を併せ持ってると思いますが、Xamarin.Formsはフレームワークに合わないところはいろいろとごにょれるフックがあるのでだいぶバランスもよく多くの場面で問題に対処できるのではないでしょうか。
自分は次の開発でもおそらくXamarin.Formsを使うと思います。

この補足が皆さんのとりあえずのXamarin沼への一歩の一助になれば幸いです。
多分底なし沼だと思いますが、皆で沈めば怖くない(´・ω・`)

改めて

 Xamarin、Xamarin.Formsはいいぞ。F#もいいぞ。