【Android】Androidで使用するProGuard勘案

世の中にはProGuardって言う、成果物の不要なクラス・メソッドを自動で削除してくれたり、難読化してくれたりするツールがあります。Support Libraryを使うときはこれも使ったほうがいいとオススメされていたり、ADTでそれなりにサポートされているので使ったことがある人も多いでしょう。

が、これがですね、何かもう考えることがいっぱいありすぎてわけわかんない状態になってしまってですね、世界でこんなにFxxkって言葉を使うのは私かAVGNかぐらいの気持ちになってきたので、ちょっと一旦整理してみたいなと思います。

そもそもProGuardは何をするためのものなのか

案外この辺の解説をしているところが少ないです。ちゃんと公式サイトのIntroductionを読んでみましょう。2011年段階のものであれば翻訳してくださってる方がいるのでそちらを読んでもいいです。

図を見ればわかる通り、Input jarsLibrary jarsと呼ばれるものがあり、このうちのInput jarsの方が以下のProGuardの処理対象となるようです。

  • shrinking step(圧縮)
  • optimization step(最適化)
  • obfuscation step(難読化)
  • preverification step(事前検証)

Androidアプリを対象に考えると、対象のプロジェクトのソースとlibs配下にあるものがInput jars、Android SDKJDKのようなものがLibrary jarsとなるでしょう。

ただ、Androidアプリに関してはその辺のことを自動でやってくれるらしく、自分でproguard-project.txtに記述すると多重定義となってエラーになるとか。(You should never explicitly specify the input jars yourself (with -injars or -libraryjars), since you’ll then get duplicate definitions.)

ここで覚えておくべきことは、圧縮と難読化のステップ(及びオプション)は別物と言うことです。そもそも圧縮(削除)する対象なら難読化しなくていいわけですからね。

どのオプションが圧縮から対象外にするもので、どのオプションが難読化から対象外にするものなのかは常に頭の中に入れておく必要があります。めんどくせー。

エントリポイントの指定

厳密に言うと、どの要素を圧縮・難読化するかはInput jarsのエントリポイントから再帰的にどのクラス・メソッド・フィールドを使用しているかを調べることで割り出します。エントリポイントに指定されたクラスは保護対象となり、圧縮・難読化が行われません。

エントリポイントを指定するために使うオプションがKeepオプションです。(翻訳版はこっち

ここで注意すべき点は、-keepでクラスのみを指定すると、クラスメンバ(フィールドやメソッド)は保護対象にならないと言う点です。

また、Keepオプションにも色々な種類があります。初めのうちはこの表を常に見ながらどれを指定すべきか考えましょう。(翻訳版

圧縮・難読化してはいけないものは何かを洗い出す

proguard-project.txtには基本的に「この要素は圧縮・難読化しないで欲しい」と言うものを記述します。つまり、Keepオプションをガンガン記述していくことになります。

まず、どれを圧縮・難読化されると困るのか。その方針を決めましょう。とりあえず以下の要素が含まれているものは何らかの形で保護したいと思います。

外部ライブラリはなるべく全部圧縮・難読化から逃がします。理由は上記内容のどれかをやっていてもわからないからです。そう言う意味では、何でもかんでもInput jarsにする今の仕組みは本当にファックとしか言いようがないです。

リフレクションに関してはある程度自動で圧縮・難読化を避けてくれるようですが、いちいちその例外を探るのが面倒なので全部残すようにします。

アノテーションはそもそもなくても動くのが筋と言えば筋ですが、普通に実行時情報を残してリフレクションしたいことも多々あるので残します。

Android特有のフィールド・メソッド呼び出し」とは、レイアウトファイル絡みのアレです。一番わかりやすい例だとレイアウトファイルに「android:onClick」を記述したときのやつです。対象となるActivityにそれ用のメソッドを記述しなくてはいけないんですが、まぁ大概の場合、消し飛ばされると考えて良いでしょう。

最後に、Serializableを実装するクラスも逃がします。難読化によってシリアライズする内容が変わってしまうと面倒だからです。

他にもJNIを使用している場合は色々やらなきゃいけないっぽいですが、私は使ってないので特に何もしません。

[2014/07/08追記]

ProGuardのコードを読むときの基礎知識

大体以下の要素を覚えておけばニュアンスで読めます。

*が連続していっぱい出てくることがあるんですが、Class Specificationsでしか出てきません。

後はオプションが何のオプションなのかわかれば、何とかなります。読むだけなら。

[2014/07/08追記ここまで]

外部ライブラリを処理対象外とする

正直jarレベルで外すにはそもそもinput jarsの対象から外すのが一番早いんですが、Googleが要らんことするせいで他の方法を考えるしかないです。

例としてTwitter4Jで考えてみましょう。

結局のところ-keepを使うことになるんですが、当然全クラス/全メンバをいちいち指定していたら日が暮れるので、上手いことフィルタリングしてやります。(翻訳版

-keep class twitter4j.** { *; }

上記の内容は「twitter4jと言うパッケージ配下のあらゆるクラスのあらゆるメンバをKeepする」と言う意味です。

他の外部ライブラリも同じ要領でぶちこんでいきましょう。

あと、色々な解説で-keepとあわせて-keepnamesを指定している例がありますが、基本的には-keepで指定しておけば圧縮も難読化もされません。翻訳版

アノテーションをProGuardの処理対象外にする

これは簡単にできます。以下の一文をいれておくだけです。

-keepattributes *Annotation*

ただし、この-keepattributesは難読化のオプションです。圧縮の対象にはなります。アノテーションを定義しているクラス(厳密にはインターフェースか?)は別途-keepで保持しておいた方がいいかもしれません。例がないとアレなので、このSqliteTableSqliteFieldと言うアノテーションをkeepしてみます。

-keepattributes *Annotation*

-keep class inujini_.sqlite.meta.annotation.** { *; }

# @inujini_.sqlite.meta.annotation.**でもいいのかも
-keep @inujini_.sqlite.meta.annotation.SqliteTable class * { *; }
-keep @inujini_.sqlite.meta.annotation.SqliteField class * { *; }

「-keep classでいいの?」と思うかもしれませんが、classはインターフェースも対象としてくれます。明示的にinterfaceを指定するとinterfaceだけkeepしてくれるみたいです。(The class keyword refers to any interface or class. The interface keyword restricts matches to interface classes.)

ついでに「あるアノテーションがついているものはkeepする」と言う指定もできます。

# クラスに紐付くアノテーションはimplementsで表現
-keep class * implements @inujini_.sqlite.meta.annotation.SqliteTable { *; }

# メンバに紐付くアノテーションはこんな感じ
# keepclassmembersは「クラスが保護対象であるとき、同様に保護すべきクラス・メンバー」
# 要はクラスが保護対象じゃなかったら保護されない
# -keepclassmembers class *とすれば「保護対象となるクラスすべてで」みたいな意味合いになる
-keepclassmembers class * {
    @inujini_.sqlite.meta.annotation.SqliteField *;
}

Android特有のフィールド・メソッド呼び出しを処理対象外にする

これはProGuardの公式に一例が載っています。参考にしてみましょう。

-injars      bin/classes
-injars      libs
-outjars     bin/classes-processed.jar
-libraryjars /usr/local/java/android-sdk/platforms/android-9/android.jar

-dontpreverify
-repackageclasses ''
-allowaccessmodification
-optimizations !code/simplification/arithmetic
-keepattributes *Annotation*

-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider

-keep public class * extends android.view.View {
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
    public void set*(...);
}

-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet);
}

-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet, int);
}

-keepclassmembers class * extends android.content.Context {
   public void *(android.view.View);
   public void *(android.view.MenuItem);
}

-keepclassmembers class * implements android.os.Parcelable {
    static ** CREATOR;
}

-keepclassmembers class **.R$* {
    public static <fields>;
}

-keepclassmembers class * {
    @android.webkit.JavascriptInterface <methods>;
}

keepとつくところをざっと眺めただけでも「その辺消されるとヤバイね」ってところに何となく合点がいきます。

最初の数行(-injarsとか-outjarsとか)は自動で組み込まれるかもしれないですね。いまいちわかんないです。

@hideなAPIをリフレクションで使ってる場合なんかは別途指定しておいたほうがいいと思います。

また、AIDLを使用している場合はこの辺も入れておくと吉です。

-keep class * extends android.os.IInterface
-keep class * extends android.os.Binder

AIDLファイルを置いておくとgenフォルダ配下にjavaが生成されます。この自動生成されたファイルを見ればわかる通り、インターフェースはandroid.os.IInterfaceを、その配下のStubはandroid.os.Binderを継承します。それを逃がそうってわけです。

[2014/07/08追記]

keep以外のオプションについても軽く触れておきましょう。-dontpreverifyは事前検証に関するオプションです。翻訳版

「Only when eventually targeting Android, it is not necessary, so you can then switch it off to reduce the processing time a bit. 」とあるので、まぁ、入れておきましょう。

-repackageclassesは難読化に関するオプションですね。(翻訳版

これは名前を変更されたクラスファイルを引数で指定された一つのパッケージにまとめるオプションです。引数に何も渡さない or 空文字を渡すと名前を変更されたクラスファイルのパッケージ情報を削除します。これ入れといて大丈夫なのかな…?

当然同じパッケージ内にリソースファイルがあったりすると読み込めなくなる可能性があるので注意しましょう、とのこと。

次は-allowaccessmodificationです。これは最適化オプションの一種になります。(翻訳版

特に今まで説明しませんでしたが、ProGuardはコードの最適化もある程度行ってくれます。イントロダクションによると、エントリポイントではないメソッドのprivate、static final化、使用していない引数の削除、インライン化などです。Androidではメソッド呼び出しですらコストがたけぇと無茶苦茶なことを言っているので、この手の最適化はなるべく自動でやってほしいところですね。

で、-allowaccessmodificationなんですが、クラスとそのメンバのアクセス修飾子を自動で広げてもいいよ、と言う許可を出すオプションです。どうせProGuardをかけた後のソースなんて触らないし、「This can improve the results of the optimization step.」とのことなので入れておきましょう。

-optimizationsは読んで字の如く最適化の方法を指定するオプションです。指定例としてはこんな感じです。

どんなオプションがあるかの一覧が全くないのもアレだし、文法がわかりにくいのもアレですが、「!code/simplification/arithmetic」だと「算術の単純化を行わない」ぐらいの意味合いでしょうか…?

[2014/07/08追記ここまで]

Serializableを実装するクラスを処理対象外にする

これも公式で一例があります。

-keepnames class * implements java.io.Serializable

-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    !static !transient <fields>;
    !private <fields>;
    !private <methods>;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}

これをまるまるコピペしておけば大丈夫そうな気がします。

-keepattributesの設定をする

アノテーションのときにちらっと説明しましたが、-keepattributesはかなり大事です。アノテーションだけでなく色々なものを難読化から逃がすことができます。(keepしていなければ圧縮されるので注意)

指定できる要素は以下の通りです。(何か他にもあるっぽいんだけど一覧が見つからない)

  • Exceptions
  • Signature
  • Deprecated
  • SourceFile
  • SourceDir
  • LineNumberTable
  • LocalVariableTable
  • LocalVariableTypeTable
  • Synthetic
  • EnclosingMethod
  • RuntimeVisibleAnnotations
  • RuntimeInvisibleAnnotations
  • RuntimeVisibleParameterAnnotations
  • RuntimeInvisibleParameterAnnotations
  • AnnotationDefault
  • InnerClass

逆に-keepattributesを指定しないとこの辺全部難読化されるんですかね…。

先ほど紹介した「-keepattributes *Annotation*」は上記のAnnotationって文字列が入っている要素は全部難読化しない、ってことです。

他にもまともなスタックトレースが取れない可能性があるのでもうProGuard使うのやめるか…この辺のものを指定しておくとよさそうです。

-keepattributes *Annotation*,Exceptions,Signature,SourceFile,LineNumberTable,InnerClass

[2014/07/08追記]

dontwarnで警告を握りつぶす

実際にAndroidプロジェクトに対してProGuardをかけると、いっぱい警告が出てきてエラーになることがあります。

きっとほとんどは参照が見つからない系のエラーでしょう。で、この公式のトラブルシューティングにも書いてありますが、JDK 1.6にはデフォルトで入っていてもAndroidで対応していなければ当然参照を解決できません。(公式ではjava.awtを例に出しているけど、大多数のjavax.*とかは多分ダメだと思う。)

主に外部ライブラリでぶつかるケースが多いと思います。私はAPTのAbstractProcessorでエラーが出ました。

確実に使わないとわかっているものに関しては-dontwarnで警告を握りつぶしてしまっても問題ないです。必ず出力されたエラーメッセージ、どのパッケージで発生しているのか、原因となっているクラスは何かを確認してから-dontwarnを使いましょう。当たり前ですけど、どんなエラーでも握りつぶせるので、必要なものが見つかってないのに握りつぶすと実行時エラーとなる恐れがあります。

先ほどのAPTのAbstractProcessorを継承したクラスであるPropertyFactoryを-dontwarnで指定する場合はこんな感じになります。

-dontwarn inujini_.sqlite.meta.factory.PropertyFactory

また、これ(参照解決できない)以外のエラーが出ても一度プロジェクトのクリーンを行ったら普通に動いたケースもありました。とってもEclipseって感じですね。

[2014/07/08追記ここまで]

一旦のまとめ

後は適宜残しておきたいものをちまちま-keepしていくことになるでしょう。それがリフレクションへの対応です。だ、だっせー。

正直言ってめちゃめちゃコストが高いです。何かもっと上手い仕組みは思いつかなかったんでしょうか。そう、Javaを捨てるとかさ!

参考