Xamarin.Androidのproguardサポートについて

Xamarin Advent Calendar3日目のエントリーです。今年は全部埋まっていて気楽ですね! まあ去年も2回書いただけですが。

 

さて、もうだいぶ前になりましたが、Xamarin Evolve 2014のライトニングトークで、Xamarin.Androidでちょろっと実装していたproguardサポートについて話しました。今日はそのネタについて書こうと思います。LTで話したことに、必要になりそうな前提知識をちまちまと追加しただけで、その後追加された目新しいニュースはありません。

 

はじめに残念なお知らせですが、このproguardサポートの機能、秘密ではないのですが次のリリースでは大々的に告知されません。何が起こったかというと…IDEに必要なコードが何も追加されていなかった上に、QAが何もやってないことが判明したんですね(!)。そんなわけで、10月には「5.0に入れるよ!」としゃべったのに、告知はされないという何とも残念な事態です。 まあ個人的には「使えりゃいい」ですが…

Proguardとは何なのか?

proguard。聞いたことがない人も多いと思いますが、Android開発ではよく使われるSDKツールです。proguardが何をするかというと、主に2つの用途があります。

  1. 難読化 (obfuscation)
  2. 不要コードの削除 (dead code elimination)

Androidで使われるJavaライブラリの中を覗くと、たまにaだのbだのといった名前のクラスが散見されますが、これらはproguardで難読化されたものと思えば良いです。難読化については、今回は話題にしないので(Xamarinではサポートしません)、主に不要コード削除の話をしたいと思います。

 

不要コード削除が何のために行われるかというと、主に、アプリケーションサイズを縮小する、ということが挙げられます。最近では、Google Play Servicesのjarだけでも3MB近くあります。これがそのままアプリケーションに含まれるとなると、けっこうな負担増です。自分のアプリでは、Google Play Servicesのほんの一部の機能しか使いたくないのに。地図を出すだけなのに、GamesのGoogle Plusリーダーボードの機能なんかいらんですよね。

 

不要なコードが多く含まれると、他にも問題が生じてきます。

  • Javaコードはapk作成時にdex形式に変換され、さらにtarget上でdexから実機コード(DalvikのodexやARTのelf)に変換されますが、それぞれのコストが大きくなります。
  • Dalvik VMにはメソッド数が65535を越えられないという制約があって、特にGoogle Play Servicesを使ったアプリケーションなんかだと、この問題が顕現するようです。

Xamarin環境だと、伝統的に、まずmonoランタイムとマネージドコードの大きさが問題になっていました。Mono.Android.dllなんかは20MB近くあるわけです。これをそのままアプリケーションに入れるわけにはいかないので、Xamarin環境ではMonoがSilverlight対応(Moonlight)のために開発していたassembly linkerの機能を使って、dllから使われていないコードを削除するようにしています。やっていることはproguardと同じですね。

 

(Mono.Android.dllはandroid.jarに対するバインディングとなるわけですが、android.jarはターゲット側に既に入っているため、Java/Android開発において、android.jarのコードを削る工程は無いわけです。)

 

リンクされたMono.Android.dllと組み込みmonoランタイムは、HelloWorldレベルのアプリケーションでも、合わせて3MB以上はあります。伝統的には、proguardのコード削除機能は小さい規模での最適化だったため、「大きすぎるライブラリの問題」が最近になって顕現してくるまでは、相手にしてこなかったわけです。

XamarinでのProguardの使い方

さて、proguardの位置付けと必要性がわかってきたところで、Xamarin環境でproguardを「使う」方法について説明しましょう。Xamarin.Android 4.99以降が必要です。

 

有効にするためには、Xamarin.Androidのプロジェクト ファイル(*.csprojあるいは*.fsproj)を手作業で編集する必要があります*1。Releaseビルド用のPropertyGroup要素(Condition属性でConfiguration=='Release'として設定されているやつ)の中に、以下の要素を追加します。

 

<EnableProguard>True</EnableProguard>

 

これだけで、proguardはひとまず有効になります。

 

proguardはDebugビルドでも有効にできなくはないですが、assembly linkerがDebugビルドで無効になっているのと同様の理由で、fast deploymentと相容れる機能ではないので(無駄なファイル変更があるたびにapkを更新していたらfast deploymentの意味がない)、Releaseビルド専用と思っておけば間違いないです。

 

proguardはひとまず上記のプロパティで有効になりますが、実はこれではほとんど最適化は行われません。proguardを実効的に使うためには、もうひとつ重要な作業があります。それは

  • LinkerのモードをLink All Assembliesに設定する

ということです。これは少しややこしい理由によりますが、これは以降で詳しく説明します。

proguardの動作原理について

Link All Assembliesが必要になる理由を理解するためには、proguardがどのように「不要コード」を削除しているか、大まかに理解する必要があります。(理屈を理解しなくても、まあproguardを有効にすることはできますが、もし「調整」が必要になった場合は、proguardの挙動を理解しておかないと難しいでしょう。)

 

proguardは、「入り口」となるメソッドから、コードを探索して、中で使用されているメンバーをマークしていきます。マーキングが終わったら、全てのクラスの中から、マークされなかったメンバーを削除していきます。GCのマーク&スイープと似ていますね。

 

ちなみにこれは、マネージドコードのLinkerも概ね同様です。

 

そういうわけで、原理としては、各種「Androidフレームワークから呼びだされそうな」メソッドのエントリポイント(Acvitity.onCreate()など)から呼び出されるコードを全てマークすればいい、ということになるわけですが、実際にアプリケーションを作成していると、どうもそれだけでは済まない、自前でいろいろproguardに「これもマークしろ」といった指示を与えてやる必要が出てきます。proguardは、それを可能にするために、コマンドライン オプションをいろいろ用意しています。

 

実のところ、proguard自体は、Android専用ツールではないので(GoogleAndroid開発環境を構成するために、さまざまなオープンソース ツールを活用しているわけで、これもその一例です)、proguardはAndroid固有の事情を考慮しません(「固有の事情」というのは、android.jarを削る必要はなく、また呼び出されるアプリケーション コードのエントリポイントはstatic void main(string[])ではなく、さまざまなフレームワークAPIのオーバーライドである、といったところです)。そのため、Android SDK自身が、proguardにいくつかのデフォルト設定を与えています。android-sdkのtools/proguard/ディレクトリには、proguard-android*.txtというファイルがありますが、これらがその内容です。

 

Android SDKを使った開発では、典型的にはproguard.cfgというファイルを編集して、proguardに対するオプションを記述します。これはXamarin.Androidでも同じことができます。ProguardConfigurationというビルド アクションを指定されたファイルが存在すれば、Xamarin.Androidはデフォルトでその設定内容をproguardの呼び出しに反映させます。

 

(ちなみに、proguardとは関係ありませんが、マネージドコードのLinkerについても、LinkDescription.xmlというファイルで、linkerに対する指示をいろいろ加えることができます。)

 

Xamarin.Androidにおける設定ファイルの自動生成

さて、ここからが肝心の話ですが、Xamarin.AndroidJava呼び出しAPIは、全てJNIを経由して行われます。JNIを経由した呼び出しというのは、動的なJava APIの呼び出しであり、.NETで言えばリフレクションを使っているようなものです。動的な呼び出しは、proguardやlinkerのようなツールでは追跡できません。追跡できないけど、使われてはいるコードなので、削除されると実行時にNoSuchMethodExceptionになってしまいます。

 

というわけで、マネージドコードから呼びだされている機能については、明示的にマークして救済してやらないといけないわけです。これはユーザー任せにしていてはとても実現できないでしょう(「お前は今まで使ったAPIを全て覚えているか?」)。

 

幸いなことに、マネージドコードからのJava APIの呼び出しは、RegisterAttributeによって全てマークされています。というわけで、Xamarin.Androidでは、「アプリケーションのコードに含まれる全てのメソッドについて、RegisterAttributeをチェックして、もしあれば(そしてDoNotGenerateAcw="true"であれば)、それに対応するprogardオプションを動的に生成する」という荒業で、これに対応しています。

 

(もともとLinkerが同じようなことをやっているのと、最終的にproguardがかかってdex化するjavaバイトコードが減ることもあって、ビルドが遅くなる影響はあまりないようです。改善にもなりませんでしたが…)

 

そして、「アプリケーションのコード」と書きましたが、実のところこれは「RegisterAttributeの付いた全てのコード」に他なりません。全ての呼び出しは動的であり、特にフレームワーク側からACW(Android Callable Wrapper)経由で呼び出されるメソッドの呼び出しも含めると、何が実際に使われるコードであるかを判断するのは、動的な呼び出しをベースとしているXamarin環境では困難で、わたしには現状でこれ以上何が出来るかは、ちょっと思いつきません。まずは「概ね動作する機能を有効にする」かたちでのリリースを優先させたというわけです。

 

というわけで、ようやく本題に戻りますが、Link All Assembliesを指定するのは、「アプリケーションのコード」を減らすためです。Link SDK Assemblies Onlyを選択すると、Google Play ServicesやSupport Library(NuGetから入手できる最新のほう)などはlinkerがコードを削除しないため、全てのバインディングが残ったままになり、結果的にjarに含まれるほぼ全てのjavaコードが残ってしまうことになります。これではproguardの呼び出し損です。ビルド時間が長くなるだけです。

自動生成の設定ファイルを「外す」 - 高度なproguard設定

さて、以上のように、Xamarinにおけるproguardのサポートは、割と複雑な処理をはらんでいます。特に自動生成ファイルについては、本当にそれが常に存在すべきオプションだけを含むものなのか、わたしには確信がありません。(まあ主にkeepオプションを追加するだけなので、どちらかというと「安全」ではありますし、自前で作成するのはあまり現実味がない気もしますが…)

 

というわけで、場合によっては、この中間生成ファイルがむしろ効果的なproguardの邪魔になるかもしれない、だから場合によっては排除できるべきだ、とわたしは考えています。そこで、ProguardConfigFilesというMSBuildプロパティを用意して、もし必要であればカスタマイズできるようにしました。デフォルトの内容は次のようになっています。

       <ProguardConfigFiles Condition="'$(ProguardConfigFiles)' == ''">
               {sdk.dir}tools\proguard\proguard-android.txt;
               {intermediate.common.xamarin};
               {intermediate.references};
               {intermediate.application};
               @(ProguardConfiguration);
       </ProguardConfigFiles>

この{...}の部分には、自動生成ファイルなどが含まれることになります。たとえば、{intermediate.common.xamarin}に対応するのは、以下のような内容のファイルです:

# This is Xamarin-specific (and enhanced) configuration.
-dontobfuscate
-keep public class mono.MonoRuntimeProvider
-keep public class mono.MonoPackageManager
-keep public class mono.MonoPackageManager_Resources
-keep class mono.android.**
-keep public class mono.android.app.Application
-keep public class android.runtime.*

# Android's template misses fluent setters...
-keepclassmembers class * extends android.view.View {
  *** set*(***);
}

# also misses those inflated custom layout stuff from xml...
-keepclassmembers class * extends android.view.View {
  <init>(android.content.Context,android.util.AttributeSet);
  <init>(android.content.Context,android.util.AttributeSet,int);
}

これで、Xamarin環境で必要なクラスが残されることになります。また難読化も無効にされます(JNI呼び出しは動的に行われるので、難読化されたらまともに呼べなくなります)。

 

{intermediate.references}には、上記の、アプリケーションが参照するdllに含まれるRegisterAttributeを探索した処理の結果が含まれています。

 

{intermediate.application}には、アプリケーションのコードから自動生成されたACWについて、全てkeepとする設定が含まれています。(アプリケーションのdllには通常はRegisterAttributeが付いていないので、linkerの処理では対応できないわけです。)

 

これらは全て、ProguardConfigFilesというプロパティで既定の設定となっているがゆえに、読み込まれてproguardに渡されるようになっています。ですので、もしこれらの中に排除したいものがある場合は、csprojあるいはfsprojに、MSBuildプロパティを追加指定してやると、オーバーライドすることができます。

 

もちろん、単にオプションを「追加」したいだけであれば、ProguardConfigurationのビルドアクションをもつproguard.cfgなどをプロジェクトに追加して、そこに記述してやれば、ProjectConfigFilesに@(ProguardConfiguration)が含まれている通りで、そのままproguardに渡されます。

 

まとめ

Xamarin.Androidにおけるproguardサポートは、.csprojあるいは.fsprojファイルをいじって、MSBuildプロパティEnableProguardをtrueとして追加し、Link All Assembliesを指定することで、簡単に「使う」ことができます(リリースビルドだけにするのが望ましいところです)。

 

必要であればProguardConfigurationのビルド アクションをもつファイルにproguardオプションを追加すると良いでしょう。

 

ただし、中では、Xamarinのような環境でproguardを有効にするために、いくつかのオプションが自動生成されるなど、それなりに高度なことを行っています。もしそれらの自動生成ファイルが邪魔になったら、ProguardConfigFilesプロパティの内容を上書きして、好きなように指定すると良いでしょう。

 

場合によっては、自前でJavaクラスをkeepしておかないとけないこともあるはずなので、proguardがどのような原理で動作するものなのかは、proguardのドキュメンテーションなどを見て調べると良いでしょう。

*1:リリースチームに、IDEでこれを有効にするオプションを追加するよう頼まれて、コードを書いたのですが、頼んできた直後にリリースブランチを締め切られて、せっかく1時間で書いたのがまだ含まれていないという…