ものがたり(旧)

atsushieno.hatenablog.com に続く

Mono for Android 4.0がリリースされました

http://android.xamarin.com/Releases/Mono_for_Android_4/Release_4.0.0

いきなり4.0と銘打っていますが、当初Mono for Android 2.0と言われていたやつのマーケティング的な番号の変更にすぎないので、実態はこれまで1.9.xとして出していたものとそれほど変わりません。

…と言いたいところですが、実際には1.9.2から内部的には小さくない変更が加えられています。ここには1.9.0のリリース以来何も書いていなかったので、1.9.1-4.0.0の間に行われた変更について、一度まとめておこうと思います。

Android 4.0 (IceCreamSandwich) support

MfA 1.9.2で、ICSに対応しました。Honeycombの時はタブレットを用意してHoneycombGalleryというサンプルを移植して動作確認できたのですが、ICS端末はまだ手元に来るほど出回っておらず、BeamとかNFCとか(あ、かぶってるか)wifidirectとか、新機能を利用するサンプルの多くがデバイス前提なので、とりあえずAPIバインディングは用意したというレベルです。

ICS対応については、重要な変更があります。それは、Android 4.0で加えられたdalvik VMのJNIのメモリモデルの変更によって、従来のMfAでビルドされたアプリケーションが動作しなくなっている(!)という問題に対応するものです。ICS上で動作するapkをビルドするためには、MfA 4.0で改めてビルドし直す必要があります。

(従来のAndroidのJNIでは、local referenceとglobal referenceを区別できなかったため、MfAに含まれるmonoランタイムのGCでも、これらを区別して扱うことはなかった(できなかった)というわけです。JNIメモリモデルの変更は、Androidで今後Copying GCを導入するにあたって解決する必要があった問題と言えます。)

ちなみに、この変更に関連するものとして、MCW (Managed Callable Wrapper)として作成されていたクラスでは、IntPtrだけを取るコンストラクタを、IntPtrとJniHandleOwnershipという2つの引数を取るコンストラクタに変更する必要がありました。もしユーザーレベルでカスタムバインディングコードを作成していた場合は、同様の変更を加える必要があります。

fast deployment support

Titanium Mobileにはfastdevというアプローチがあり、開発中のアプリケーションのapkを全てまとめてインストールすることなく、アプリケーションのコードを変更して実行できるようにするものです。Androidの特にarmエミュレータは非常に遅く、apkをインストールするだけでも何秒も待たされてイライラすることになります。apkをインストールするということは、アプリケーション全体をインストールするということです。

Android SDK r14以降では、apkの作成時に、更新されていないファイルの再アーカイブをスキップする改善が加えられましたが、apk全体を転送しなければならないことに変わりはなく、依然としてしばらく待たされることに変わりはありません。

Mono for Androidの場合、開発時(デバッグ中)のランタイムは、Market等で配布するためのリリースビルドとは異なり、開発機に別途インストールされた共有ランタイムパッケージを利用しています。デバッグ時にビルドされたアプリケーションのapkに含まれて実際に転送されるのは、アプリケーションのコードのみであり、全体をパッケージして長大な転送処理を行うようなことにはなっていません。

そんなわけで、デバッグのパッケージの転送には、長大な時間がかかっていたわけではないのですが(共有ランタイムが入っていない環境でデバッグを開始した場合は、共有ランタイムをインストールすることになるので、これはもちろん時間がかかりますが初回のみです)、fast deploymentでは、この部分にさらに改良を加えます。fast deploymentモードでは、コードにのみ変更がある場合、apkの再インストールは行われません。更新されたファイルのみが転送され、アプリケーションのフォルダ内で更新されます。

fast deploymentモードは、ストレートに実行する場合よりは複雑な処理を伴う以上、安定性に対する不安が無いとは言えないので、プロジェクト設定でoffにすることもできます。

起動時間の短縮

MfAアプリケーションは、apkの中にmonoランタイムが含まれていて、これをネイティブコードとして実行して初期化し、さらにJavaクラスをJNIでやりとりできるようにmono for androidのランタイムに登録するところから始めなければなりません。これが起動時間が長くなる原因なのですが、今回のバージョンではこの部分に大きな改良が加えられ、起動時間を開発者の実機環境で2.451秒から1.141秒までに短縮することに成功しています。

実際には遅延読み込みに切り替えているので、後で必要になったら読み込み処理が行われることに変わりはないのですが、起動時間の改善は多くの人から要望として上がっていたことでもあり、大きな改善になったと思います。

Google Maps API binding

これはMfA 1.9.2でICSサポートと同時に追加されたAPIで、google APIの対応するライブラリのバインディングということになります。Google Maps APIを使いたい人にとっては普通に便利でしょうし、今後追加される機能を示唆するものでもあります。

アプリケーションサイズの縮小

これはMfA 1.9.1で行われた改良のひとつで、実際にはリリースビルドに含まれるアセンブリを大きく縮小したということになります。

MonoTouchやそれ以前のmonoを知っている人は(わたしもこれまで何度か書いていますが)、cecil linker / tunerというツールが、アプリケーションで使用されていないコードを参照アセンブリから削り落とすことが出来るという話を知っているかもしれませんが、MfA 1.9.1では、(それ以前からlinkerは使用されていましたが)これがより効果的に作用するように、Mono.Android.dllに改良を加えています。リリースノートにも例が載っていますが、4MBだったアプリケーションが3MB以下に収まるようになっています。

おまけ: generic API bindingの拡大

従来のMfAではJava genericsが使用されているクラスのバインディングのサポートは限定的なもので、AdapterView<T>みたいな重要なクラスだけが実は手動で追加されているのですが、今回のリリースで、これまでサポートしていなかったジェネリッククラスやジェネリックメソッドに依存するメンバーを拾えるように改良を加えました。

Javaと.NETの両方のジェネリクスに詳しい人は(なかなかいないと思いますが)、これらが性格を異にし相性が悪いことを知っているかもしれません。erased genericやvarianceの違い、constraintの違いなど、問題がいくつもあります。この辺を改良してAPI coverageを広げたいというのがわたしの希望で、今回ある程度納得の出来る範囲でこれを実現できたことになります。個人的に大きいのはParcelable.Creator<T>を取り込めたことで、出来ることが広がっています。がこの話はまた今度。

というわけで

今回のMono for Androidのリリースが、今年最後のものになると思いますが、これまでにない数多くの改善が加えられた the latest and greatest releaseになっていると思います。Mono for Android 1.2の頃と比べて、だいぶ変わってきたと思います。

ちなみに先月はXamarin本社でミーティングがあって、チームメンバーが開発しているものを見せてもらったりしてきたのですが、今後のMfAにも割とビックリするような新機能が追加されそうで、既に公開される日が待ち遠しいところです。

12/10 MfA GC at マーク&スイープ勉強会

来週は id:n7shi:20111113:1321190227 におじゃましてMono for AndroidGCの話をちょろっとだけしてきます。

…のつもりだったのですが、20分くらいあるようなので、mono本体のGCまわりの話からいろいろしようかと思います(本当はMfAのGCまわりでどんな作業が行われていた(いる)か、Android 4.0でどんなことが起こるか、だけでお茶を濁すつもりだったのですが)。今までも各所でちょろっとだけお話ししてきたことですが、わりと大変なんです。

ちなみに僕はGCまわりはど素人に近いし開発にも全くタッチしていないので、実装のディープな話が出来る予定はほとんどありません。昨日ちょろちょろとスライドを作っていたんですが、「さわり」だけになると思います。20分に収まるかどうか微妙な長さの「さわり」ですが…。Ice Cream Sandwichで変わった部分の話なども含める予定です。

というわけで、もし興味を持たれた方がいらっしゃったら是非おいでくださいませ。
http://atnd.org/events/20914

Instrumentationからアプリケーションにフックを仕掛ける

もう2週間以上前に書いて新バージョンのリリースまで待っていたんだけど、いつまでたっても自分の対応コードがまともに取り込まれない事態に業を煮やしたので、先に文章を上げてしまおうと思う。書いたのが古いので多少時間がおかしいのは目をつぶってやって下さいまし。

  • -

Mono for Android(以下MfA)はクローズドソースなので、中がどうなっているのかサッパリ分からないかもしれないが、実は表からでも分かることが少々ある。今日はそのひとつの例として、mono for Androidにおけるmonoランタイムのbootstrapについて、Androidアプリケーションの仕組みに言及しつつ触れてみたい。

MfAのアプリケーションは、debugビルドとreleaseビルドで構成が大きく変わるものの、基本的な構成としては、どちらも通常のAndroidアプリケーションだ。通常のアプリケーションというのは、AndroidManifest.xmlやclasses.dexを含むapkのzipアーカイブになっているということだ。Monoランタイムも、通常のAndroidアプリケーションにおけるネイティブライブラリとしてビルドされて含まれている。

MfAのランタイムは、ContentProviderのひとつとして、ビルドされたアプリケーション上に登録されている。このことはMfAアプリケーションに含まれるAndroidManifest.xmlを覗いてみると分かる。MfAはprovider要素として登録されているのだ。ContentProviderは、attachInfo()の呼び出しによって、アプリケーションのContextやProviderInfoと関連付けられる。ここでアプリケーションの情報をもとにmonoランタイムが初期化されていると想像することは、難しくないだろう(アプリケーションの情報が全く無いと、そのapkにどのライブラリが含まれているかも分からなかったりして、いろいろ不自由がある)。monoランタイムの初期化以前はmonoの機能は何も使えないので、ここでmonoをbootstrapするContentProviderはJavaで書かれている。その実装はJNIを経由したCのコードだ。

いったんmonoランタイムがセットアップされると、そこでJNIのRegisterNatives()を呼び出してJavaクラスを登録すこともできるし、必要に応じてJavaのコードからJNI経由でmonoの組み込みAPIを呼び出すこともできるようになる。MfAは、Android Callable Wrapper (ACW)という名前で、Monoランタイム上のオブジェクト(つまりC#/.NETのコード)からDalvik上のJavaオブジェクトを呼び出すことができる機能を呼んでいるのだけど、このACWを実現するために、C#/.NETのクラスから自動生成されたJavaクラスをRegisterNativesで登録している。

このContent ProviderとしてMfAのような外部ランタイムを登録するアプローチは、ほとんどの場合には適用できるが、実はこれが上手く行かないケースがある。それがInstrumentationだ。

Instrumentationが何をするものか、どう使われるのか、については、Android SDKのリファレンスを参照してもらいたい。Instrumentationについては、前回Native Driverについて書いた時も言及している。
http://www.techdoctranslator.com/android/guide/manifest/instrumentation-element

このInstrumentationは、Applicationよりも前にインスタンスが生成されることになる。もちろんAndroidManifest.xmlに記述されているContentProviderのインスタンスよりも前だ。これはandroid.app.ActivityThreadの中身を読めるようになると分かる。
https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/ActivityThread.java#L3121

Androidのアプリケーションの動作の仕組みまで詳しい人はなかなかいないと思うけど、基本的にはWindows APIのメッセージループ(WndProcとか)、CoreFoundation/Cocoaのイベントループ、X11のXEventのループ、GLibのメインループと似たようなイベントループが存在している。
http://en.wikipedia.org/wiki/Event_loop
ActivityThreadは、アプリケーションのメインスレッドと理解できる。アプリケーションの起動については、ちょうど最近じつに詳細なまとめが公開されたので、参考にすると良いと思う。
http://dsas.blog.klab.org/archives/52003951.html

先のandroid.app.ActivityThreadのソースのリンクは、handleBindApplication()というメソッドを示していて、これは中でApplicationインスタンスの生成を行っている。
https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/ActivityThread.java#L3260

このメソッドの中で、ContentProviderの生成とattachInfo()の呼び出しも行われているのだが、それ以前にInstrumentationの有無で処理が分岐していることに気づくだろうか。もしアプリケーションがInstrumentation経由で作成される場合は、そのInstrumentationのインスタンスがまず生成され、次にApplicationインスタンスが生成される、という流れになっている。

さて、Instrumentationを使用する場合は、通常は派生クラスを定義することで実現するのだけど、MfAでAndroid.App.Instrumentationの派生クラスを定義した場合はどうなるだろうか。Instrumentation(の派生クラス)の派生クラスのインスタンスは、Applicationの生成とContentProviderの生成より前に行われる。しかしContentProviderの初期化フェーズでMfAランタイムが初期化されないと、MfAを利用して定義したクラスに相当するJavaクラスはまだRegisterNatives()で登録できない。

そんなわけで、Instrumentationを利用する場合は、静的にclasses.dexのみでJavaクラスが解決できていないといけない。MfAみたいに独自のライブラリを使ってJNIのRegisterNatives()を使ってクラスを登録しなければならない場合は、Instrumentationにも気をつけたほうが良い。

いったんInstrumentationのクラスを作成したら、あとはそのonCreate()をオーバーライドして、そこに初期化コード(MfAならmonoランタイム初期化コードとか)を追加すると良い。

  • -

というわけで、これを調べてから、Instrumentationをちゃんと動かせるようなコードを書いてうちのチームに投げたのだけど、ニーズがまだ大きくないため*1他の優先タスクの中に埋もれたままもうひと月ちかく経とうとしていて、次のリリースにも含まれそうにない。とはいえ、そのうち含まれることにはなると思う。

*1:そんなことは無くて作れるものが増えるのだけど、対応しないとコードが書けないというchicken and egg問題

slides for スマートフォン勉強会@関東

昨日のすまべんに参加して話を聞いて下さったみなさまありがとうございました。それなりにいろんな方面のモバイル開発者の方がいらっしゃっていたみたいで*1、予想していたより全然キモくない普通じゃーんと思っていたのですが、後半そして懇親会に流れるに連れて以下自粛

昨日のスライドはこちらに置いておきます。今回は読んでもあまり内容が伝わらないかもしれませんが…
https://docs.google.com/present/view?id=dfbvrv4g_44pxz5vhcc

30分で話せる範囲に絞り込んだはずなのですが、後半話すにつれて、もちっと細かく話さないと伝わらないなあと思うようになって、だいぶん申し訳ない気持ちになったのですが、その辺はおいおいフォローアップしていければと思います。

あと最後に話した件ですが、store... に付けてください。現場にいなかった人でも大丈夫です。

*1:ちなみに本来は開発者対象の集まりですらないらしい、というのは昨日初めて知りました

Mono for Android from Xamarin, at スマートフォン勉強会@関東

事前にお知らせするのを忘れてましたが…表題のイベントでMono for Androidのお話をさせていただくことになりました。
http://sumaben.jp/?SPWorkshopKanto15

実のところ一度も「すまべん」には参加したことが無いので参加される皆さんがどういうキモヲタ層の方々なのか存じあげないのですが、30分なので手短にお話しすることになると思います。C#で書くとこんなに楽チン!みたいな話は、まあしてもいいというかMfAのウリではあるはずですが、今更感があるので、そういうのは少なめにして、なるべく仕組みなど応用のきく話をしようかなと思います。「使い方」を勉強したい人の集まり、ってわけでもないよね…? (そんなに*MfAの*ユーザーの人が集まれるほどいたら喜んで勉強会開きますw)

MonoDevelop 2.8

2.6が出てからまだ1ヶ月も経っていないのですが
http://monodevelop.com/Download/What%27s_new_in_MonoDevelop_2.8

2.8が出たのは主にMac環境のリニューアル(XCode4とか)に伴うマネジメントサイドの都合です。XCode4まわりの後方互換性の欠落が理由で、MonoTouchプロジェクトのファイルも後方互換性のないアップグレードをさせられることになるので、気をつけたほうが良いようです。

Ubuntu 11.10の準備をしていたコミュニティの人が「最新の2.6を出せると思ったらまた古いバージョンでリリースだよ…」などとぼやいていましたが、まあ非Mac環境ではそんなに大掛かりな変更はないと思います。

Android AOSPのXMLをjavaクラスから生成する

めでたくMono for Android 1.9がリリースされたので、わたしがXamarinで最初にやっていた仕事のはなしを書こうと思う。いや実は書いたのもう先月だったんですけどね。リリース出たら出そうと思ってたらもう9月も終わりそうな。

Mono for Androidでは、Androidのソース (AOSP) に含まれるAPI定義XMLからクラスライブラリを生成している。しかし、HoneycombはGoogleソースコードを公開していないクローズドソースのフレームワークであり、Mono for Androidがこれに対応するのは不可能だ。

というわけで、XamarinではAOSPのXMLに相当するものをandroid.jarから生成して対応することにした。既に基本的なjavaクラスライブラリ解析部分とXML生成部分は他のハッカーがやっつけていたのだけど、それを製品に適用できるところまでもっていく人手が必要だったというわけだ。このコードは一応Xamarinのgithub上でも公開されている。ただ、AOSPに特化したもので、汎用的ではない。

そんなわけで、今回はこのXML生成ツールの(というか、それに絡む面倒な問題の)はなし。XMLからコードを生成する部分はMono for Android独自の話題なので書かない。

変換処理の基本

初期のコードは単純だった。変換のためのライブラリ解析部分はごく単純にJavaのリフレクションAPIjava.lang.reflect)を利用して行われており(Javaで書かれている)、XMLの生成はDOMとJAXPで行われている。これだけなら時間さえあれば誰でも出来るだろう。

しかし、ほどなくして、単純な実装のアウトプットと、実際にAOSPで公開されているXMLを比較すると、だいぶ違うことに気が付いた。われわれのコードジェネレータはAOSPのものを前提として作られているので、あまり独自形式で出力するわけにもいかない。かくして、AOSPとの違いを探索する作業が始まった。

定数値の取得

今回の変換処理では、static finalなフィールド(たとえばMath.PIなど)の値がもし取得可能であれば取得することになるのだけど、これが必ずしもリフレクションで取得できるとは限らない。

  • boolean型のフィールドについてField#getBoolean(null)を呼んでも、その戻り値がfalseであれば、それが規定値なのか、単に値が存在しないのかすら分からない(これはField#get(null)でも同様)。
  • また、DoubleのINFINITYなど特殊な定数の値は数値では表現できないため、実はAOSPでも “1 / 0” だの “-1 / 0” だのといった文字列表現になっている(つまり単純な自動出力ではない)。
  • さらに、protectedなフィールドの値を取得しようとするとIllegalAccessExceptionになってしまう。

これらの問題のいくつかは、リフレクションをやめてasmバイトコード解析することで解決した。

proguardの洗礼

android.jarは、単純にjavacでコンパイルされて生成されたものではなく、その後にproguardを使用してコードが調整されている。たとえばその過程でメンバーに付加されていたはずのアノテーションが消滅しており、@Deprecatedを拾うことができなくなっていた。ちなみにクラスに付加されていた@Deprecatedは残っている。

ClassLoaderの動作による混乱

アノテーションの問題は、javaのリフレクションにかかる制約もあいまって、だいぶややこしい事態になる。Javaランタイムに含まれるクラスの情報は、外部のURLClassLoader.loadClass()などから呼び出しても、ランタイム自体から返されてしまうということにある。つまり、android.jarに含まれるjava.*クラスの情報を取得しようとしても、実行しているJREの情報しか取得できない。(これは.NETのリフレクションで外部にあるmscorlib.dllをロードした場合にも制約がついて回ることと似ている。)

その結果、アノテーションは、java.*クラスについては取得できる(ただしandroid.jarから正しく取得しているわけではなく、JREの情報を取得しているに過ぎない)が、android.*など他のクラスについては取得できない、という、実態に合わない結果が返ってくる。

ちなみにJava 6のJREandroid.jarに含まれるJavaのクラスは、AOSPのXMLから判別できる限りでは、javax.net.ServerSocketFactory.getDefault()がsynchronizedであるかそうでないかの違いがあるだけで、後は全て一致しているので、JREからクラスをロードしていることにはあまり不安要素は無い。

バイトコードの制約

Javaバイトコードに関係する制約として、まず分かりやすい問題として、JavaのリフレクションAPIでは、メソッドのパラメータ名を取得することができない。これは仕方ないので、全てAPIリファレンスをscrapingして得ている。

バイトコードで悩ましいのは、一部の(全てではない)ジェネリクス情報が消えてしまうこと(いわゆるerased generics)だ。ソースコード上にあった情報が、バイトコード上では消えてしまって取得できない。AOSPのXMLでは、ジェネリック型の型引数情報が消滅していることが多い(これも全てではなく、条件の整理が必要になるだろう)。

ジェネリクス情報が消えてしまうと、ジェネリックインターフェースを実装しているクラスで実装する必要のあるメソッドのシグネチャーも変わってしまう。たとえば、IComparableであれば、compareTo(T)を実装しなければならないが、getDeclaredMethods()ではcompareTo(Object)が(も)返ってくる(compareTo(T)と2つ返ってくるのは、ひとつはコード上の明示的な実装で、もうひとつはインターフェースメソッドとして自動的に返されるもの、ということだろう。次のセクションで触れる)。

Class.getDeclaringMethods()の戻り値のフィルタリング

AOSPのXMLは、オーバーライドされたメソッドの扱いについて非常に不可解で、派生クラスで宣言されたメソッドが含まれたり含まれなかったりしている。その条件は不可解で、わたしはこれを無理やり実装し終えた今でも、AOSPのXMLの内容を合理的に説明できない。(たとえば、AdapterView#setAdapter()の宣言が欠けているにもかかわらず、java.security.Provider#put()が含まれている理由を説明できない。説明できないので、後者は手作業であえてフィルタリングせずにXMLに出力している。)

解決済みだがハマった問題のひとつとして、Javaでは、基底クラスでnon-abstractであるメソッドと同じシグネチャーをもつメソッドを、派生クラスでabstractとして定義できてしまう。これによって、多くのクラスでtoString()メソッドがabstractとして再宣言され、XMLにも含まれているのを発見した。これらは先のオーバーライドされたメソッドの中でも、排除されないメンバーとして扱うことになる。

また、AOSPでは、java.lang.StringBuilderとjava.lang.StringBufferの共通の基底クラスであるjava.lang.AbstractStringBuilderが消滅している。この問題は、Javaの言語仕様上、publicなクラスがnon-publicな基底クラスを持ててしまうという(.NET開発者にとっては信じがたい)設計になっていることに原因があって、このAbstractStringBuilderはまさにnon-publicな基底クラスというわけだ。そしてこのクラスでgetDeclaringMethods()を呼び出したときの戻り値リストから、AOSPのXMLに合わせる作業も厄介なものになる(実のところ完全には行っていない)。

また、Class.getDeclaredMethods()が、インターフェースのメソッドのうち、宣言していないものを自動的に返してくるのだけど、これが基底クラスと派生クラスでジェネリック型引数が変わる(たとえば具体化する)と、改めて別のインターフェースメソッドが返ってくるので、混乱が生じる。これはジェネリック型消去の有無でさらに難しくなる。

余談だが、Javaではprotectedメソッドをpublicでオーバーライドすることが出来てしまう。Androidアプリケーションのサンプルの多くはActivityクラスのOnCreate()をpublicメソッドとしてオーバーライドしているが、.NETではprotectedでオーバーライドすることになる。

AOSP XMLのバグ

最後に、AOSPのXMLには、細かいところでさまざまな不足部分があって、クラスライブラリを正確に反映していない。GoogleにどれだけAOSPにコミットする気があるかを見るためのひとつの試金石として、androidのissuesに登録しておいた。
http://code.google.com/p/android/issues/detail?id=19569

ちなみにAOSPのXMLに相当するものは、最新のソースコードについてはAOSPのビルド過程でも生成される(out/target/common/obj/PACKAGING/android_jar_intermediaries/public_api.xml)しアノテーションが付加された状態のdebug jarもout/target/common/obj/JAVA_LIBRARIES/framework_intermediariesとかout/target/common/obj/JAVA_LIBRARIES/core_intermediariesとかに生成されるのだけど、AOSPのビルドには相当時間がかかるので、とても追求する気にはなれない。古いバージョンでも同じようにファイルが生成されるかどうかは分からないし、古いバージョンをビルドするにはJava 6とJava 5を切り替えたり…とても考えたくない。

まとめ

androidのライブラリをJavaリフレクションで眺めるとたいへん面倒だということがわかった。これは特に、ランタイムライブラリが分析対象と異なること、proguardが一枚かんでいること、Javaのerased genericsの問題、が大きい。でも、その辺の壁を乗り越えれば、フレームワーク全体を見通した汎用的なツールが作れるようになるかもしれない。