ProGuardによるクラッシュ・不具合を正しく回避する

  • 35
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

ProGuardを使うとアプリを小さくしたり解読を難しくしたりと、リリースする際には必須のメリットを享受することができます。しかし、ProGuardを有効にしたリリースビルドを起動した途端クラッシュするとかリクエストがコケるとか、そういった現象にぶち当たった人は少なくないのではないでしょうか。ProGuardの仕組みを把握してprogurad-rules.txtに適切なルールを書き足せばこれらのクラッシュは回避できます。

ProGuardはそもそも何をするのか

アプリケーションとライブラリのclassファイルを解析し、

  • 誰からも呼び出されていないクラスとクラスメンバ(メソッド、フィールド)や、各種メタデータを削除するなどしてバイナリサイズを小さくします(シュリンク
  • バイトコードレベルの最適化を実施し、その結果不要になったクラスメンバなどを削除します
  • リネームしても問題ない要素をリネームして、リバースエンジニアリング(解析)を難しくします(難読化

どうしてProGuardでクラッシュが発生するのか

上記処理で消えたり変更されたりしたクラス、クラスメンバ、メタデータにアクセスしようとすることでクラッシュしてしまいます。それを回避するために、ProGuardによる変更を明示的に無効化する-keepオプションが存在します。Androidの場合は、proguard-rules.txtなどbuild.gradleで指定したファイルに書けば反映されます。

いつ-keepすればよいのか

基本的に文字列によるクラス、メソッド、フィールドの操作を行う箇所に注意を払う必要があります。例えば下記の場合に注意が必要です。

  • リフレクションを用いるライブラリを使用する場合
    • フィールドをJSONに直接マッピングするGson、動的にAPIクライアントを作るRetrofitなど
    • リフレクションを趣旨としたライブラリでなくとも、内部でリフレクションを使用している場合もあります・・!
    • なお、RealmやDagger 2のようなコード生成を行うライブラリは、リフレクションをなるべく避けていることが多いように感じます
  • (フィールドを動的に参照する)Serializableを使用してディスクに保存したりする場合
  • リフレクションに限らずコード上のメタデータが必要な場合(デフォルトでは削られます)

指定の例

多くの例が下記のURLにあります。Serializableをディスクに保存しているケースがあったものの、後述のAndroid標準のルールにはkeep設定が含まれていなかったので自分で追加したりしました。
http://proguard.sourceforge.net/index.html#manual/examples.html

-keepの指定方法

検討

まず、何をkeepすべきなのかを検討します。

対象の軸:

  • パッケージ全体
    • あるライブラリの中でどんなリフレクション処理をしているかわからない場合など
  • クラス
    • 単純にクラス名のことも、アノテーション、親クラス、フィールドの存在などで指定することも
  • クラスメンバ(メソッド、フィールド)
    • SerializableのserialVersionUIDなど
    • Gsonのような自動マッピング
  • メタデータ(後述の-keepattributes)
    • アノテーション情報、型情報など

keepの仕方の軸:

  • 直接使用されていなくても削除しない、リネームもしない
    • 動的に呼び出されたり参照される場合など
  • 削除はするけどリネームはしない
    • クラス名を使って何か(自動生成コードなど)と対応をとっているが、使われてなければ不要なものなど

クラス・クラスメンバの指定

まずクラスを指定して、その中のクラスメンバを必要に応じて指定するという形になります。なお、クラスメンバ指定は省略可能ですが、省略すると何も指定していない(全て指定ではないので注意)という意味になるようです。

-keep class retrofit.** { *; }
-keepclasseswithmembers class * {
    @retrofit.http.* <methods>;
}

**は全てのサブパッケージに含まれるクラスになります。あとはかなり直感的です。指定方法は多岐にわたるのでドキュメントを参照してください。

翻訳: http://m12i.hatenablog.com/entry/20110416/1302942203
原文: http://proguard.sourceforge.net/index.html#/manual/index.html の Usage → Class Specifications

オプションの指定

実際に指定するオプションは下記の通りです。

保護対象 削除もリネームもしない 削除はするけどリネームはしない
クラスとクラスメンバ -keep -keepnames
クラスメンバ -keepclassmembers -keepclassmembernames
指定したクラスメンバが全て存在する場合に限り
クラスとクラスメンバ
-keepclasseswithmembers -keepclasseswithmembernames

※クラスメンバの指定が省略された場合、引数なしのコンストラクタ以外は削除・リネームしようとするので注意!

各オプションの使用例などはドキュメントを参照してください。

翻訳: http://m12i.hatenablog.com/entry/20110406/1302041050
原文: http://proguard.sourceforge.net/index.html#/manual/index.html の Usage → Keep Options

-keepattributesで指定できるメタデータの種類

自分が実際に使っているのは下記のあたりです。

  • アノテーション(*Annotation*
    • Retrofitのようにアノテーションを使って動的に(注: Dagger 1は動的だがDagger 2はコード生成により静的)何かをする場合に必要。ないとバグ。
  • throws句(Exceptions
    • java.lang.reflect.Proxyで動的にインタフェースを実装し、そのメソッドからチェック例外を投げる場合に必要。ないとクラッシュ。
  • ジェネリクスの型パラメータ(Signature
    • Gsonのように動的にジェネリクス情報にアクセスする場合に必要。ListのFoobarが何なのかを参照できる。ないとライブラリがエラーを吐くはず。
    • 詳しくはTypeTokenを参照のこと。
  • ソースファイル情報(SourceFileLineNumberTable
    • あればスタックトレースに含められる。Crashlyticsは両方keepすることを推奨している(がSourceFileからクラス名を簡単に復元できそうなので自分はそっちは切ってます)。

http://proguard.sourceforge.net/manual/attributes.html

いつ-dontwarnにすればよいのか

USE IT ONLY WHEN WHAT YOU ARE DOING!!!!!1!!!!11!!!!!!

ProGuardは参照されているのに存在していないメソッドなどが存在していると警告を出すようになっていて、ビルドエラーになり事前に気づくことができます。しかし、-dontwarnを指定すると、指定されたクラスorクラスが参照している何かが存在していなかったとしても一切警告を出しません

これを使うときは、あるライブラリが別のライブラリにoptionalな依存関係があり、存在していないクラスやメソッドを絶対使わないとわかっているときだけにします。そして、可能な限り範囲を狭めましょう。そうでない場合は、必ず問題があるのでちゃんと直しましょう・・!!

Androidでの事情

デフォルトの指定

proguard-android-optimize.txtを指定すると思いますが、ソースコードが公開されているので確認することができます。
下記はマシュマロ(6.0系)の一例ですが、ParcelableのCREATORやenumの-keep設定があったりして面白いです。
https://android.googlesource.com/platform/sdk/+/marshmallow-mr2-release/files/proguard-android-optimize.txt

ライブラリへのルールの同梱

consumerProguardRulesという仕組みによって、ライブラリ側に-keepなどのルールを追加することができるようになっています。
http://gfx.hatenablog.com/entry/2016/01/16/184846

GitHub上で確認してみてそのような説明or定義があれば、proguard-rules.txtに自分で追加する必要はありません。

Resource Shrinking

res以下の未使用画像がいっぱいあってアプリが太ってしまうケースはあると思います。
Android Gradle PluginはProGuardの解析結果をもとに使われていないリソースファイルを削除する機能があります。
http://developer.android.com/intl/ja/tools/help/proguard.html#shrink-resources