NO_NAME

適当に技術・仕事・ライフハックの話を書いていく予定です。

Proguardでコードの圧縮・最適化・難読化を行う

Android SDK には Proguard が組み込まれており、コードを圧縮・最適化・難読化できる。
特にライセンス認証を行っていたり、コア技術が入っていたりする場合、難読化は必須。

Androidプロジェクトに Proguard を適用する際にわかったことを記述する。

Androidプロジェクトで Proguard を利用する方法

<project_root>/project.properties ファイルに以下のように proguard.configプロパティで proguard の設定ファイルを指定するとリリースビルド作成時(Eclipse、Antどちらにも適用される)に自動で Proguard が適用される。

proguard.config=proguard.cfg

Progurad への入力

Proguard へは通常以下のファイルを入力として与える必要がある。
ただし Android SDK でのビルドで自動的に Proguard が適用される場合、自動で設定される。

  1. 加工するクラスファイル(Jar, wars, ears, zips, ディレクトリなどで指定)
    • プロジェクトのクラスクラスファイル
    • プロジェクトが依存しているライブラリのクラスファイル
      通常のプロジェクトではライブラリは 2 のほうに分類されるが、Android プロジェクトではビルド時にライブラリは分解されプロジェクトのクラスファイルと混ざるため。
  2. 加工するクラスファイルが利用しているライブラリ(ライブラリ自体は変更されない)
    • target に指定されている Android Framework

エントリーポイントの指定

クラス・メソッドの呼び出し階層を自動でたどれないような箇所はエントリーポイントに指定する必要がある。
エントリーポイントは Proguard で変更されない。
エントリーポイントとして指定されるのは主に外部との接合部分になる。
たとえば次のような箇所をエントリーポイントとして指定する。

  • リフレクション・イントロスペクションを直接的・間接的(ライブラリやフレームワークを通して)に利用しているケース
  • ダイナミックProxyを使っているクラス
  • シリアライズ・デシリアライズされ永続化されるSerializableクラス

リフレクション・イントロスペクションの自動検出・対応

ダイナミックにオブジェクトの生成やメソッド呼び出しが実行されるので基本的には Proguard で自動対応出来ず、エントリーポイントで指定する必要がある。
しかし以下のようなケースは自動で検出・対応される。

  • Class.forName("SomeClass")
  • SomeClass.class
  • SomeClass.class.getField("someField")
  • SomeClass.class.getDeclaredField("someField")
  • SomeClass.class.getMethod("someMethod", new Class[] {})
  • SomeClass.class.getMethod("someMethod", new Class[] { A.class })
  • SomeClass.class.getMethod("someMethod", new Class[] { A.class, B.class })
  • SomeClass.class.getDeclaredMethod("someMethod", new Class[] {})
  • SomeClass.class.getDeclaredMethod("someMethod", new Class[] { A.class })
  • SomeClass.class.getDeclaredMethod("someMethod", new Class[] { A.class, B.class })
  • AtomicIntegerFieldUpdater.newUpdater(SomeClass.class, "someField")
  • AtomicLongFieldUpdater.newUpdater(SomeClass.class, "someField")
  • AtomicReferenceFieldUpdater.newUpdater(SomeClass.class, SomeType.class, "someField")

さらに (SomeClass)Class.forName(variable).newInstance() のようなケースでは SomeClass とそのサブクラスの圧縮・最適化・難読化 を防止したほうがよいことを検出・提案してくれる。

設定例

Androidプロジェクトでの Proguard の設定例を記載する。
わかりやすいように日本語でコメントを書いてあるが、日本語でコメントを書くとうまく動かなくなることがあるので避けた方が良い。
プロジェクトのパッケージ名は com.example ということにしておく。
ProGuard Examples も参考に。

# =======================
# Android SDKにより自動生成される部分
# =======================
-optimizationpasses 5
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses 
-dontpreverify # Dexコンパイラでは事前検証は意味を持たないので不要
-verbose
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/* # Dalvik VMが対応していない最適化を行わない

# AndroidManifest.xml に記載されるような、コンポーネントはクラス名を変更しない
-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.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class com.android.vending.licensing.ILicensingService

# nativeメソッドの名前はネイティブコードへ合わせる必要があるので変更しない
-keepclasseswithmembernames class * {
    native <methods>;
}

# layoutファイルに記載されるようなクラス名を変更しない。
-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet);
}

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

# menuファイルに記載されるようなメソッドがある場合、クラス名、メソッド名を変更しない。
-keepclassmembers class * extends android.app.Activity {
   public void *(android.view.View);
}

-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# =======================
# 独自に付け足した部分
# =======================

# ライブラリは詳しく把握していないため、
# Proguard の影響が想像つかないため避けておく。
# ライブラリ部分をきちんと把握しており、
# また Proguard の効用を限界まで高めたい場合はこれは必要ない。
-keep class !com.example.** {
    *;
}

# JNI で利用されているクラスをエントリーポイントとして指定する。
# nativeメソッドだけがネイティブコードから利用されている場合は、
# 上記にあるnativeメソッドをエントリーポイントとして指定するだけで良いが、
# クラス内のフィールドをネイティブコードから利用している場合、
# そのフィールドもエントリーポイントに指定する必要がある。
-keep class com.example.NativeClass {
    *;
}

# アノテーションを必要とするライブラリがある場合には
# アノテーション属性を削除しないように指定する。
# これを指定しないとクラスファイルについた属性は全て削除される。
# エントリーポイントとして指定した部分も例外ではない。
# Jacksonライブラリなど、この指定がないと実行時にエラーが発生する。
-keepattributes *Annotation*

# 以下の指定をすると Proguard の最中、次のクラスが見つからなくてもエラーを出さない。
# ライブラリ中の利用していない箇所から参照されているクラスでエラーが出た場合、指定すると良い。
# ちなみにこれはJacksonライブラリへの対応。
-dontwarn org.w3c.dom.bootstrap.**
-dontwarn org.joda.**

注意点

日本語コメントを書くとうまく動かなくなることがあるので日本語は使わない方が良い。
最悪のケースではビルドは通るが、ビルドされたアプリの実行中にエラーがでる。

おまけ:Proguard の実行ステップ

Proguardの実行ステップは以下の4つからなる。

  • 圧縮ステップ
    エントリーポイントして指定された場所からスタートして利用しているクラスやクラスメンバーを確定し、利用されていないクラス、クラスのメンバーを削除する。
  • 最適化ステップ
    エントリーポイントでないクラスやメソッドを private, static, final にしたり、利用されていないパラメータを削除したり、インライン化する。
  • 難読化ステップ
    エントリーポイントでないクラスやメソッドをリネーム化する。リネーム結果にあわせてオプションとして加工する Jar に含まれているリソースのファイル名やコンテンツを変更することも可能。
  • 事前検証ステップ
    事前検証情報をクラスにつける。 JavaMEでは必須、Java6だと起動時間の短縮になる。