Mengaktifkan potensi penuh pengoptimal R8

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

Panduan ini ditujukan bagi developer Android yang ingin menggunakan pengoptimalan R8 yang paling canggih. Dokumen 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 kelas yang terkait dengan atribut

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

Kode berikut menunjukkan seperti apa 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 mengandalkan atribut ini untuk memeriksa dan memahami struktur kode Anda secara dinamis. Secara default dalam mode penuh R8, atribut dipertahankan hanya 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.

Pertimbangkan contoh berikut saat kita melakukan deserialisasi 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> muncul sebagai List mentah. Oleh karena itu, library seperti Gson, yang mengandalkan refleksi, tidak dapat menentukan jenis objek tertentu yang dideklarasikan List untuk dimuat saat mendeserialisasi daftar JSON, yang dapat menyebabkan masalah runtime.

Untuk mempertahankan informasi jenis, Gson menggunakan TypeToken. Pembungkusan TypeToken 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. Dengan demikian, Gson dapat menggunakan refleksi untuk membaca Signature dan berhasil menemukan jenis List<User> lengkap yang diperlukan untuk deserialisasi.

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

// keep rule for compatibility mode
-keepattributes Signature

Jika 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. Aturan penyimpanan 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 dicocokkan oleh aturan keep.

  • -keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken: Aturan ini diperlukan karena TypeToken membungkus jenis objek yang sedang dideserialisasi. Setelah penghapusan jenis, class dalam anonim 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.

Mulai dari Gson versi 2.11.0, library menggabungkan aturan keep yang diperlukan untuk deserialisasi dalam mode penuh. Saat Anda membuat aplikasi dengan R8 diaktifkan, R8 akan otomatis menemukan dan menerapkan aturan ini dari library. Hal ini memberikan perlindungan yang dibutuhkan aplikasi Anda tanpa mengharuskan Anda menambahkan atau memelihara aturan tertentu ini secara manual di project Anda.

Penting untuk memahami bahwa aturan yang dibagikan sebelumnya hanya memecahkan 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 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, bahkan saat class itu sendiri dipertahankan. Jika Anda membuat instance class menggunakan class.getDeclaredConstructor().newInstance() atau class.newInstance(), Anda harus secara eksplisit mempertahankan konstruktor tanpa argumen dalam mode penuh. Sebaliknya, mode kompatibilitas selalu mempertahankan konstruktor tanpa argumen.

Pertimbangkan contoh saat instance PrecacheTask dibuat menggunakan refleksi untuk memanggil metode run secara dinamis. Meskipun skenario ini tidak memerlukan aturan tambahan dalam mode kompatibilitas, dalam mode penuh, konstruktor default PrecacheTask akan dihapus. Oleh karena itu, aturan penyimpanan 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 penyisipan inline.

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 error pada aplikasi. Untuk mencegahnya, 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 anggota pribadi menggunakan refleksi tidak disarankan dan aturan keep untuk mempertahankan kolom/metode tersebut.