R8 をフルモードで使用する

R8 には、互換モードとフルモードの 2 つのモードがあります。フルモードでは、アプリのパフォーマンスを向上させる強力な最適化を利用できます。

このガイドは、R8 の最も強力な最適化を使用したい Android デベロッパーを対象としています。互換モードとフルモードの主な違いと、プロジェクトを安全に移行して一般的なランタイム クラッシュを回避するために必要な明示的な構成について説明します。

フルモードを有効にする

フルモードを有効にするには、gradle.properties ファイルから次の行を削除します。

android.enableR8.fullMode=false // Remove this line to enable full mode

属性に関連付けられたクラスを保持する

属性は、実行可能コードの一部ではない、コンパイル済みクラス ファイル内に保存されるメタデータです。ただし、特定のタイプのリフレクションで必要になることがあります。一般的な例としては、Signature(型消去後にジェネリック型情報を保持)、InnerClassesEnclosingMethod(クラス構造のリフレクション用)、ランタイムで表示可能なアノテーションなどがあります。

次のコードは、バイトコードのフィールドの Signature 属性を示しています。フィールドの場合:

List<User> users;

コンパイル済みクラス ファイルには、次のバイトコードが含まれます。

.field public static final users:Ljava/util/List;
    .annotation system Ldalvik/annotation/Signature;
        value = {
            "Ljava/util/List<",
            "Lcom/example/package/User;",
            ">;"
        }
    .end annotation
.end field

リフレクションを多用するライブラリ(Gson など)は、これらの属性に依存してコードの構造を動的に検査し、理解することがよくあります。R8 のフルモードでは、デフォルトで、関連付けられたクラス、フィールド、メソッドが明示的に保持されている場合にのみ、属性が保持されます。

次の例は、属性が必要な理由と、互換モードからフルモードに移行する際に必要な keep ルールを示しています。リフレクションされるクラス、フィールド、メソッドを保持するだけでなく、それらが依存する属性も明示的に保持する必要があります。

Gson ライブラリを使用してユーザーのリストを逆シリアル化する次の例を考えてみましょう。


import com.google.gson.Gson
import com.google.gson.reflect.TypeToken

data class User(
    @SerializedName("username")
    var username: String? = null,
    @SerializedName("age")
    var age: Int = 0
)

fun GsonRemoteJsonListExample() {
    val gson = Gson()

    // 1. The JSON string for a list of users returned from remote
    val jsonOutput = """[{"username":"alice","age":30}, {"username":"bob","age":25}]"""

    // 2. Deserialize the JSON string into a List<User>
    // We must use TypeToken for generic types like List
    val listType = object : TypeToken<List<User>>() {}.type
    val deserializedList: List<User> = gson.fromJson(jsonOutput, listType)

    // Print the list
    println("First user from list: ${deserializedList}")
}

コンパイル時に、Java の型消去によってジェネリック型引数が削除されます。つまり、ランタイムでは、List<String>List<User> の両方が生の List として表示されます。そのため、リフレクションに依存する Gson などのライブラリは、JSON リストを逆シリアル化するときに、 List に含まれるように宣言された特定のオブジェクト タイプを判別できず、ランタイムの問題が発生する可能性があります。

型情報を保持するために、Gson は TypeToken を使用します。TypeToken をラップすると、必要な逆シリアル化情報が保持されます。

Kotlin 式 object:TypeToken<List<User>>() {}.type は、 TypeToken を拡張してジェネリック型 情報をキャプチャする匿名内部クラスを作成します。この例では、匿名クラスの名前は $GsonRemoteJsonListExample$listType$1 です。

Java プログラミング言語は、スーパークラスのジェネリック シグネチャを、コンパイル済みクラス ファイル内の Signature 属性というメタデータとして保存します。 TypeToken は、この Signature メタデータを使用して、ランタイムで型を復元します。 これにより、Gson はリフレクションを使用して Signature を読み取り、逆シリアル化に必要な完全な List<User> 型を正常に 検出できます。

R8 が互換モードで有効になっている場合、特定の keep ルールが明示的に定義されていない場合でも、$GsonRemoteJsonListExample$listType$1 などの匿名内部クラスを含むクラスの Signature 属性が保持されます。そのため、R8 互換モードでは、この例が想定どおりに動作するために、明示的な keep ルールを追加する必要はありません。

// keep rule for compatibility mode
-keepattributes Signature

R8 がフルモードで有効になっている場合、匿名内部クラス $GsonRemoteJsonListExample$listType$1Signature 属性は削除されます。Signature にこの型情報がない場合、Gson は正しいアプリケーション タイプを見つけることができず、IllegalStateException が発生します。

2.11.0 より前のバージョンの Gson を使用している場合、これを防ぐために必要な keep ルールは次のとおりです。

// keep rule required for full mode
-keepattributes Signature
-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken { *; }
-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken
  • -keepattributes Signature: このルールは、Gson が読み取る必要のある属性を保持するように R8 に指示します。フルモードでは、R8 は keep ルールで明示的に一致するクラス、フィールド、メソッドの Signature 属性のみを保持します。

  • -keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken: このルールは、 TypeToken が逆シリアル化されるオブジェクトの型をラップするため必要です。型消去後、ジェネリック型情報を保持するために匿名内部クラスが作成されます。com.google.gson.reflect.TypeToken を明示的に保持しない場合、フルモードの R8 は、逆シリアル化に必要な Signature 属性にこのクラスタイプを含めません。

  • -keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken: This rule retains the type information of anonymous classes that extend TypeToken, such as $GsonRemoteJsonListExample$listType$1 in this example. このルールがない場合、フルモードの R8 は必要な型情報を削除し、逆シリアル化が失敗します。

前述のルールは、ジェネリック型(List<User> など)の検出の 問題のみを解決するものであることを理解しておくことが重要です。R8 はクラスのフィールドの名前も 変更します。データモデルで @SerializedName アノテーションを使用しない場合、フィールド名が JSON キーと一致しなくなるため、Gson は JSON を逆シリアル化できません。

ただし、2.11 より前のバージョンの Gson を使用している場合、またはモデルで @SerializedName アノテーションを使用していない場合は、それらのモデルに明示的な keep ルールを追加する必要があります。

デフォルト コンストラクタを保持する

R8 フルモードでは、クラス自体が保持されている場合でも、引数なし/デフォルト コンストラクタは暗黙的に保持されません。class.getDeclaredConstructor().newInstance() または class.newInstance() を使用してクラスのインスタンスを作成する場合は、フルモードで引数なしのコンストラクタを明示的に保持する必要があります。一方、互換モードでは、引数なしのコンストラクタは常に保持されます。

リフレクションを使用して PrecacheTask のインスタンスを作成し、その run メソッドを動的に呼び出す例を考えてみましょう。このシナリオでは、互換モードでは追加のルールは必要ありませんが、フルモードでは PrecacheTask のデフォルト コンストラクタが削除されます。そのため、特定の keep ルールが必要です。

// In library
interface StartupTask {
    fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
    fun execute(taskClass: Class<out StartupTask>) {
        // The class isn't removed, but its constructor might be.
        val task = taskClass.getDeclaredConstructor().newInstance()
        task.run()
    }
}

// In app
class PreCacheTask : StartupTask {
    override fun run() {
        Log.d("Pre cache task", "Warming up the cache...")
    }
}

fun runTaskRunner() {
    // The library is given a direct reference to the app's task class.
    TaskRunner.execute(PreCacheTask::class.java)
}
# Full mode keep rule
# default constructor needs to be specified

-keep class com.example.fullmoder8.PreCacheTask {
    <init>();
}

アクセス変更はデフォルトで有効になっている

互換モードでは、R8 はクラス内のメソッドとフィールドの可視性を変更しません。ただし、フルモードでは、R8 はメソッドとフィールドの可視性を変更(たとえば、private から public に変更)することで最適化を強化します。 これにより、インライン化が強化されます。

この最適化により、特定の可視性を持つメンバーに依存するリフレクションをコードで使用している場合に問題が発生する可能性があります。R8 はこの間接的な使用を認識せず、アプリがクラッシュする可能性があります。これを防ぐには、特定の -keep ルールを追加してメンバーを保持する必要があります。これにより、元の可視性も保持されます。

詳細については、リフレクションを使用して非公開 メンバーにアクセスすることが推奨されない理由と、それらの フィールド/メソッドを保持するための keep ルールについて、こちらのをご覧ください。

Kotlin 固有のメタデータ

Kotlin コードをコンパイルすると、Kotlin コンパイラは言語固有のメタデータ(null 可能性、拡張関数、コルーチン シグネチャなど)を各クラス ファイルの @kotlin.Metadata アノテーションに保存します。

アプリまたはその依存関係で Kotlin リフレクション(kotlin.reflect)を使用している場合、リフレクション ライブラリはランタイムでこのメタデータを解析してクラス構造を検査します。 R8 フルモードでは、アノテーションが明示的に保持されていない場合、R8 はデフォルトでアノテーションを削除します。また、メタデータを保持して更新せずに R8 がクラスを縮小または圧縮すると、Kotlin リフレクションはランタイムで失敗し、予測できない動作やクラッシュ(KotlinReflectionInternalError など)が発生します。

予測できない動作を防ぎ、縮小後に Kotlin リフレクションが正しく機能するようにするには、ランタイムで表示可能なアノテーションを保持し、kotlin.Metadata クラスを明示的に保持する必要があります。

# Preserve runtime-visible annotations required for inspecting metadata
-keepattributes RuntimeVisibleAnnotations

# Keep Kotlin metadata to ensure kotlin.reflect functions correctly
-keep class kotlin.Metadata { *; }