1 of 85

なんとなく動いている

Proguardから脱出するために

DroidKaigi 2018

佐藤 隼(Sato Shun)

2 of 85

最初に

3 of 85

Proguardってつらい

  • 設定ファイルが難しい(良く分からない)

  • リリースビルドでProguardを有効にすると大量のコンパイル/ランタイムエラー

-dontwarn okhttp3.**

-dontwarn okio.**

-dontwarn javax.annotation.**

-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase

4 of 85

覚えなければいけないことは多い

ただ頻出パターンがあり、そのパターンのいくつかを理解すれば意外となんとかなる

  • 問題が起きても、Proguardの動き、ノウハウに照らし合わせれば納得出来ることがほとんど(自分調べ)

5 of 85

本セッションのゴール

Proguardの基本的な動きと

ユースケースを見ながら

Proguardの理解を深めること

6 of 85

とはいえ本当にProguardは

必要なのか?

7 of 85

Proguardのメリット

コードサイズの縮小/メソッド数の削減

  • メモリ使用量が減る
  • MultiDexを使わなくて済む(かも)
  • Instant Appの4MB制限

-> 具体的にどれくらい削除できるか?

8 of 85

実例: DroidKaigi 2018

Before(2 dex files)

  • classes: 8600 + 3319 = 11919
  • methods: 54219 + 19113 = 73332
  • dex size: 3.5 + 1.3 = 4.8MB

After(1 dex file)

  • classes: 7312
  • methods: 40389
  • dex size: 2.4MB

9 of 85

実例: アメブロアプリ(担当アプリ)

Before(3 dex files)

  • classes: 8402 + 8514 + 6368 = 23284
  • methods: 52260 + 53648 + 37456 = 143364
  • dex size: 3.7 + 3.2 + 2.3 = 9.2MB

After(2 dex file)

  • classes: 8890 + 7523 = 16413
  • methods: 53237 + 38572 = 91809
  • dex size: 3.2 + 1.8 = 5.0MB

10 of 85

Proguardの仕組み/機能

11 of 85

12 of 85

shrink(削除)とobfuscate(難読化)

が重要(ハマりどころ)

13 of 85

削除と難読化

  • 削除(shrink)
    • 設定したエントリポイントから、参照されていないクラス、メンバーを全て削除する
  • 難読化(obfuscate)
    • クラス、メンバー名のリネームを行う
      • ex, class User -> class a
      • ex, getUserByName -> a

14 of 85

エントリポイント?

15 of 85

どうやってエントリポイント、

また難読化の有無を指定するか?

16 of 85

削除、難読化を防ぐKeepルール

削除/難読化をしない

難読化をしない

クラス、メンバー

-keep

-keepnames

メンバー

-keepclassmembers

-keepclassmembernames

メンバーが存在する時

クラス、メンバー

-keepclasseswithmemembers

-keepclasseswithmembernames

17 of 85

Androidにおける具体的な

ユースケース

18 of 85

その1: OkHttp

19 of 85

公式のOkHttp Proguard

-dontwarn okhttp3.**

-dontwarn okio.**

-dontwarn javax.annotation.**

-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase

20 of 85

OkHttpのProguardルール

-dontwarn

-keepnames

2つ種類のルールを指定している

21 of 85

これらのProguardを指定しない時

22 of 85

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

...

23 of 85

Warning地獄

100行以上のWarningが出る

しかしWarningを見ると�can’t find referenced class クラス名�しかない

can’t find referenced class?

24 of 85

can’t find referenced class Nullable?

javax.annotation.Nullableが参照出来ないためWarningが出ている

  • Nullableアノテーションが参照出来ない?

25 of 85

なぜNullableが参照できないのか?

  • Nullableアノテーションはfindbugs:jsr305に組み込まれている
  • OkHttpではfindbugs:jsr305をprovidedで依存関係を指定している
  • provided指定の場合、最終的なjarには含まれない
  • jarに含まれていないので組込んだ側から参照することが出来ない
  • Warning: Can’t find referenced class

26 of 85

なぜprovided指定なのか?

  • JARに組み込んでくれれば良いのに?
  • NullableなどはIDE、人へのヒントのためのアノテーション
    • ランタイム時には必要ないのでJARに含める必要がない。むしろ含めないほうが良い

27 of 85

結論

  • javax.annotation.Nullableはランタイム時に必要ないので参照できなくて問題ない
  • このWarningは無視して良いことが分かった

28 of 85

dontwarnルール

Warningを無視するルール

-dontwarn javax.annotation.Nullable

-dontwarn javax.annotation.**

javax.annotationパッケージ以下に関するWarningを全て無視する

29 of 85

-dontwarn okhttp3.**

-dontwarn okio.**

-dontwarn javax.annotation.**

を指定しWarningが消え、無事にコンパイルが通った

30 of 85

まだルールを指定している

-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase

コンパイルは通ったのにドキュメントには上記が必要と書いてある

  • これは本当に必要なのか?

31 of 85

keepnamesルール

指定したクラス、メンバーを難読化しない

okhttp3.internal.publicsuffix.PublicSuffixDatabase

a.a.a.p

のように難読化されるのを防ぐ

32 of 85

なぜこのクラスを難読化しては駄目か?

PublicSuffixDatabase.java

PublicSuffixDatabase::class.java.getResourceAsStream("publicsuffixes.gz")

PublicSuffixDatabaseクラスの相対パスからgzファイルを読み込んでいる

(このgzファイルはOkHttp jarに含まれる)

33 of 85

PublicSuffixDatabaseが難読化されると

  • クラスパスがa.a.a.pに変更される
  • しかし、gzファイルがあるのはokhttp3.internal.publicsuffixパッケージ内
  • 相対パスでファイルを開けなくなる
    • gzファイルと同じパッケージを保つ必要がある

34 of 85

クラスパスを変更しては駄目

-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase

難読化を防ぎ、クラスパスを保つ

  • 無事、gzファイルを開くこと出来た

35 of 85

-dontwarn okhttp3.**

-dontwarn okio.**

-dontwarn javax.annotation.**

-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase

36 of 85

その2: moshi

37 of 85

moshiとは?

  • A modern JSON library for Android and Java
    • 似たライブラリだとGsonやJacksonなど
  • serialize/deserializeライブラリ + Proguardはハマりどころある
    • リフレクションを使っていることが多いため

38 of 85

公式のmoshi Proguard

-dontwarn okio.**

-dontwarn javax.annotation.**

-keepclasseswithmembers class * {

@com.squareup.moshi.* <methods>;

}

-keep @com.squareup.moshi.JsonQualifier interface *

39 of 85

moshiの公式Proguardルール

-dontwarn

-keepclasseswithmembers

-keep

3つの種類のルールを使用している

  • dontwarnはOkHttpと同様の理由で必要(can’t find referenced class)

40 of 85

moshi: サンプルコード

val moshi = Moshi.Builder()

.add(ColorAdapter())

.build()

val r = moshi

.adapter(Rectangle::class.java)

.fromJson("{\"width\":1,\"color\":\"#ff0000\"}")

41 of 85

@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適用前

42 of 85

@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適用後

43 of 85

2つの問題が発生

1. ColorAdapterのメソッドが削除される

2. @HexColorが削除される

これらはRuntime時に必要なので、削除してはいけない

  • 過剰に削除してしまった

44 of 85

ColorAdapterのメソッドが削除される

  • moshiでは@ToJson、@FromJsonを目印にメソッド呼び出しを行う
    • 直接的にJavaコード上からメソッド呼び出しを行わないため削除される
  • @ToJson、 @FromJsonがついたメソッドを削除から防ぐ必要がある

45 of 85

-keepclasseswithmembers class ColorAdapter {

@com.squareup.moshi.ToJson <methods>;

@com.squareup.moshi.FromJson <methods>;

}

46 of 85

-keepclasseswithmembers class ColorAdapter {

@com.squareup.moshi.ToJson <methods>;

@com.squareup.moshi.FromJson <methods>;

}

-keepclasseswithmembers class * {

@com.squareup.moshi.* <methods>;

}

47 of 85

@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)

}

}

48 of 85

@HexColor削除問題

@HexColorはメソッド、フィールドについているだけで、Javaコード上から参照されていない

-> 結果、Proguardが削除する

-> なぜ削除されているのか分かるか?

49 of 85

APK Analyzer

APKの中身を解析するツール

50 of 85

APK Analyzer: Show Bytecode

メソッドの情報にFromJsonアノテーションの情報しか無く、HexColorアノテーションが削除されていることが分かる

51 of 85

-keep interface HexColor

52 of 85

-keep interface HexColor

-keep @com.squareup.moshi.JsonQualifier interface *

JsonQualifierアノテーションが付いた全てのinterfaceを削除/難読化しない

53 of 85

@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)

}

}

54 of 85

-dontwarn okio.**

-dontwarn javax.annotation.**

-keepclasseswithmembers class * {

@com.squareup.moshi.* <methods>;

}

-keep @com.squareup.moshi.JsonQualifier interface *

55 of 85

その3: Keepアノテーション

56 of 85

Keepアノテーション?

クラス、メンバーにつけることで削除、難読化から防ぐことが出来る

@Keep

class User { … }

どのように実現しているのか?

57 of 85

Proguardルールのみで実現している

  • @Keep用のProguardルールがデフォルトのproguard-android.txtに含まれている
    • Proguardのkeepルールを使い実現している
  • @Keepのための特別なことは何もしていない!!

58 of 85

@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>(...);

}

59 of 85

-keep class android.support.annotation.Keep

Keepアノテーションを削除/難読化しない

60 of 85

-keep @android.support.annotation.Keep class * {*;}

Keepアノテーションがついたクラスが対象

クラスとメンバーの削除/難読化をしない

61 of 85

-keepclasseswithmembers class * {

@android.support.annotation.Keep <methods>;

}

Keepアノテーションがついたメソッドが対象

クラスを難読化、メソッドを削除/難読化しない

62 of 85

-keepclasseswithmembers class * {

@android.support.annotation.Keep <fields>;

}

Keepアノテーションがついたフィールドが対象

クラスを難読化、フィールドを削除/難読化しない

-keepclasseswithmembers class * {

@android.support.annotation.Keep <init>(...);

}

Keepアノテーションがついたコンストラクタが対象

クラスを難読化、コンストラクタを削除/難読化しない

63 of 85

@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>(...);

}

64 of 85

その4: AAPT

65 of 85

ActivityやViewの

Keepルールは(ほぼ)自動生成される

66 of 85

AAPT

AAPT = Android Asset Packaging tool

  • Android Manifest、Layout(Menu) XMLを解析し、自動でProguardルールを生成する
    • ActivityなどのComponents、Viewに対しては、基本的にProguardルールの指定は必要ない

ただし例外がある

67 of 85

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

68 of 85

v7.SearchViewが取得できない

<item

android:id="@+id/action_search"

android:title="@string/search_title"

app:actionViewClass="....widget.SearchView"

app:showAsAction="always" />

上記のMenuレイアウトからSearchViewを参照している

69 of 85

AAPTが対応していなかった

AAPTツールはapp:actionViewClassで指定したクラスに対してProguardルールを生成してくれない

  • android:actionViewClassで指定した場合はProguardルールを生成してくれる

70 of 85

こんなの回避不可能

  • Proguardは絶対に何かの問題が起こる
    • 問題が起きたときに対処するための知識・ノウハウが必要

71 of 85

この例で言うと

  • エラーログからSearchViewが取得できていないことが分かる
  • Menu Layoutから指定しているクラスなのでAAPTが生成したaapt_rules.txtを見る
  • SearchViewのProguardが生成されていないことを確認
  • 自動生成してくれなそうなので、自分で適切なProguardルールを指定する

72 of 85

この例で言うと

  • エラーログからSearchViewが取得できていないことが分かる
  • Menu Layoutから指定しているクラスなのでAAPTが生成したaapt_rules.txtを見る
  • SearchViewのProguardが生成されていないことを確認
  • 自動生成してくれなそうなので、自分で適切なProguardルールを指定する

73 of 85

または

  • エラーログからSearchViewが取得できていないことが分かる
  • Android Studio のAPK Analyzerを使い、classes.dexの中身を確認する
  • SearchViewのコンストラクタが消されていることが分かる

74 of 85

Viewに必要なコンストラクタが消されていることが分かる

75 of 85

-keep class android.support.v7.widget.SearchView {

<init>(...);

}

SearchViewの全てのコンストラクタを削除しないためのルールを追加

76 of 85

↓ 先程のProguardを適用

このルールを指定するとViewに必要なコンストラクタが削除されていないことが分かる

77 of 85

こんなの回避不可能

  • Proguardは絶対に何かの問題が起こる
    • 問題が起きたときに対処するための知識・ノウハウが必要

78 of 85

まとめ

  • Proguardは基礎、仕組みをある程度知ってしまえば意外と何とかなる(重要)
    • ただ絶対に何かしらの問題が起こるので慌てない心が必要
  • keepルールのバリエーションはやりながら覚えるのが良いと思います(小並)
  • Proguardを使って最高のアプリサイズを達成しましょう!!!!

79 of 85

おしまい & お前誰よ?

Sato Shun/佐藤 隼

Twitter: @stsn_jp

Github: satoshun

Organization/所属: CyberAgent, inc.

80 of 85

その他/tips

81 of 85

極端な例: RxJava2

以下のコードしか使っていないとする

Observable.just(100)

.map { it.toString() }

.subscribe()

82 of 85

極端な例: RxJava2

(io.reactivex.**のメソッド数)

  • Before: 9186
  • After: 113

ほとんどのクラス、メソッドを使用していないのでほぼ削除される

83 of 85

Proguardの出力ファイルについて

  • seeds.txt (Proguardルール解析フェーズ)
    • 何かしらのkeepルールにマッチ
  • usage.txt (shrinkフェーズ)
    • 削除されるクラス、メンバーのリスト
  • mapping.txt (obfuscateフェーズ)
    • 難読化されたクラス、メンバーの対応表
  • dump.txt
    • Proguardの結果

84 of 85

consumer proguard

ライブラリ側でProguardを提供することが出来る

ex. leakcanary

https://github.com/square/leakcanary/blob/master/leakcanary-android/consumer-proguard-rules.pro

85 of 85

R8

  • Proguardファイルと互換がある
  • Proguardよりコードサイズの削減、バイトコード最適化が期待できる