インサイドXamarin(10)
Xamarin.AndroidにおけるJava相互運用の仕組みと、Javaバインディング・プロジェクト
Xamarin.AndroidでJavaとの相互運用を実現するアーキテクチャについて、さらにメモリ管理などの注意点を説明。さらにXamarin.Androidの制限事項についても解説する。
第7回からはXamarin.Androidについて取り上げている。前回の「Xamarin.Androidアプリの開発」に続き、今回はJavaとの相互運用などについて説明する。
Javaとの相互運用
Java相互運用のアーキテクチャ
Android SDKでは、標準的なJavaではないが、ほぼJavaのやり方が通用する。AndroidアプリケーションのJavaコードは、開発者によっていったんJavaのバイトコードにコンパイルされた後、Android SDKに含まれるビルドツール「dx」によって、Dalvikが実行できる「dex(Dalvik Executable Format)」というバイトコードに再度コンパイルされる。Dalvikでは、JNI(Java Native Interface)を使用することが可能で、C++で書かれたネイティブコードを呼び出すことができる。AndroidプラットフォームはLinuxカーネル(kernel)の上で動作しており、一方で「Bionic llibc」という、BSD由来のlibc実装を使用しており、Android NDKにもこれが含まれる。
Xamarin.Androidには、Android NDKを使用してビルドしたmonoランタイムの組み込みライブラリが含まれており、これはアプリケーションのリリースビルドのパッケージ時に、依存ネイティブライブラリとしてAndroidアプリケーションパッケージ(=.apkファイル)に同梱される。Xamarin.Androidアプリケーションのスタートアップ時には、このmonoランタイムがJNIの仕組みに基づいてロードされる仕組みになっている。Xamarin.Androidアプリケーションのブートストラップについては、筆者が以前に横浜Androidプラットフォーム部の勉強会で行ったセッションの資料でも、詳しく説明してあるので、興味がある方はそちらを参照されたい。
ランタイムのブートストラップは、ネイティブコードとJavaの相互運用の一部にすぎない。Xamarin.iOSがC#でObjective-Cの型を派生させられるのと同様、Xamarin.AndroidはC#でJavaの型を派生させることができる。実際に行われていることを正確に言えば、Xamarin.Androidでは、Java由来の(CILで実装された)クラスから派生したクラスをC#で定義でき、それに対応するJavaのグルー(glue: 接着用)クラスが、必要に応じてJavaメソッドをオーバーライドした上で、C#で書かれたロジックをJNI経由で呼び出すようになっている。
このグルーとなるJavaクラスを「Android Callable Wrapper(ACW)」という。これは、Dalvikランタイムが動的にクラスを生成できないという制約上、回避できないことだ。
逆に、Javaクラスを呼び出すものとして生成された.NETのクラスを「Managed Callable Wrapper(MCW)」ということもある(通常あまり気にする必要はない)。Mono.Android.dllはこのMCWの巨大な集合体だ。
これらのJava統合のアーキテクチャについては、公式サイトに比較的詳しいドキュメント(英語)が公開されている。また、Xamarin.AndroidからJNIを使い倒す方法については、公式サイトに膨大でとても読めない量のドキュメント(英語)が1ページにまとめられているので、興味がある方はそちらを参照されたい。通常のアプリケーション開発において、この知識はそれほど重要ではない。Javaライブラリを使う場合は、後述するバインディング・プロジェクトを使うとよい。
メモリ管理
Xamarin.Androidでやや面倒なのが、メモリ管理を気にしなければならない点だ。monoランタイムとDalvikランタイムは並行動作している。monoランタイム上にあるオブジェクトは、Dalvik上のJavaオブジェクトとライフサイクルを共にしなければならない。Java側から使われるかもしれないオブジェクトを、mono側で解放するわけにはいかないのである。Dalvik側のGC(ガベージコレクター)をほしいままに操作することはmonoランタイム側ではできないので、mono側のGCにフックを仕込んである。
開発者がコードを書くときに意識すべきことは、DalvikのGCとmonoのGCは別々の基準で別々の動作を行っている、ということである。例えば、巨大な画像リソースをAndroid.Graphics.Bitmap
クラスで読み込んだとしよう。このメモリはDalvik側が管理しているもので(もちろん実際にはJavaのヒープではなくネイティブコードのメモリであろうが、Dalvikの管理下であることに変わりはない)、mono上のメモリをいくら解放しても、この画像リソースが解放されることはない。
GCは別々に動作しているが、ある程度のJavaオブジェクトが生成されて閾値に達した場合、mono側でフルGC(monoのGCは世代別である)を呼び出すことで、Java側の不要なオブジェクトの回収を期待する、ということはmono側で行われている。どうしても手動でJava側のメモリを解放したいけどmono側のJava参照が邪魔になっている場合は、Java.Lang.Object
のDispose()
メソッドを呼び出すことで、危険ではあるが参照を解放できる。
Java.Lang.Object
クラスから派生するオブジェクトは、全てGCルートとして登録されるので、そのようなオブジェクトを大量に生成すると、パフォーマンスを劣化させることにもなる。特別にマーシャリングの必要がなければ、Java.Lang.String
型よりもSystem.String
型のインスタンスを生成する方が、一般的にはコストが低い(マーシャリングにおける、内部的なJavaオブジェクトへの変換など、複雑な要因が絡むので、一概に良いとは言えないが、GCルートを大量に抱え込むことは、一般的にパフォーマンスの悪化につながる)。
ガベージコレクションの協調動作の詳細やメモリ効率化のTipsは、公式サイトのドキュメント(英語)を確認されたい。
ExportAttribute
Java相互運用の補足として、ExportAttribute
という機能について説明しておきたい。
ACWのJavaコード生成は、自動的に行われるものなので、通常はその内容を意識する必要はない。しかし、生成されるJavaコードが重要である場合もある。Java側のフレームワークがリフレクションなどを使用して、メンバーを動的に取得する場合などだ。
例えば、java.io.Serializable
インターフェースでは、readObject()
、writeObject()
というメンバーを要求するが、これらはインターフェース定義には含まれていないので、実装クラスのACWにも出力されない。
またAndroidには、Parcelable
インターフェースという、インテント通信におけるデータのやりとりに使われるシリアライゼーション機構がある。この機構では、デシリアライズするときに、対象クラスから「CREATOR」という、クラス上に定義のないstatic
フィールドを取得して、それを使用してデシリアライズされるインスタンスを生成する。C#コードでいくらこのメソッドを定義しても、明示的にそのメソッドが使われない限り、ACWには生成されないし、明示的に使ったとしても、Java上で定義される名前は「CREATOR」にはならない。
これらの場合に必要となるJavaコードを、半ば手動で生成できるようにするために、Xamarin.AndroidではExportAttribute
という属性をメソッドに指定することで、そのメソッドに対応するJavaメソッドを、指定した名前で生成できる。同様に、ExportFieldAttribute
をメソッドに適用すると、対応するJavaフィールドを生成した上で、初期値がそのメソッドの呼び出し結果に初期化される。これらは場合によっては必要になる、Java相互運用のトリックだ。
Javaバインディング・プロジェクト
Javaバインディング・プロジェクトは、既存のJavaライブラリの.jarファイルを解析して、その型とメンバーに対応する.NETのAPIを生成するためのC#プロジェクトだ(F#はサポートしていないが、ビルドされるものはほぼ自動生成されるコードでしかないし、サポートする必要はないだろう)。
使い方は簡単で、.jarファイルをプロジェクトに追加して、「EmbeddedJar」というビルドアクションを指定すれば足りる。あとはビルドするだけだ。
Javaバインディング・プロジェクトのビルドは、次の3つのステップで行われる。
- .jarファイルを解析し、API定義を生成する。これはXML形式になっている
- API定義のXMLデータから、C#のコードを生成する
- C#のコードをビルドする。ここには、自作のC#ソースコードを追加することもできる
生成されるC#のコードには、第8回の「Mono.Android.dll」の節で説明した、C#化が施された内容になっている(enum
化やasync
対応メンバーの追加などは、手作業で行わなければならない)。
現実には、.jarファイルを追加してF5キーを押しただけでビルドが成功することはあまりない。実際にビルドしてみると、ビルドエラーが多発する。jarライブラリをバインドしようとする開発者は、これを全て修正しなければならない。実のところ、Javaバインディング・プロジェクトを使いこなすのは非常に難しい。.NET Frameworkに、WSDLからC#のSOAPクライアントを自動生成するツール「wsdl」とか「svcutil」といったツールがあるが、あれらが.NETから自動生成されていないWSDL文書を処理できないのと同じかそれ以上に、自動的には機能しない。
なぜビルドエラーが多発するのかというと、ひとえにJavaとC#の設計の違いにある。継承1つとっても、Javaではnon-publicなクラスからpublic
なクラスを派生させられる。C#ではできない。Javaではprotected
なメソッドをpublic
でオーバーライドできる。C#ではできない。消えているジェネリック(Generics)のメンバーが関与してくれば、それに対応する修正も必要になる。原因はさまざまである。
ともあれ、ビルドエラーを修正する方法はある。Javaバインディング・プロジェクトには「Metadata.xml」というファイルが含まれている。これは、XPathで修正対象ノードを記述して、修正方法を指定するものだ。これをじっくり説明するとかなりの長文が必要になってしまうので、ここでは公式サイトのドキュメント(英語)へのリンクを示すだけにとどめておきたい(このドキュメントには、半ばウォークスルー・ドキュメントであるが、追加ソースコードを作る場合の制限事項など、それなりに重要な説明も含まれている)。
一番簡単なTipsを書いておくと、ビルドエラーになっている型に対応するAPI定義XMLのノードを、<remove-node path="(xpath)" />
と指定すると、その問題のノードのバインディングが生成されなくなる。
Javaバインディング・プロジェクトには、もう一つワナがある。これはJavaのライブラリではなく、Androidのライブラリに特有の問題だが、Xamarin.AndroidではないJavaのAndroidでも「ライブラリ・プロジェクト」があり、これはAndroidのJavaアプリケーションをビルドする際にライブラリとして参照して使用する。このライブラリ・プロジェクトの.jarファイルには、実際にアプリケーションをビルドして実行するために必要なファイルの全ては入っていないのである。具体的には、.jarファイルの中にassetsやres、ネイティブライブラリ(*.so)が含まれていない。jarライブラリはあくまでコンパイルされたJavaクラス群でしかないのである。これでは参照するときにもライブラリ・プロジェクトをソースからビルドしなければならず、再利用性が悪い。
ともあれ、そのような「ライブラリ・プロジェクト」をXamarin.Androidでバインドする場合は、単に.jarファイルをバインドするだけではなく、関連するリソースなどを全て含めて.zipアーカイブファイルとしてプロジェクトに追加する必要がある。このためのビルドアクションとして「LibraryProjectZip」というものがある。
もう少しお手軽なものとして、「LibraryProjectProperties」というビルドアクションもあって、これはJavaのAndroidライブラリ・プロジェクトのproject.propertiesファイルを指定すると、必要な.zipファイルを自動的に作るというものだ(ただしその場合でも、一度、Eclipseなどでライブラリ・プロジェクトをビルドしておかないと、必要な.jarファイルが見つからないことになる)。
なお、2013年にAndroid SDKの新機能としてMaven(=Java用プロジェクト管理ツール)サポートが追加され、新しく「.aar」という拡張子のライブラリ・パッケージが登場した。この機能は、長い間、Android開発者が解決を求めていたAndroid SDKの仕様の1つだ。この.aarファイルは、実質的にXamarinが「LibraryProjectZip」で実現していたことと同じなので、XamariのJavaバインディング・プロジェクトで、.aarファイルを「LibraryProjectZip」として追加することもできる。
最後になるが、Xamarin.Androidのサンプルには、ActionBarSherlockやFacebook SDK for Androidのバインディングなど、数多くのプロジェクトが含まれているので、具体例としてはそれらを参照されたい。
また、(多少古い部分はあるが)筆者がJavaバインディングライブラリの詳細についてXamarinユーザーグループのイベントで解説した時に使用したスライドもあるので、こちらも参考にされたい。
Xamarin.Androidの制限事項
Xamarin.Androidの制限事項は、iOSに比べると少ないが、それでもいくつか気にすべきことがある。
- Androidでは、Javaのクラスを動的に生成できない(詳しくはこちらの記事が参考になる)。そして、ビルド時に使用していないコードは、アセンブリ・リンカーによって削除されてしまうことから、DLR(動的言語ランタイム)によって動的に型を読み込む場合には、その型がきちんとリンク後も残るようにしなければならない。
- Xamarin.AndroidのC#/F#コードからのJavaコードの呼び出しは、ビルド時に生成されるJavaのコードを経由して行われる。通常は生成されるJavaコードを気にする必要はないが、Javaのライブラリによっては、リフレクションで動的にJavaクラスのメンバーを拾い上げることがある。このJavaコードの生成は、前述の
ExportAttribute
などを使えばある程度自由に生成できるが、例えばコンストラクターの生成などはできない。
- ジェネリックメソッドで
ExportAttribute
やExportFieldAttribute
を使うことはできない。これは実行時にJavaコードが生成できない制約による。
- .NETのジェネリック型のインスタンスを、Dalvik側からインスタンス生成することはできない。Dalvikからのインスタンス生成とは、つまり、Androidのフレームワーク側がインスタンスを生成することを意味する。例えば
Activity
の派生クラスは、アプリケーションのスタートアップなど、Androidのアプリケーションフレームワークが生成することになる可能性が高いから、これは回避すべきクラス設計だ。
次回の予定
さて、以上かなり長くなってしまったが、Xamarin.Androidについての解説は以上である。これでもそれなりにはしょっている部分は多いので、気になる部分は適宜、公式サイトのドキュメント(英語)を参照されたい。
次回はXamarin Studioについて解説する。
※以下では、本稿の前後を合わせて5回分(第8回~第12回)のみ表示しています。
連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
8. Xamarin.Androidで使用するライブラリ
Androidの.NET APIに相当する「Mono.Android.dll」の特徴と注意事項、さらにAndroidサポートパッケージやGoogle Play Servicesについて説明する。
10. 【現在、表示中】≫ Xamarin.AndroidにおけるJava相互運用の仕組みと、Javaバインディング・プロジェクト
Xamarin.AndroidでJavaとの相互運用を実現するアーキテクチャについて、さらにメモリ管理などの注意点を説明。さらにXamarin.Androidの制限事項についても解説する。
11. Xamarin Studio/MonoDevelopの基本機能と、C#コーディング補助機能
MonoDevelopとXamarin Studioはどう違うのか? MonoDevelopの基本的な機能を解説。C#コーディング補助機能についても紹介する。
12. MonoDevelopにおけるビルド/実行/デバッグと、iOS/Android向けのGUIデザイナー
MonoDevelopでアプリをビルド/実行/デバッグするための機能を解説。iOS/Android向けのGUIデザイナーや、MonoDevelopのカスタムアドインについても紹介する。