保持ルールを追加する

大まかに言うと、保持ルールは、クラス(またはサブクラスや実装)を指定し、そのクラス内で保持するメンバー(メソッド、コンストラクタ、フィールド)を指定します。

保持ルールの一般的な構文は次のとおりです。ただし、特定の保持オプションでは、オプションの keep_option_modfier は使用できません。


-<keep_option>[,<keep_option_modifier_1>,<keep_option_modifier_2>,...] <class_specification>

次の例は、keepclassmembers を保持オプション、allowoptimization を修飾子として使用し、com.example.MyClass から someSpecificMethod() を保持する保持ルールの例です。

-keepclassmembers,allowoptimization class com.example.MyClass {
  void someSpecificMethod();
}

保持オプション

保持オプションは、保持ルールの最初の部分です。クラスのどの側面を保持するかを指定します。keepkeepclassmemberskeepclasseswithmemberskeepnameskeepclassmembernameskeepclasseswithmembernames の 6 つの保持オプションがあります。

次の表に、これらの保持オプションを示します。

Keep オプション Description
keepclassmembers 最適化後にクラスが存在する場合にのみ、指定されたメンバーを保持します。
keep 指定されたクラスと指定されたメンバー(フィールドとメソッド)を保持し、最適化されないようにします。

: keep は、一致するクラスで最適化が行われないようにするため、通常は keep オプション修飾子でのみ使用します。keep
keepclasseswithmembers クラスにクラス仕様のすべてのメンバーが含まれている場合にのみ、クラスとその指定されたメンバーを保持します。
keepclassmembernames 指定されたクラス メンバーの名前変更を防止しますが、クラスまたはそのメンバーの削除は防止しません。

注: このオプションの意味は誤解されることが多いため、同等の -keepclassmembers,allowshrinking の使用を検討してください。
keepnames クラスとそのメンバーの名前変更は禁止されますが、未使用と判断された場合は完全に削除される可能性があります。

注: このオプションの意味は誤解されることが多いため、同等の -keep,allowshrinking の使用を検討してください。
keepclasseswithmembernames クラスとその指定されたメンバーの名前変更を禁止します。ただし、メンバーが最終コードに存在する場合に限ります。コードの削除を防ぐことはできません。

注: このオプションの意味は誤解されることが多いため、同等の -keepclasseswithmembers,allowshrinking を使用することを検討してください。

適切な保存オプションを選択する

適切な保持オプションを選択することは、アプリの適切な最適化を決定するうえで非常に重要です。特定の保持オプションは、参照されていないコードを削除するプロセスであるコードの縮小を行います。一方、別の保持オプションはコードの難読化(名前の変更)を行います。次の表に、さまざまな保持オプションの動作を示します。

Keep オプション クラスを縮小する クラスを難読化する メンバーを縮小する メンバーを難読化する
keep
keepclassmembers
keepclasseswithmembers
keepnames
keepclassmembernames
keepclasseswithmembernames

Keep オプションの修飾子

保持オプション修飾子は、保持ルールのスコープと動作を制御するために使用されます。保持ルールには、0 個以上の保持オプション修飾子を追加できます。

keep オプション修飾子の有効な値は、次の表で説明しています。

説明
allowoptimization 指定した要素の最適化を許可します。ただし、指定された要素の名前は変更されず、削除もされません。
allowobfucastion 指定された要素の名前変更を許可します。ただし、要素が削除されたり、最適化されたりすることはありません。
allowshrinking R8 が指定された要素への参照を検出しない場合、その要素の削除を許可します。ただし、要素の名前が変更されたり、最適化されたりすることはありません。
includedescriptorclasses 保持されるメソッド(パラメータの型と戻り値の型)とフィールド(フィールドの型)の記述子に現れるすべてのクラスを保持するよう R8 に指示します。
allowaccessmodification 最適化プロセス中に、R8 がクラス、メソッド、フィールドのアクセス修飾子(publicprivateprotected)を変更(通常は拡大)できるようにします。
allowrepackage R8 がデフォルト(ルート)パッケージを含む、異なるパッケージにクラスを移動できるようにします。

クラスの仕様

クラス、スーパークラス、または実装されたインターフェースは、保持ルールの一部として指定する必要があります。java.lang.String などの java.lang 名前空間のクラスを含むすべてのクラスは、完全修飾 Java 名を使用して指定する必要があります。使用する名前を理解するには、生成された Java 名を取得するで説明されているツールを使用してバイトコードを検査します。

次の例は、MaterialButton クラスの指定方法を示しています。

  • 適切: com.google.android.material.button.MaterialButton
  • 不正解: MaterialButton

クラス仕様では、保持するクラス内のメンバーも指定します。次のルールは、MaterialButton クラスとそのすべてのメンバーを保持します。

-keep class com.google.android.material.button.MaterialButton { *; }

サブクラスと実装

インターフェースを実装するサブクラスまたはクラスをターゲットにするには、それぞれ extendimplements を使用します。

たとえば、次のようにサブクラス Foo を持つクラス Bar があるとします。

class Foo : Bar()

次の保持ルールは、Bar のすべてのサブクラスを保持します。保持ルールにはスーパークラス Bar 自体は含まれていないことに注意してください。

-keep class * extends Bar

Bar を実装するクラス Foo がある場合:

class Foo : Bar

次の保持ルールは、Bar を実装するすべてのクラスを保持します。保持ルールにはインターフェース Bar 自体は含まれていません。

-keep class * implements Bar

アクセス修飾子

publicprivatestaticfinal などのアクセス修飾子を指定して、保持ルールをより正確にすることができます。

たとえば、次のルールでは、api パッケージとそのサブパッケージ内のすべての public クラスと、これらのクラス内のすべての public メンバーと protected メンバーが保持されます。

-keep public class com.example.api.** { public protected *; }

クラス内のメンバーに修飾子を使用することもできます。たとえば、次のルールは Utils クラスの public static メソッドのみを保持します。

-keep class com.example.Utils {
    public static void *(...);
}

Kotlin 固有の修飾子

R8 は、internalsuspend などの Kotlin 固有の修飾子をサポートしていません。このようなフィールドを保持するには、次のガイドラインを使用します。

  • internal クラス、メソッド、フィールドを保持するには、それらを public として扱います。たとえば、次の Kotlin ソースについて考えてみましょう。

    package com.example
    internal class ImportantInternalClass {
      internal f: Int
      internal fun m() {}
    }
    

    Kotlin コンパイラによって生成された .class ファイルでは、internal クラス、メソッド、フィールドは public になります。そのため、次の例に示すように、public キーワードを使用する必要があります。

    -keepclassmembers public class com.example.ImportantInternalClass {
      public int f;
      public void m();
    }
    
  • suspend メンバーがコンパイルされたら、コンパイルされたシグネチャを保持ルールで照合します。

    たとえば、次のスニペットに示すように fetchUser 関数が定義されているとします。

    suspend fun fetchUser(id: String): User
    

    コンパイルすると、バイトコードのシグネチャは次のようになります。

    public final Object fetchUser(String id, Continuation<? super User> continuation);
    

    この関数のキープルールを記述するには、このコンパイル済みシグネチャを照合するか、... を使用する必要があります。

    コンパイルされたシグネチャの使用例を次に示します。

    -keepclassmembers class com.example.repository.UserRepository {
    public java.lang.Object fetchUser(java.lang.String,  kotlin.coroutines.Continuation);
    }
    

    ... を使用した例を次に示します。

    -keepclassmembers class com.example.repository.UserRepository {
    public java.lang.Object fetchUser(...);
    }
    

メンバーの詳細

クラス仕様には、保持するクラス メンバーをオプションで含めることができます。クラスに 1 つ以上のメンバーを指定すると、ルールはそれらのメンバーにのみ適用されます。

たとえば、特定のクラスとそのすべてのメンバーを保持するには、次のようにします。

-keep class com.myapp.MyClass { *; }

クラスのみを保持し、メンバーを保持しない場合は、次のようにします。

-keep class com.myapp.MyClass

ほとんどの場合、メンバーを指定する必要があります。たとえば、次の例では、クラス MyClass 内にパブリック フィールド text とパブリック メソッド updateText() を保持しています。

-keep class com.myapp.MyClass {
    public java.lang.String text;
    public void updateText(java.lang.String);
}

すべての公開フィールドと公開メソッドを保持するには、次の例をご覧ください。

-keep public class com.example.api.ApiClient {
    public *;
}

メソッド

保持ルールのメンバー仕様でメソッドを指定する構文は次のとおりです。

[<access_modifier>] [<return_type>] <method_name>(<parameter_types>);

たとえば、次の保持ルールは、void を返し String を受け取る setLabel() という名前の公開メソッドを保持します。

-keep class com.example.MyView {
    public void setLabel(java.lang.String);
}

<methods> をショートカットとして使用して、クラス内のすべてのメソッドを次のように照合できます。

-keep class com.example.MyView {
    <methods>;
}

戻り値の型とパラメータの型を指定する方法については、をご覧ください。

コンストラクタ

コンストラクタを指定するには、<init> を使用します。キープルールのメンバー仕様でコンストラクタを指定する構文は次のとおりです。

[<access_modifier>] <init>(parameter_types);

たとえば、次のキープルールは、ContextAttributeSet を受け取るカスタム View コンストラクタを保持します。

-keep class com.example.ui.MyCustomView {
    public <init>(android.content.Context, android.util.AttributeSet);
}

すべての公開コンストラクタを保持するには、次の例を参考にしてください。

-keep class com.example.ui.MyCustomView {
    public <init>(...);
}

フィールド

保持ルールのメンバー指定でフィールドを指定する構文は次のとおりです。

[<access_modifier>...] [<type>] <field_name>;

たとえば、次の保持ルールは、userId という名前のプライベート文字列フィールドと、STATUS_ACTIVE という名前のパブリック静的整数フィールドを保持します。

-keep class com.example.models.User {
    private java.lang.String userId;
    public static int STATUS_ACTIVE;
}

<fields> をショートカットとして使用して、クラス内のすべてのフィールドを次のように照合できます。

-keep class com.example.models.User {
    <fields>;
}

パッケージ レベルの関数

クラスの外で定義された Kotlin 関数(一般にトップレベル関数と呼ばれます)を参照するには、Kotlin コンパイラによって暗黙的に追加されたクラスの生成された Java 名を使用してください。クラス名は、Kotlin ファイル名に Kt を付加したものです。たとえば、次のように定義された MyClass.kt という名前の Kotlin ファイルがあるとします。

package com.example.myapp.utils

// A top-level function not inside a class
fun isEmailValid(email: String): Boolean {
    return email.contains("@")
}

isEmailValid 関数の保持ルールを作成するには、クラス仕様で生成されたクラス MyClassKt をターゲットにする必要があります。

-keep class com.example.myapp.utils.MyClassKt {
    public static boolean isEmailValid(java.lang.String);
}

このセクションでは、キープルールのメンバー仕様で戻り値の型、パラメータの型、フィールドの型を指定する方法について説明します。Kotlin ソースコードと異なる場合は、生成された Java 名を使用して型を指定してください。

プリミティブ型

プリミティブ型を指定するには、その Java キーワードを使用します。R8 は、booleanbyteshortcharintlongfloatdouble のプリミティブ型を認識します。

プリミティブ型を含むルールの例を次に示します。

# Keeps a method that takes an int and a float as parameters.
-keepclassmembers class com.example.Calculator {
    public void setValues(int, float);
}

一般的な種類

コンパイル中に Kotlin/Java コンパイラはジェネリック型情報を消去するため、ジェネリック型を含む保持ルールを記述する場合は、元のソースコードではなく、コードのコンパイル済み表現をターゲットにする必要があります。ジェネリック型がどのように変更されるかについて詳しくは、型消去をご覧ください。

たとえば、Box.kt で定義された上限のない汎用型を含む次のコードがあるとします。

package com.myapp.data

class Box<T>(val item: T) {
    fun getItem(): T {
        return item
    }
}

型消去後、TObject に置き換えられます。クラスのコンストラクタとメソッドを保持するには、ルールで汎用の T の代わりに java.lang.Object を使用する必要があります。

保持ルールの例を次に示します。

# Keep the constructor and methods of the Box class.
-keep class com.myapp.data.Box {
    public init(java.lang.Object);
    public java.lang.Object getItem();
}

NumberBox.kt にバウンドされた汎用型を含む次のコードがある場合:

package com.myapp.data

// T is constrained to be a subtype of Number
class NumberBox<T : Number>(val number: T)

この場合、型消去では T がその境界 java.lang.Number に置き換えられます。

保持ルールの例を次に示します。

-keep class com.myapp.data.NumberBox {
    public init(java.lang.Number);
}

アプリ固有の汎用型をベースクラスとして使用する場合は、ベースクラスの保持ルールも追加する必要があります。

たとえば、次のコードの場合:

package com.myapp.data

data class UnpackOptions(val useHighPriority: Boolean)

// The generic Box class with UnpackOptions as the bounded type
class Box<T: UnpackOptions>(val item: T) {
}

includedescriptorclasses でキープルールを使用すると、次のように 1 つのルールで UnpackOptions クラスと Box クラスメソッドの両方を保持できます。

-keep,includedescriptorclasses class com.myapp.data.Box {
    public <init>(com.myapp.data.UnpackOptions);
}

オブジェクトのリストを処理する特定の関数を保持するには、関数のシグネチャに正確に一致するルールを記述する必要があります。ジェネリック型は消去されるため、List<Product> などのパラメータは java.util.List として認識されます。

たとえば、次のように Product オブジェクトのリストを処理する関数を含むユーティリティ クラスがあるとします。

package com.myapp.utils

import com.myapp.data.Product
import android.util.Log

class DataProcessor {
    // This is the function we want to keep
    fun processProducts(products: List<Product>) {
        Log.d("DataProcessor", "Processing ${products.size} products.")
        // Business logic ...
    }
}

// The data class used in the list (from the previous example)
package com.myapp.data
data class Product(val id: String, val name: String)

次の保持ルールを使用すると、processProducts 関数のみを保護できます。

-keep class com.myapp.utils.DataProcessor {
    public void processProducts(java.util.List);
}

配列型

配列の各ディメンションのコンポーネント型に [] を追加して、配列型を指定します。これは、クラス型とプリミティブ型の両方に適用されます。

  • 1 次元のクラス配列: java.lang.String[]
  • 二次元プリミティブ配列: int[][]

たとえば、次のコードがあるとします。

package com.example.data

class ImageProcessor {
  fun process(): ByteArray {
    // process image to return a byte array
  }
}

次の保持ルールを使用できます。

# Keeps a method that returns a byte array.
-keepclassmembers class com.example.data.ImageProcessor {
    public byte[] process();
}

ワイルドカード

次の表は、ワイルドカードを使用して、特定のパターンに一致する複数のクラスまたはメンバーに保持ルールを適用する方法を示しています。

ワイルドカード クラスまたはメンバーに適用 説明
** 両方 最も一般的に使用されます。任意の数のパッケージ区切り文字を含む、任意の型名に一致します。これは、パッケージとそのサブパッケージ内のすべてのクラスを照合する場合に便利です。
* 両方 クラス仕様の場合、パッケージ区切り文字(.)を含まない型名の任意の部分に一致します。
メンバー仕様の場合、メソッド名またはフィールド名に一致します。単独で使用される場合は、** のエイリアスにもなります。
両方 クラス名またはメンバー名内の任意の 1 文字に一致します。
*** メンバー プリミティブ型(int など)、クラス型(java.lang.String など)、任意の次元の配列型(byte[][] など)を含む任意の型と一致します。
メンバー メソッドのパラメータのリストと一致します。
% メンバー 任意のプリミティブ型(`int`、`float`、`boolean` など)に一致します。

特殊なワイルドカードの使用例をいくつかご紹介します。

  • 同じ名前で異なるプリミティブ型を入力として受け取るメソッドが複数ある場合は、% を使用して、それらをすべて保持する保持ルールを記述できます。たとえば、次の DataStore クラスには複数の setValue メソッドがあります。

    class DataStore {
        fun setValue(key: String, value: Int) { ... }
        fun setValue(key: String, value: Boolean) { ... }
        fun setValue(key: String, value: Float) { ... }
    }
    

    次の保持ルールは、すべてのメソッドを保持します。

    -keep class com.example.DataStore {
        public void setValue(java.lang.String, %);
    }
    
  • 名前が 1 文字だけ異なるクラスが複数ある場合は、? を使用して、それらをすべて保持する保持ルールを記述します。たとえば、次のクラスがあるとします。

    com.example.models.UserV1 {...}
    com.example.models.UserV2 {...}
    com.example.models.UserV3 {...}
    

    次の保持ルールは、すべてのクラスを保持します。

    -keep class com.example.models.UserV?
    
  • クラス ExampleAnotherExample(ルートレベルのクラスの場合)は一致させるが、com.foo.Example は一致させない場合は、次の保持ルールを使用します。

    -keep class *Example
    
  • * を単独で使用すると、** のエイリアスとして機能します。たとえば、次の保持ルールは同等です。

    -keepclasseswithmembers class * { public static void main(java.lang.String[];) }
    
    -keepclasseswithmembers class ** { public static void main(java.lang.String[];) }
    

生成された Java 名を検査する

キープルールを記述する際は、Java バイトコードにコンパイルされた後の名前を使用して、クラスやその他の参照型を指定する必要があります(例については、クラスの指定をご覧ください)。コード用に生成された Java 名を確認するには、Android Studio で次のいずれかのツールを使用します。

  • APK Analyzer
  • Kotlin ソースファイルを開いた状態で、[Tools] > [Kotlin] > [Show Kotlin Bytecode] > [Decompile] に移動して、バイトコードを検査します。