ものがたり(旧)

atsushieno.hatenablog.com に続く

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問題