なんとなく動いている
Proguardから脱出するために
DroidKaigi 2018
佐藤 隼(Sato Shun)
最初に
Proguardってつらい
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn javax.annotation.**
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
覚えなければいけないことは多い
ただ頻出パターンがあり、そのパターンのいくつかを理解すれば意外となんとかなる
本セッションのゴール
Proguardの基本的な動きと
ユースケースを見ながら
Proguardの理解を深めること
とはいえ本当にProguardは
必要なのか?
Proguardのメリット
コードサイズの縮小/メソッド数の削減
-> 具体的にどれくらい削除できるか?
実例: DroidKaigi 2018
Before(2 dex files)
After(1 dex file)
実例: アメブロアプリ(担当アプリ)
Before(3 dex files)
After(2 dex file)
Proguardの仕組み/機能
shrink(削除)とobfuscate(難読化)
が重要(ハマりどころ)
削除と難読化
エントリポイント?
どうやってエントリポイント、
また難読化の有無を指定するか?
削除、難読化を防ぐKeepルール
| 削除/難読化をしない | 難読化をしない |
クラス、メンバー | -keep | -keepnames |
メンバー | -keepclassmembers | -keepclassmembernames |
メンバーが存在する時 クラス、メンバー | -keepclasseswithmemembers | -keepclasseswithmembernames |
Androidにおける具体的な
ユースケース
その1: OkHttp
公式のOkHttp Proguard
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn javax.annotation.**
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
OkHttpのProguardルール
-dontwarn
-keepnames
2つ種類のルールを指定している
これらのProguardを指定しない時
Error:Note: there were 11 duplicate class definitions.
(http://proguard.sourceforge.net/manual/troubleshooting.html#duplicateclass)
Warning: okhttp3.Address: can't find referenced class javax.annotation.Nullable
Warning: okhttp3.Address: can't find referenced class javax.annotation.Nullable
Warning: okhttp3.Address: can't find referenced class javax.annotation.Nullable
Warning: okhttp3.Address: can't find referenced class javax.annotation.Nullable
Warning: okhttp3.Address: can't find referenced class javax.annotation.Nullable
Warning: okhttp3.Address: can't find referenced class javax.annotation.Nullable
Warning: okhttp3.Address: can't find referenced class javax.annotation.Nullable
Warning: okhttp3.Address: can't find referenced class javax.annotation.Nullable
Warning: okhttp3.Address: can't find referenced class javax.annotation.Nullable
Warning: okhttp3.Address: can't find referenced class javax.annotation.Nullable
Warning: okhttp3.Address: can't find referenced class javax.annotation.Nullable
Warning: okhttp3.Address: can't find referenced class javax.annotation.Nullable
Warning: okhttp3.Address: can't find referenced class javax.annotation.Nullable
Warning: okhttp3.Authenticator: can't find referenced class javax.annotation.Nullable
...
Warning地獄
100行以上のWarningが出る
しかしWarningを見ると�can’t find referenced class クラス名�しかない
can’t find referenced class?
can’t find referenced class Nullable?
javax.annotation.Nullableが参照出来ないためWarningが出ている
なぜNullableが参照できないのか?
なぜprovided指定なのか?
結論
dontwarnルール
Warningを無視するルール
-dontwarn javax.annotation.Nullable
↓
-dontwarn javax.annotation.**
javax.annotationパッケージ以下に関するWarningを全て無視する
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn javax.annotation.**
を指定しWarningが消え、無事にコンパイルが通った
まだルールを指定している
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
コンパイルは通ったのにドキュメントには上記が必要と書いてある
keepnamesルール
指定したクラス、メンバーを難読化しない
okhttp3.internal.publicsuffix.PublicSuffixDatabase
↓
a.a.a.p
のように難読化されるのを防ぐ
なぜこのクラスを難読化しては駄目か?
PublicSuffixDatabase.java
PublicSuffixDatabase::class.java.getResourceAsStream("publicsuffixes.gz")
PublicSuffixDatabaseクラスの相対パスからgzファイルを読み込んでいる
(このgzファイルはOkHttp jarに含まれる)
PublicSuffixDatabaseが難読化されると
クラスパスを変更しては駄目
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
難読化を防ぎ、クラスパスを保つ
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn javax.annotation.**
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
その2: moshi
moshiとは?
公式のmoshi Proguard
-dontwarn okio.**
-dontwarn javax.annotation.**
-keepclasseswithmembers class * {
@com.squareup.moshi.* <methods>;
}
-keep @com.squareup.moshi.JsonQualifier interface *
moshiの公式Proguardルール
-dontwarn
-keepclasseswithmembers
-keep
3つの種類のルールを使用している
moshi: サンプルコード
val moshi = Moshi.Builder()
.add(ColorAdapter())
.build()
val r = moshi
.adapter(Rectangle::class.java)
.fromJson("{\"width\":1,\"color\":\"#ff0000\"}")
@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class HexColor
class Rectangle(
@field:Json(name = "width") val w: Int,
@field:HexColor @field:Json(name = "color") val c: Int
)
class ColorAdapter {
@ToJson fun toJson(@HexColor rgb: Int): String {
return String.format("#%06x", rgb)
}
@FromJson @HexColor fun fromJson(rgb: String): Int {
return Integer.parseInt(rgb.substring(1), 16)
}
}
Proguard適用前
@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class HexColor
class Rectangle(
@field:Json(name = "width") val w: Int,
@field:HexColor @field:Json(name = "color") val c: Int
)
class ColorAdapter {
@ToJson fun toJson(@HexColor rgb: Int): String {
return String.format("#%06x", rgb)
}
@FromJson @HexColor fun fromJson(rgb: String): Int {
return Integer.parseInt(rgb.substring(1), 16)
}
}
Proguard適用後
2つの問題が発生
1. ColorAdapterのメソッドが削除される
2. @HexColorが削除される
これらはRuntime時に必要なので、削除してはいけない
ColorAdapterのメソッドが削除される
-keepclasseswithmembers class ColorAdapter {
@com.squareup.moshi.ToJson <methods>;
@com.squareup.moshi.FromJson <methods>;
}
-keepclasseswithmembers class ColorAdapter {
@com.squareup.moshi.ToJson <methods>;
@com.squareup.moshi.FromJson <methods>;
}
↓
-keepclasseswithmembers class * {
@com.squareup.moshi.* <methods>;
}
@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class HexColor
class Rectangle(
@field:Json(name = "width") val w: Int,
@field:HexColor @field:Json(name = "color") val c: Int
)
class ColorAdapter {
@ToJson fun toJson(@HexColor rgb: Int): String {
return String.format("#%06x", rgb)
}
@FromJson @HexColor fun fromJson(rgb: String): Int {
return Integer.parseInt(rgb.substring(1), 16)
}
}
@HexColor削除問題
@HexColorはメソッド、フィールドについているだけで、Javaコード上から参照されていない
-> 結果、Proguardが削除する
-> なぜ削除されているのか分かるか?
APK Analyzer
APKの中身を解析するツール
APK Analyzer: Show Bytecode
メソッドの情報にFromJsonアノテーションの情報しか無く、HexColorアノテーションが削除されていることが分かる
-keep interface HexColor
-keep interface HexColor
↓
-keep @com.squareup.moshi.JsonQualifier interface *
JsonQualifierアノテーションが付いた全てのinterfaceを削除/難読化しない
@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class HexColor
class Rectangle(
@field:Json(name = "width") val w: Int,
@field:HexColor @field:Json(name = "color") val c: Int
)
class ColorAdapter {
@ToJson fun toJson(@HexColor rgb: Int): String {
return String.format("#%06x", rgb)
}
@FromJson @HexColor fun fromJson(rgb: String): Int {
return Integer.parseInt(rgb.substring(1), 16)
}
}
-dontwarn okio.**
-dontwarn javax.annotation.**
-keepclasseswithmembers class * {
@com.squareup.moshi.* <methods>;
}
-keep @com.squareup.moshi.JsonQualifier interface *
その3: Keepアノテーション
Keepアノテーション?
クラス、メンバーにつけることで削除、難読化から防ぐことが出来る
@Keep
class User { … }
どのように実現しているのか?
Proguardルールのみで実現している
@Keepのためのルール
-keep class android.support.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
-keep class android.support.annotation.Keep
Keepアノテーションを削除/難読化しない
-keep @android.support.annotation.Keep class * {*;}
Keepアノテーションがついたクラスが対象
クラスとメンバーの削除/難読化をしない
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
Keepアノテーションがついたメソッドが対象
クラスを難読化、メソッドを削除/難読化しない
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
Keepアノテーションがついたフィールドが対象
クラスを難読化、フィールドを削除/難読化しない
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
Keepアノテーションがついたコンストラクタが対象
クラスを難読化、コンストラクタを削除/難読化しない
@Keepのためのルール
-keep class android.support.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
その4: AAPT
ActivityやViewの
Keepルールは(ほぼ)自動生成される
AAPT
AAPT = Android Asset Packaging tool
ただし例外がある
AAPTが対応していない例
DroidKaigi 2018でProguard対応のPR
Process: io.github.droidkaigi.confsched2018.debug
b.h: null cannot be cast to non-null type android.support.v7.widget.SearchView
at io.github.droidkaigi.confsched2018.presentation.search.c.a(SearchFragment.kt:135)
https://github.com/DroidKaigi/conference-app-2018/pull/198#issuecomment-358142894
v7.SearchViewが取得できない
<item
android:id="@+id/action_search"
android:title="@string/search_title"
app:actionViewClass="....widget.SearchView"
app:showAsAction="always" />
上記のMenuレイアウトからSearchViewを参照している
AAPTが対応していなかった
AAPTツールはapp:actionViewClassで指定したクラスに対してProguardルールを生成してくれない
こんなの回避不可能
この例で言うと
この例で言うと
または
Viewに必要なコンストラクタが消されていることが分かる
-keep class android.support.v7.widget.SearchView {
<init>(...);
}
SearchViewの全てのコンストラクタを削除しないためのルールを追加
↓ 先程のProguardを適用
このルールを指定するとViewに必要なコンストラクタが削除されていないことが分かる
こんなの回避不可能
まとめ
おしまい & お前誰よ?
その他/tips
極端な例: RxJava2
以下のコードしか使っていないとする
Observable.just(100)
.map { it.toString() }
.subscribe()
極端な例: RxJava2
(io.reactivex.**のメソッド数)
ほとんどのクラス、メソッドを使用していないのでほぼ削除される
Proguardの出力ファイルについて
consumer proguard
ライブラリ側でProguardを提供することが出来る
ex. leakcanary
https://github.com/square/leakcanary/blob/master/leakcanary-android/consumer-proguard-rules.pro
R8