R8 には、互換モードとフルモードの 2 つのモードがあります。フルモードでは、アプリのパフォーマンスを向上させる強力な最適化が提供されます。
このガイドは、R8 の最も強力な最適化を使用したい Android デベロッパーを対象としています。互換モードとフルモードの主な違いについて説明し、プロジェクトを安全に移行して一般的なランタイム クラッシュを回避するために必要な明示的な構成を提供します。
フルモードを有効にする
フルモードを有効にするには、gradle.properties ファイルから次の行を削除します。
android.enableR8.fullMode=false // Remove this line to enable full mode
属性に関連付けられたクラスを保持する
属性は、実行可能コードの一部ではないコンパイル済みクラスファイル内に保存されるメタデータです。ただし、特定の種類の振り返りには必要になることがあります。一般的な例としては、Signature(型消去後にジェネリック型の情報を保持)、InnerClasses と EnclosingMethod(クラス構造をリフレクションするため)、実行時に可視のアノテーションなどがあります。
次のコードは、バイトコードのフィールドの 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 のフルモードでは、デフォルトで、関連付けられたクラス、フィールド、メソッドが明示的に保持されている場合にのみ、属性が保持されます。
次の例は、属性が必要な理由と、互換モードからフルモードに移行する際に必要な保持ルールを示しています。
次の例では、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 が有効になっている場合、特定の保持ルールが明示的に定義されていなくても、$GsonRemoteJsonListExample$listType$1 などの匿名内部クラスを含むクラスの Signature 属性が保持されます。そのため、R8 互換モードでは、この例が想定どおりに動作するために、明示的なキープルールを追加する必要はありません。
// keep rule for compatibility mode
-keepattributes Signature
R8 がフルモードで有効になっている場合、匿名内部クラス $GsonRemoteJsonListExample$listType$1 の Signature 属性は削除されます。Signature にこの型情報がないと、Gson は正しいアプリケーション型を見つけることができず、IllegalStateException が発生します。これを防ぐために必要な 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: このルールは、この例の$GsonRemoteJsonListExample$listType$1など、TypeTokenを拡張する匿名クラスの型情報を保持します。このルールがないと、フルモードの R8 は必要な型情報を削除し、逆シリアル化が失敗します。
Gson バージョン 2.11.0 以降では、ライブラリはフルモードでの逆シリアル化に必要な必要な保持ルールをバンドルします。R8 を有効にしてアプリをビルドすると、R8 はライブラリからこれらのルールを自動的に検索して適用します。これにより、プロジェクトでこれらの特定のルールを手動で追加または維持することなく、アプリに必要な保護を提供できます。
前述のルールは、汎用型(List<User>)。R8 はクラスのフィールドの名前も変更します。データモデルで @SerializedName アノテーションを使用しない場合、フィールド名が JSON キーと一致しなくなるため、Gson は JSON の逆シリアル化に失敗します。
ただし、2.11 より前のバージョンの Gson を使用している場合や、モデルで @SerializedName アノテーションを使用していない場合は、それらのモデルに明示的な保持ルールを追加する必要があります。
デフォルトのコンストラクタを保持する
R8 フルモードでは、クラス自体が保持されていても、引数なしのデフォルト コンストラクタは暗黙的に保持されません。class.getDeclaredConstructor().newInstance() または class.newInstance() を使用してクラスのインスタンスを作成する場合は、フルモードで引数なしのコンストラクタを明示的に保持する必要があります。一方、互換性モードでは、引数なしのコンストラクタが常に保持されます。
リフレクションを使用して PrecacheTask のインスタンスを作成し、その run メソッドを動的に呼び出す例を考えてみましょう。このシナリオでは、互換モードで追加のルールは必要ありませんが、フルモードでは PrecacheTask のデフォルトのコンストラクタが削除されます。そのため、特定の保持ルールが必要です。
// 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 ルールを確認してください。