Xamarin.AndroidでもInstant Run (cold swap) がしたい!


これはXamarin Advent Calendarの空白の最終日を埋めるべく書かれたエントリーです。


今回はXamarin.Android 7.0に新しく追加された…ことになっている…Improved Fast Deploymentについて解説します。

ちなみに、Android Studio 2.0のInstant Runの仕組みを解読するの内容を把握している人向けにひと言で書くと、今回のImproved Fast Deploymentとは、これまで出来ていなかった…ことになっている…dex事前コンパイルと分割アップロードをXamarin.Androidでも実現した、という(だけの)ことです。

まあ、わたしの知る限り、Xamarin.Androidを使っていて、かつ、きちんとInstant Runの挙動まで把握している人は、おそらくほぼ皆無なので(知っていたYO! という人はぜひ教えてください! トモダチになりましょう(!?) )、引き続き読み進めてください。

そもそも、Fast Deploymentとは何か?

まずFast Deploymentについて説明しましょう。

Xamarin.Androidには、デバッグビルドとリリースビルドが存在します。これらは、通常の.NET Frameworkアプリケーションでは、ほとんど違いが意識されることが無かったと思います(デバッグシンボルの有無やコードの最適化オプションくらいでしょうか)。Xamarin.Androidでは、実はこれらは全くと言っていいほど異なるものです。(Xamarin.iOSも、これらのビルドの挙動は大きく異なるのですが、話が広がりすぎるので今回は議論しません。)

なぜそのような違いが生じるのでしょうか? その答えは、Androidアプリケーションのフルビルドが、とにかく長いことにあります。開発者は、通常のAndroidアプリケーションapkのビルドを、数十秒の単位で待たなければなりません。あまりにも長いので、ビルドを開発用に特化させてビルド時間を短縮できないか、ということになるわけです。ちなみにこれはXamarin特有の問題ではなく、Android Java開発にも当てはまる話です。

なぜAndroidアプリケーションのビルドが長いのかというと、ビルドのために行う作業が多数あって、最終的にインストールしてデバッグするアプリケーションのapkをビルドするためには、全てを実行しなければならないからです。Google Playで配布できるようなXamarin.Androidアプリケーションのapkには、(1)AndroidManifest.xml、(2)マネージドコードのdll、(3)monodroidランタイム(monoランタイムを含む)、(4)Java Callable WrapperのjavaクラスなどをDalvikバイトコードに変換したclasses.dex、(5)Androidリソースをコンパイルしてzipしたパッケージ、(6)その他の依存ネイティブライブラリ…などが必要になります。これらのどれかひとつでも更新されたら(そしてdllはC#コンパイラでビルドするたびに更新されます)、apkをビルドしなければなりません。

もうひとつ、アプリケーションをデバッグするためには、そのアプリケーションをデプロイ(インストール)しなければなりません。Xamarin.Androidのアプリケーションは、時には数十MBにもなります。数十MBのapkをAndroidターゲットに転送してインストール処理を実行するのにかかる時間は、ばかになりません。assembly linkerを使えばこれを縮小することが出来るのですが、リンク処理には時間がかかります。今はビルド時間を削減したいのですから、転送時間を減らすためにサイズを減らすためにビルド時間を伸ばすのでは、本末転倒です。

もちろん、ビルドシステムも盲目的にビルド作業を先頭から実行するわけではありません。たとえば、dllをビルドする時に、ソースファイルの最終更新時間が、コンパイル結果となるdllの最終更新時間よりも古い場合は、ソースファイルに何ら変更は加えられていないので以前のビルド結果を使い回すことが出来るはずです。という感じで、モダンなビルドシステムには通常、各ビルドタスクの入力ファイルとビルドの出力ファイルをタイムスタンプで比較して、出力のほうが新しければスキップする、という機能が入っています。

MSBuildの場合、ビルド対象は「プロジェクトファイル」(csprojとかvbprojとか。slnはちょっと特別で、複数のプロジェクトをまとめてビルドしているだけ)で、通常はその中で指定されているImport要素が指定する.targets と(慣例的に)名付けられたファイルで、それらのビルドタスクが定義されています。そして各ビルドタスクには、InputsOutputsが定義されています。Inputsで指定されたファイルがOutputsで指定されたファイルより古い場合は、そのタスクはスキップされます。基本的には、無駄なビルドタスクは行われないように設計されています。

しかし、いずれにしろ、毎回ビルドしてデバッグするたびにこのデプロイメント作業が発生するのでは、時間がかかりすぎてしまいます。

Xamarin.AndroidのFast Deploymentとは、この「遅すぎるビルド」問題を、トリッキーな方法によって解決する仕組みなのです。

これまでのFast Deploymentの仕組み

では、ここからは、Fast Deploymentがどのような仕組みで実現しているかを見ていきます。再び先のInstant Runの解説記事に戻りますが、これまでのFast Deploymentは、基本的にdllをInstant Runのdexのようにアップロードする仕組みです。コレで分かった人はこの節を読む必要はありません。

Fast Deploymentが有効になっていると、アプリケーションの累積的なビルドの際に、apkをビルドしてインストールする場面が少なくなります。

Xamarin.Androidアプリケーションのロジックの実体はdllとなってアップロードされますが、AndroidフレームワークがDalvikバイトコードをロードする機構ではなく、mondroidランタイムがマネージドコードをロードする機構については、Xamarinがコントロールする余地が大いにあります(何しろ自分たちで作っているわけですから)。具体的には、monodroidランタイムは、apkの中身だけではなく、任意のファイルパスにあるdllファイルをロードするように、動作を調整できます。もしそれが可能なら、apkの中には、アプリケーションのdllをパッケージする必要がなくなって、アプリケーションをデバッグする時は、apkを再インストールする代わりに、更新されたdllファイルだけをAndroidターゲットにアップロードすれば良いのです。これは、毎回のデバッグに際して行われる転送量と作業時間を、大いに削減してくれます。

さらに、mscorlib.dllやSystem.dll、Mono.Android.dllなどについては、アプリケーションごとに転送する必要すら無いはずです。mscorlib.dllやSystem.dllはどのアプリケーションでも同じですし、所定のTargetPlatformを使っているアプリケーションについては、同一のMono.Android.dllを使用しているはずだからです。Xamarin.AndroidにおけるDebug Runtime APKやPlatform APKと呼ばれているパッケージは、実のところこのためにあります。これらは、共通のdllを他のアプリケーションから要求された時に提供するためのパッケージとして機能しているのです。

もちろん、この仕組みで実際にアプリケーションをデバッグモードで動かすためには、更新が必要なファイルを検出して、apkインストール後に転送するデバッグ プロトコルが必要になります。これらは残念ながらxamarin-android (OSS) には含まれていません。(わたしはOSSコミュニティがデバッグ機能を改善でき、カスタムビルドしたxamarin-androidもIDEと組み合わせて使えるようにするために、これもOSSに含めるべきだと主張しているのですが…)

ともあれ、これがFast Deploymentの実態です。正確に言えば、従来のFast Deploymentの実態です。

新しいFast Deploymentは何が「改善された」のか?

Xamarin.Androidにおけるアプリケーション ロジックはマネージドコードであり、dllが更新されてもapkの再インストールは発生しない、ということは、C#のソースにどのような変更を加えてもapkが再インストールされることはない、ということでしょうか?

実はそうではないのです。

Xamarin.Androidアプリケーションが動作する仕組みを思い出して下さい。.NETのアプリケーション ロジックでは、JavaのAPIを呼び出しますし、逆にAndroidフレームワークがアプリケーションで実装されている(はずの)Java APIを呼び出すこともあります。Androidフレームワークからのマネージドコードの呼び出しを正しく処理できるように、Xamarin.Androidはアプリケーションのビルド時に「Javaスタブコード」を生成します。Java Callable Wrapper、JCWとも言われるやつです。

Mono.Android.dllに含まれるAPIを含んでいるJavaのAPIはandroid.jarです。ただし、android.jarがapkに含まれることはありません。android.jarはただのブラフです。PCLのbait and switchにおけるbaitのようなものです。android.jarに相当するDalvikバイトコード化されたライブラリは、既にAndroidターゲットのプラットフォームに含まれており、Xamarin.Androidアプリケーションがmonodroidランタイム経由でAndroid APIをJNIで呼び出した時、Dalvik/ARTのランタイムはこれを問題なく読み込んで実行してくれます。

先に述べたJCWは、マネージドコードの中でAndroid APIをオーバーライドすると生成されます。Mono.Android.dllに含まれない、アプリケーション固有のJava呼び出しについては、JCWをビルドする必要があります。…あれ? このJCWはdllではありませんね? monodroidランタイムが自前でカスタマイズできたのはdllの読み込みだけでした。JCWはJavaコードなので、最終的にはclasses.dexの内容になります。これはapkにしか含まれていません。apkを再インストールしなくても良いのでしょうか?

…良くありません。JCWに変更が加わると、結局apkを再インストールしなければならないのです…

これが、従来のFast Deploymentの限界でした。

ではこれを改善する方法は無いのでしょうか? すなわち、Javaコードを変換して得られたclasses.dexが更新された場合にも、これをapkに含めずに別途アップロードする方法は無いのでしょうか?

あります。それが、Android開発ツールについて2015年11月に新機能として発表されたInstant Runと呼ばれる機能(の一部)なのです。

Instant Runでは、Java…のコードを変換して得られたdex…のClassLoaderを差し替えたダミーのandroid.app.Applicationクラスをでっち上げて、その中からアプリケーションの実際のコードを動的にカスタムClassLoaderで呼び出して実行します。このClassLoaderは、apkの中からではなく、所定のファイルパスからdexファイルをロードできるようになっています。つまり、Xamarin.AndroidのFast Deploymentと同じことを、dexについてやっているわけですね。

従来型のapkのローディング モデルだと、全てのJavaクラスはひとつのclasses.dexにまとめられてロードされなければならなかったので、このファイルをビルドするために毎回のJavaビルドでandroid support v4やGoogle Play Servicesなどのライブラリが全てリンクされdexに変換されていました。非常に時間のかかる処理です。これが、dexについてもビルド キャッシュが効くようになれば、ビルドが非常に早くなります。実際、Instant Runを有効にしたAndroidアプリケーションのデバッグ実行は、1秒以内に始まることも少なくありません。

今回のXamarin.Android 7.0のImproved Fast Deploymentとは、このアプリケーション モデルに相当するものを、こちらにも適用して、JCWの更新があった場合でも、JCWをコンパイルして得られたdexはapkとは別にアップロードされるので、再インストールに時間がかかることはありません。これは、アプリケーションの種類によっては、大いにデバッグ時間の削減に繋がることでしょう。

改善されたFast Deploymentを適用するには?

この機能、実は2016年1月くらいにはだいたい出来上がっていて、cycle7(もしかしたらcycle6)の時点で既に使えていたのですが、IDE統合が出来ていないだの(結局IDEは何もしないのですが…アドインのパッチまで作って送ったのに…)、QAが何もテストしていないだのといった理由でどんどん公開が後回しになって、ようやくcycle9で出てきたという体たらくです。その間にMSBuildタスクの実装がいろいろいじられて、他所のregressionがfast deploymentの実装のバグ扱いされたり…そんなわけで、一応まだα版という状態です。

さて、そんなfast deploymentですが、有効になるためにはMSBuildプロパティAndroidFastDeploymenTypeの値がAssemblies:Dexesになっている必要があります。使用しているXamarin.Android の Xamarin.Android.Common.targets (インストール先から探して下さい(!) )の中でデフォルト値としてコレが指定されていれば、アプリケーション側で手作業で追加する必要はありません。もし指定されていなければ、明示的に.csprojに追加するか、xbuild/msbuildを実行する時に /p オプションで明示的に指定する必要があります。

<PropertyGroup>
  <AndroidFastDeploymenType>Assemblies:Dexes</AndroidFastDeploymenType>
</PropertyGroup>

… or …

$ xbuild FooBar.csproj /p:AndroidFastDeploymentType=Assemblies:Dexes

これで、状況によっては、インクリメンタル ビルドが爆速になることでしょう。(初回ビルドではライブラリのjarをdexに変換する作業なども行われるため、遅くなる部分もあるかもしれません。)

総括

Xamarin.AndroidのビルドはAndroid(Java)よりも複雑で、また常にGoogleの気まぐれ(?)な変更に振り回される側面があって、ビルドの最適化は難しい課題のひとつです。それはAndroid本家にも言えることですが、長い暗黒時代を経てInstant Runのような最適化が実現したことは、Android SDKのひとつの進歩だと思います。Xamarin.Androidでも、今回そうしたように、こういった進歩は積極的に取り入れていきたいですね。

January 3, 2017
129 words


Categories

Tags

Author

Backlog