Menggunakan R8 dalam mode penuh

R8 menyediakan dua mode, yaitu mode kompatibilitas dan mode penuh. Mode penuh memberi Anda pengoptimalan yang canggih dan meningkatkan performa aplikasi.

Panduan ini ditujukan untuk developer Android yang ingin menggunakan pengoptimalan R8 yang paling canggih. Panduan ini membahas perbedaan utama antara mode kompatibilitas dan mode penuh serta memberikan konfigurasi eksplisit yang diperlukan untuk memigrasikan project Anda dengan aman dan menghindari error runtime umum.

Mengaktifkan mode penuh

Untuk mengaktifkan mode penuh, hapus baris berikut dari file gradle.properties Anda:

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

Mempertahankan class yang terkait dengan atribut

Atribut adalah metadata yang disimpan dalam file class yang dikompilasi dan bukan bagian dari kode yang dapat dieksekusi. Namun, atribut dapat diperlukan untuk jenis refleksi tertentu. Contoh umumnya mencakup Signature (yang mempertahankan informasi jenis generik setelah penghapusan jenis), InnerClasses dan EnclosingMethod (untuk merefleksikan struktur class), dan anotasi yang terlihat saat runtime.

Kode berikut menunjukkan tampilan atribut Signature untuk kolom dalam bytecode. Untuk kolom:

List<User> users;

File class yang dikompilasi akan berisi bytecode berikut:

.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

Library yang banyak menggunakan refleksi (seperti Gson) sering kali mengandalkan atribut ini untuk memeriksa dan memahami struktur kode Anda secara dinamis. Secara default dalam mode penuh R8, atribut hanya dipertahankan jika class, kolom, atau metode terkait dipertahankan secara eksplisit.

Contoh berikut menunjukkan alasan atribut diperlukan dan aturan keep yang perlu Anda tambahkan saat bermigrasi dari mode kompatibilitas ke mode penuh. Selain mempertahankan class, kolom, atau metode yang direfleksikan, Anda juga harus mempertahankan atribut yang diandalkannya secara eksplisit.

Pertimbangkan contoh berikut saat kita mendeserialisasi daftar pengguna menggunakan library 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}")
}

Selama kompilasi, penghapusan jenis Java akan menghapus argumen jenis generik. Artinya saat runtime, List<String> dan List<User> akan muncul sebagai List mentah. Oleh karena itu, library seperti Gson, yang bergantung pada refleksi, tidak dapat menentukan jenis objek tertentu yang dideklarasikan untuk dimuat oleh List saat mendeserialisasi daftar JSON, yang dapat menyebabkan masalah runtime.

Untuk mempertahankan informasi jenis, Gson menggunakan TypeToken. Menggabungkan TypeToken akan mempertahankan informasi deserialisasi yang diperlukan.

Ekspresi Kotlin object:TypeToken<List<User>>() {}.type membuat class dalam anonim yang memperluas TypeToken dan mengambil informasi jenis generik. Dalam contoh ini, class anonim diberi nama $GsonRemoteJsonListExample$listType$1.

Bahasa pemrograman Java menyimpan tanda tangan generik superclass sebagai metadata, yang dikenal sebagai atribut Signature, dalam file class yang dikompilasi. TypeToken kemudian menggunakan metadata Signature ini untuk memulihkan jenis saat runtime. Hal ini memungkinkan Gson menggunakan refleksi untuk membaca Signature dan berhasil menemukan jenis List<User> lengkap yang diperlukan untuk deserialisasi.

Saat diaktifkan dalam mode kompatibilitas, R8 akan mempertahankan atribut Signature untuk class, termasuk class dalam anonim seperti $GsonRemoteJsonListExample$listType$1, meskipun aturan keep tertentu tidak ditentukan secara eksplisit. Oleh karena itu, mode kompatibilitas R8 tidak memerlukan aturan keep eksplisit lebih lanjut agar contoh ini berfungsi seperti yang diharapkan.

// keep rule for compatibility mode
-keepattributes Signature

Saat R8 diaktifkan dalam mode penuh, atribut Signature dari class dalam anonim $GsonRemoteJsonListExample$listType$1 akan dihapus. Tanpa informasi jenis ini di Signature, Gson tidak dapat menemukan jenis aplikasi yang benar, yang akan menghasilkan IllegalStateException.

Jika Anda menggunakan versi Gson yang lebih lama dari 2.11.0, aturan keep yang diperlukan untuk mencegah hal ini adalah:

// 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: Aturan ini menginstruksikan R8 untuk mempertahankan atribut yang perlu dibaca Gson. Dalam mode penuh, R8 hanya mempertahankan atribut Signature untuk class, kolom, atau metode yang secara eksplisit cocok dengan aturan keep.

  • -keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken: Aturan ini diperlukan karena TypeToken menggabungkan jenis objek yang sedang dideserialisasi. Setelah penghapusan jenis, class dalam anonim akan dibuat untuk mempertahankan informasi jenis generik. Tanpa mempertahankan com.google.gson.reflect.TypeToken secara eksplisit, R8 dalam mode penuh tidak akan menyertakan jenis class ini dalam atribut Signature yang diperlukan untuk deserialisasi.

  • -keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken: Aturan ini mempertahankan informasi jenis class anonim yang memperluas TypeToken, seperti $GsonRemoteJsonListExample$listType$1 dalam contoh ini. Tanpa aturan ini, R8 dalam mode penuh akan menghapus informasi jenis yang diperlukan, sehingga menyebabkan deserialisasi gagal.

Penting untuk memahami bahwa aturan yang dibagikan sebelumnya hanya menyelesaikan masalah penemuan jenis generik (misalnya, List<User>). R8 juga mengganti nama kolom class. Jika Anda tidak menggunakan anotasi @SerializedName pada model data, Gson akan gagal mendeserialisasi JSON karena nama kolom tidak akan lagi cocok dengan kunci JSON.

Namun, jika Anda menggunakan versi Gson yang lebih lama dari 2.11, atau jika model Anda tidak menggunakan anotasi @SerializedName, Anda harus menambahkan aturan keep eksplisit untuk model tersebut.

Mempertahankan konstruktor default

Dalam mode penuh R8, konstruktor tanpa argumen/default tidak dipertahankan secara implisit, meskipun class itu sendiri dipertahankan. Jika Anda membuat instance class menggunakan class.getDeclaredConstructor().newInstance() atau class.newInstance(), Anda harus mempertahankan konstruktor tanpa argumen secara eksplisit dalam mode penuh. Sebaliknya, mode kompatibilitas selalu mempertahankan konstruktor tanpa argumen.

Pertimbangkan contoh saat instance PrecacheTask dibuat menggunakan refleksi untuk memanggil metode run-nya secara dinamis. Meskipun skenario ini tidak memerlukan aturan tambahan dalam mode kompatibilitas, dalam mode penuh, konstruktor default PrecacheTask akan dihapus. Oleh karena itu, aturan keep tertentu diperlukan.

// 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>();
}

Modifikasi akses diaktifkan secara default

Dalam mode kompatibilitas, R8 tidak mengubah visibilitas metode dan kolom dalam class. Namun, dalam mode penuh, R8 meningkatkan pengoptimalan dengan mengubah visibilitas metode dan kolom Anda, misalnya, dari pribadi menjadi publik. Hal ini memungkinkan lebih banyak inlining.

Pengoptimalan ini dapat menyebabkan masalah jika kode Anda menggunakan refleksi yang secara khusus bergantung pada anggota yang memiliki visibilitas tertentu. R8 tidak akan mengenali penggunaan tidak langsung ini, yang berpotensi menyebabkan aplikasi error. Untuk mencegah hal ini, Anda harus menambahkan aturan -keep tertentu untuk mempertahankan anggota, yang juga akan mempertahankan visibilitas aslinya.

Untuk mengetahui informasi selengkapnya, lihat contoh ini untuk memahami alasan akses ke anggota pribadi menggunakan refleksi tidak disarankan dan aturan keep untuk mempertahankan kolom/metode tersebut.

Metadata khusus Kotlin

Saat mengompilasi kode Kotlin, compiler Kotlin menyimpan metadata khusus bahasa (seperti nullability, fungsi ekstensi, dan tanda tangan coroutine) dalam anotasi @kotlin.Metadata pada setiap file class.

Jika aplikasi Anda atau dependensinya menggunakan refleksi Kotlin (kotlin.reflect), library refleksi akan mengurai metadata ini saat runtime untuk memeriksa struktur class. Dalam mode penuh R8, R8 akan menghapus anotasi secara default jika tidak dipertahankan secara eksplisit. Selain itu, jika R8 meminimalkan atau mengurangi ukuran class Anda tanpa mempertahankan dan mengupdate metadata, refleksi Kotlin akan gagal saat runtime, yang menyebabkan perilaku atau error yang tidak terduga (seperti KotlinReflectionInternalError).

Untuk mencegah perilaku yang tidak terduga dan memastikan fungsi refleksi Kotlin berfungsi dengan benar setelah minimisasi, Anda harus mempertahankan anotasi yang terlihat saat runtime dan mempertahankan class kotlin.Metadata secara eksplisit:

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

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