Contoh berikut didasarkan pada skenario umum saat Anda menggunakan R8 untuk pengoptimalan, tetapi memerlukan panduan lanjutan untuk menyusun aturan keep.
Refleksi
Secara umum, untuk performa yang optimal, sebaiknya jangan gunakan refleksi. Namun, dalam skenario tertentu, hal ini mungkin tidak dapat dihindari. Contoh berikut memberikan panduan untuk aturan penyimpanan dalam skenario umum yang menggunakan refleksi.
Refleksi dengan class yang dimuat berdasarkan nama
Library sering memuat class secara dinamis dengan menggunakan nama class sebagai String
.
Namun, R8 tidak dapat mendeteksi class yang dimuat dengan cara ini, dan mungkin menghapus class yang dianggap tidak digunakan.
Misalnya, pertimbangkan skenario berikut di mana Anda memiliki library dan aplikasi yang menggunakan library tersebut. Kode ini menunjukkan pemuat library yang membuat instance antarmuka StartupTask
yang diimplementasikan oleh aplikasi.
Kode library-nya adalah sebagai berikut:
// The interface for a task that runs once.
interface StartupTask {
fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
fun execute(className: String) {
// R8 won't retain classes specified by this string value at runtime
val taskClass = Class.forName(className)
val task = taskClass.getDeclaredConstructor().newInstance() as StartupTask
task.run()
}
}
Aplikasi yang menggunakan library memiliki kode berikut:
// The app's task to pre-cache data.
// R8 will remove this class because it's only referenced by a string.
class PreCacheTask : StartupTask {
override fun run() {
// This log will never appear if the class is removed by R8.
Log.d("AppTask", "Warming up the cache...")
}
}
fun onCreate() {
// The library is told to run the app's task by its name.
TaskRunner.execute("com.example.app.PreCacheTask")
}
Dalam skenario ini, library Anda harus menyertakan file aturan penyimpanan konsumen dengan aturan penyimpanan berikut:
-keep class * implements com.example.library.StartupTask {
<init>();
}
Tanpa aturan ini, R8 akan menghapus PreCacheTask
dari aplikasi karena aplikasi tidak menggunakan class secara langsung, sehingga merusak integrasi. Aturan ini menemukan
class yang mengimplementasikan antarmuka StartupTask
library Anda dan mempertahankan
class tersebut, beserta konstruktor tanpa argumennya, sehingga library dapat
berhasil membuat instance dan mengeksekusi PreCacheTask
.
Refleksi dengan ::class.java
Library dapat memuat class dengan membuat aplikasi meneruskan objek Class
secara langsung,
yang merupakan metode yang lebih andal daripada memuat class berdasarkan nama. Tindakan ini akan membuat
referensi kuat ke class yang dapat dideteksi R8. Namun, meskipun hal ini mencegah
R8 menghapus class, Anda tetap harus menggunakan aturan keep untuk menyatakan bahwa
class di-instance secara reflektif dan untuk melindungi anggota yang
diakses secara reflektif, seperti konstruktor.
Misalnya, pertimbangkan skenario berikut saat Anda memiliki library dan aplikasi yang menggunakan library- loader library membuat instance antarmuka StartupTask
dengan meneruskan referensi class secara langsung.
Kode library-nya adalah sebagai berikut:
// The interface for a task that runs once.
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()
}
}
Aplikasi yang menggunakan library memiliki kode berikut:
// The app's task is to pre-cache data.
class PreCacheTask : StartupTask {
override fun run() {
Log.d("AppTask", "Warming up the cache...")
}
}
fun onCreate() {
// The library is given a direct reference to the app's task class.
TaskRunner.execute(PreCacheTask::class.java)
}
Dalam skenario ini, library Anda harus menyertakan file aturan penyimpanan konsumen dengan aturan penyimpanan berikut:
# Allow any implementation of StartupTask to be removed if unused.
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask
# Keep the default constructor, which is called via reflection.
-keepclassmembers class * implements com.example.library.StartupTask {
<init>();
}
Aturan ini dirancang agar berfungsi sempurna dengan jenis refleksi ini, sehingga memungkinkan pengoptimalan maksimum sekaligus memastikan kode berfungsi dengan benar. Aturan ini memungkinkan R8 meng-obfuscate nama class dan menyusutkan, atau menghapus, implementasi class StartupTask
jika aplikasi tidak pernah menggunakannya. Namun,
untuk setiap implementasi, seperti PrecacheTask
yang digunakan dalam contoh,
implementasi tersebut mempertahankan konstruktor default (<init>()
) yang perlu dipanggil
oleh library Anda.
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask
: Aturan ini menargetkan class apa pun yang menerapkan antarmukaStartupTask
Anda.-keep class * implements com.example.library.StartupTask
: Hal ini mempertahankan class (*
) yang mengimplementasikan antarmuka Anda.,allowobfuscation
: Ini menginstruksikan R8 bahwa meskipun mempertahankan class, R8 dapat mengganti nama, atau meng-obfuscate, class tersebut. Hal ini aman karena pustaka Anda tidak bergantung pada nama class; pustaka mendapatkan objekClass
secara langsung.,allowshrinking
: Pengubah ini menginstruksikan R8 bahwa R8 dapat menghapus class jika tidak digunakan. Hal ini membantu R8 menghapus implementasiStartupTask
yang tidak pernah diteruskan keTaskRunner.execute()
dengan aman. Singkatnya, aturan ini menyiratkan hal berikut: Jika aplikasi menggunakan class yang mengimplementasikanStartupTask
, R8 akan mempertahankan class tersebut. R8 dapat mengganti nama class untuk mengurangi ukurannya dan dapat menghapusnya jika aplikasi tidak menggunakannya.
-keepclassmembers class * implements com.example.library.StartupTask { <init>(); }
: Aturan ini menargetkan anggota tertentu dari class yang diidentifikasi dalam aturan pertama—dalam hal ini, konstruktor.-keepclassmembers class * implements com.example.library.StartupTask
: Mempertahankan anggota tertentu (metode, kolom) class yang mengimplementasikan antarmukaStartupTask
, tetapi hanya jika class yang diimplementasikan itu sendiri dipertahankan.{ <init>(); }
: Ini adalah pemilih anggota.<init>
adalah nama internal khusus untuk konstruktor dalam bytecode Java. Bagian ini secara khusus menargetkan konstruktor default tanpa argumen.- Aturan ini sangat penting karena kode Anda memanggil
getDeclaredConstructor().newInstance()
tanpa argumen apa pun, yang secara reflektif memanggil konstruktor default. Tanpa aturan ini, R8 melihat bahwa tidak ada kode yang memanggilnew PreCacheTask()
secara langsung, mengasumsikan bahwa konstruktor tidak digunakan, dan menghapusnya. Hal ini menyebabkan aplikasi Anda error saat runtime denganInstantiationException
.
Refleksi berdasarkan anotasi metode
Library sering kali menentukan anotasi yang digunakan developer untuk memberi tag pada metode atau kolom.
Kemudian, library menggunakan refleksi untuk menemukan anggota yang diberi anotasi ini saat runtime. Misalnya, anotasi @OnLifecycleEvent
digunakan untuk menemukan metode yang diperlukan saat runtime.
Misalnya, pertimbangkan skenario berikut saat Anda memiliki library dan aplikasi yang menggunakan library. Contoh ini menunjukkan bus peristiwa yang menemukan dan memanggil metode yang dianotasi dengan @OnEvent
.
Kode library-nya adalah sebagai berikut:
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class OnEvent
class EventBus {
fun dispatch(listener: Any) {
// Find all methods annotated with @OnEvent and invoke them
listener::class.java.declaredMethods.forEach { method ->
if (method.isAnnotationPresent(OnEvent::class.java)) {
try {
method.invoke(listener)
} catch (e: Exception) { /* ... */ }
}
}
}
}
Aplikasi yang menggunakan library memiliki kode berikut:
class MyEventListener {
@OnEvent
fun onSomethingHappened() {
// This method will be removed by R8 without a keep rule
Log.d(TAG, "Event received!")
}
}
fun onCreate() {
// Instantiate the listener and the event bus
val listener = MyEventListener()
val eventBus = EventBus()
// Dispatch the listener to the event bus
eventBus.dispatch(listener)
}
Library harus menyertakan file aturan keep konsumen yang secara otomatis mempertahankan metode apa pun yang menggunakan anotasinya:
-keepattributes RuntimeVisibleAnnotations
-keep @interface com.example.library.OnEvent;
-keepclassmembers class * {
@com.example.library.OnEvent <methods>;
}
-keepattributes RuntimeVisibleAnnotations
: Aturan ini mempertahankan anotasi yang dimaksudkan untuk dibaca saat runtime.-keep @interface com.example.library.OnEvent
: Aturan ini mempertahankan kelas anotasiOnEvent
itu sendiri.-keepclassmembers class * {@com.example.library.OnEvent <methods>;}
: Aturan ini mempertahankan class dan anggota tertentu hanya jika class sedang digunakan dan class berisi anggota tersebut.-keepclassmembers
: Aturan ini mempertahankan class dan anggota tertentu hanya jika class sedang digunakan dan class berisi anggota tersebut.class *
: Aturan berlaku untuk class apa pun.@com.example.library.OnEvent <methods>;
: Ini mempertahankan class apa pun yang memiliki satu atau beberapa metode (<methods>
) yang dianotasikan dengan@com.example.library.OnEvent
, dan juga mempertahankan metode yang dianotasikan itu sendiri.
Refleksi berdasarkan anotasi kelas
Library dapat menggunakan refleksi untuk memindai class yang memiliki anotasi
tertentu. Dalam hal ini, class pelaksana tugas menemukan semua class yang dianotasi dengan ReflectiveExecutor
menggunakan refleksi dan menjalankan metode execute
.
Misalnya, pertimbangkan skenario berikut saat Anda memiliki library dan aplikasi yang menggunakan library tersebut.
Library memiliki kode berikut:
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class ReflectiveExecutor
class TaskRunner {
fun process(task: Any) {
val taskClass = task::class.java
if (taskClass.isAnnotationPresent(ReflectiveExecutor::class.java)) {
val methodToCall = taskClass.getMethod("execute")
methodToCall.invoke(task)
}
}
}
Aplikasi yang menggunakan library memiliki kode berikut:
// In consumer app
@ReflectiveExecutor
class ImportantBackgroundTask {
fun execute() {
// This class will be removed by R8 without a keep rule
Log.e("ImportantBackgroundTask", "Executing the important background task...")
}
}
// Usage of ImportantBackgroundTask
fun onCreate(){
val task = ImportantBackgroundTask()
val runner = TaskRunner()
runner.process(task)
}
Karena library menggunakan refleksi secara reflektif untuk mendapatkan class tertentu, library harus menyertakan file aturan keep konsumen dengan aturan keep berikut:
# Retain annotation metadata for runtime reflection.
-keepattributes RuntimeVisibleAnnotations
# Keep the annotation interface itself.
-keep @interface com.example.library.ReflectiveExecutor
# Keep the execute method in the classes which are being used
-keepclassmembers @com.example.library.ReflectiveExecutor class * {
public void execute();
}
Konfigurasi ini sangat efisien karena memberi tahu R8 secara persis apa yang harus dipertahankan.
Refleksi untuk mendukung dependensi opsional
Kasus penggunaan umum untuk refleksi adalah membuat dependensi ringan antara library inti dan library add-on opsional. Library inti dapat memeriksa apakah add-on disertakan dalam aplikasi dan, jika disertakan, dapat mengaktifkan fitur tambahan. Hal ini memungkinkan Anda mengirimkan modul add-on tanpa memaksa library inti memiliki dependensi langsung padanya.
Library inti menggunakan refleksi (Class.forName
) untuk mencari class tertentu
berdasarkan namanya. Jika kelas ditemukan, fitur akan diaktifkan. Jika tidak, proses akan gagal
dengan baik.
Misalnya, pertimbangkan kode berikut yang menggunakan AnalyticsManager
inti untuk memeriksa
class VideoEventTracker
opsional guna mengaktifkan analisis video.
Library inti memiliki kode berikut:
object AnalyticsManager {
private const val VIDEO_TRACKER_CLASS = "com.example.analytics.video.VideoEventTracker"
fun initialize() {
try {
// Attempt to load the optional module's class using reflection
Class.forName(VIDEO_TRACKER_CLASS).getDeclaredConstructor().newInstance()
Log.d(TAG, "Video tracking enabled.")
} catch (e: ClassNotFoundException) {
Log.d(TAG,"Video tracking module not found. Skipping.")
} catch (e: Exception) {
Log.e(TAG, e.printStackTrace())
}
}
}
Library video opsional memiliki kode berikut:
package com.example.analytics.video
class VideoEventTracker {
// This constructor must be kept for the reflection call to succeed.
init { /* ... */ }
}
Developer library opsional bertanggung jawab untuk memberikan aturan penyimpanan konsumen yang diperlukan. Aturan keep ini memastikan bahwa aplikasi apa pun yang menggunakan library opsional mempertahankan kode yang dibutuhkan library inti untuk ditemukan.
# In the video library's consumer keep rules file
-keep class com.example.analytics.video.VideoEventTracker {
<init>();
}
Tanpa aturan ini, R8 kemungkinan akan menghapus VideoEventTracker
dari library opsional karena tidak ada yang menggunakannya secara langsung dalam modul tersebut. Aturan keep mempertahankan
class dan konstruktornya, sehingga library inti dapat berhasil
menginstansiasinya.
Refleksi untuk mengakses anggota pribadi
Menggunakan refleksi untuk mengakses kode pribadi atau terlindungi yang bukan bagian dari API publik library dapat menimbulkan masalah yang signifikan. Kode tersebut dapat berubah tanpa pemberitahuan, yang dapat menyebabkan perilaku yang tidak terduga atau error dalam aplikasi Anda.
Saat mengandalkan refleksi untuk API non-publik, Anda mungkin mengalami masalah berikut:
- Update yang diblokir: Perubahan pada kode pribadi atau terlindungi dapat mencegah Anda mengupdate ke versi pustaka yang lebih tinggi.
- Manfaat yang terlewat: Anda mungkin melewatkan fungsi baru, perbaikan error penting, atau update keamanan penting.
Pengoptimalan dan refleksi R8
Jika Anda harus merefleksikan kode pribadi atau terlindungi library, perhatikan
pengoptimalan R8. Jika tidak ada referensi langsung ke anggota ini, R8 mungkin menganggapnya tidak digunakan dan kemudian menghapus atau mengganti namanya.
Hal ini dapat menyebabkan error runtime, sering kali dengan pesan error yang menyesatkan seperti
NoSuchMethodException
atau NoSuchFieldException
.
Misalnya, pertimbangkan skenario berikut yang menunjukkan cara Anda dapat mengakses kolom pribadi dari class library.
Library yang bukan milik Anda memiliki kode berikut:
class LibraryClass {
private val secretMessage = "R8 will remove me"
}
Aplikasi Anda memiliki kode berikut:
fun accessSecretMessage(instance: LibraryClass) {
// Use Java reflection from Kotlin to access the private field
val secretField = instance::class.java.getDeclaredField("secretMessage")
secretField.isAccessible = true
// This will crash at runtime with R8 enabled
val message = secretField.get(instance) as String
}
Tambahkan aturan -keep
di aplikasi Anda untuk mencegah R8 menghapus kolom pribadi:
-keepclassmembers class com.example.LibraryClass {
private java.lang.String secretMessage;
}
-keepclassmembers
: Ini mempertahankan anggota tertentu dari class hanya jika class itu sendiri dipertahankan.class com.example.LibraryClass
: Ini menargetkan class persis yang berisi kolom.private java.lang.String secretMessage;
: Mengidentifikasi kolom pribadi tertentu berdasarkan nama dan jenisnya.
Java Native Interface (JNI)
Pengoptimalan R8 dapat menimbulkan masalah saat menangani upcall dari native (kode C/C++) ke Java atau Kotlin. Meskipun hal sebaliknya juga benar—panggilan ke bawah dari Java atau
Kotlin ke kode native dapat menimbulkan masalah—file default
proguard-android-optimize.txt
menyertakan aturan berikut agar
panggilan ke bawah tetap berfungsi. Aturan ini melindungi agar metode native tidak di-trim.
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
Interaksi dengan kode native melalui Java Native Interface (JNI)
Saat aplikasi Anda menggunakan JNI untuk melakukan upcall dari kode native (C/C++) ke Java atau Kotlin, R8 tidak dapat melihat metode mana yang dipanggil dari kode native Anda. Jika tidak ada referensi langsung ke metode ini di aplikasi Anda, R8 akan salah mengasumsikan bahwa metode ini tidak digunakan dan menghapusnya, sehingga menyebabkan aplikasi Anda error.
Contoh berikut menunjukkan class Kotlin dengan metode yang dimaksudkan untuk dipanggil dari library native. Library native membuat instance jenis aplikasi dan meneruskan data dari kode native ke kode Kotlin.
package com.example.models
// This class is used in the JNI bridge method signature
data class NativeData(val id: Int, val payload: String)
package com.example.app
// In package com.example.app
class JniBridge {
/**
* This method is called from the native side.
* R8 will remove it if it's not kept.
*/
fun onNativeEvent(data: NativeData) {
Log.d(TAG, "Received event from native code: $data")
}
// Use 'external' to declare a native method
external fun startNativeProcess()
companion object {
init {
// Load the native library
System.loadLibrary("my-native-lib")
}
}
}
Dalam hal ini, Anda harus memberi tahu R8 untuk mencegah jenis aplikasi dioptimalkan. Selain itu, jika metode yang dipanggil dari kode native menggunakan class Anda sendiri dalam tanda tangannya sebagai parameter atau jenis yang ditampilkan, Anda juga harus memverifikasi bahwa class tersebut tidak diganti namanya.
Tambahkan aturan penyimpanan berikut ke aplikasi Anda:
-keepclassmembers,includedescriptorclasses class com.example.JniBridge {
public void onNativeEvent(com.example.model.NativeData);
}
-keep class NativeData{
<init>(java.lang.Integer, java.lang.String);
}
Aturan keep ini mencegah R8 menghapus atau mengganti nama metode onNativeEvent
dan—yang paling penting—jenis parameternya.
-keepclassmembers,includedescriptorclasses class com.example.JniBridge{ public void onNativeEvent(com.example.model.NativeData);}
: Ini mempertahankan anggota tertentu dari suatu class hanya jika class di-instance dalam kode Kotlin atau Java terlebih dahulu—ini memberi tahu R8 bahwa aplikasi menggunakan class dan bahwa R8 harus mempertahankan anggota tertentu dari class tersebut.-keepclassmembers
: Ini hanya mempertahankan anggota tertentu dari suatu class jika class di-instance terlebih dahulu dalam kode Kotlin atau Java—ini memberi tahu R8 bahwa aplikasi menggunakan class tersebut dan harus mempertahankan anggota tertentu dari class tersebut.class com.example.JniBridge
: Ini menargetkan class persis yang berisi kolom.includedescriptorclasses
: Pengubah ini juga mempertahankan class apa pun yang ditemukan dalam tanda tangan atau deskriptor metode. Dalam hal ini, R8 tidak akan mengganti nama atau menghapus classcom.example.models.NativeData
, yang digunakan sebagai parameter. JikaNativeData
diganti namanya (misalnya, menjadia.a
), tanda tangan metode tidak akan lagi cocok dengan yang diharapkan kode native, sehingga menyebabkan error.public void onNativeEvent(com.example.models.NativeData);
: Ini menentukan tanda tangan Java yang tepat dari metode yang akan dipertahankan.
-keep class NativeData{<init>(java.lang.Integer, java.lang.String);}
: Meskipunincludedescriptorclasses
memastikan bahwa classNativeData
itu sendiri dipertahankan, setiap anggota (kolom atau metode) dalamNativeData
yang diakses langsung dari kode JNI native Anda memerlukan aturan keep sendiri.-keep class NativeData
: Ini menargetkan class bernamaNativeData
dan blok menentukan anggota di dalam classNativeData
yang harus dipertahankan.<init>(java.lang.Integer, java.lang.String)
: Ini adalah tanda tangan konstruktor. Secara unik mengidentifikasi konstruktor yang menggunakan dua parameter: yang pertama adalahInteger
dan yang kedua adalahString
.
Panggilan platform tidak langsung
Mentransfer data dengan implementasi Parcelable
Framework Android menggunakan refleksi untuk membuat instance objek Parcelable
Anda. Dalam pengembangan Kotlin modern, Anda harus menggunakan plugin kotlin-parcelize
, yang otomatis membuat implementasi Parcelable
yang diperlukan, termasuk kolom dan metode CREATOR
yang dibutuhkan framework.
Misalnya, perhatikan contoh berikut saat plugin kotlin-parcelize
digunakan untuk membuat class Parcelable
:
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
// Add the @Parcelize annotation to your data class
@Parcelize
data class UserData(
val name: String,
val age: Int
) : Parcelable
Dalam skenario ini, tidak ada aturan penyimpanan yang direkomendasikan. Plugin Gradle kotlin-parcelize
secara otomatis membuat aturan keep yang diperlukan untuk class yang
Anda anotasi dengan @Parcelize
. Ini menangani kompleksitas untuk Anda, memastikan bahwa CREATOR
dan konstruktor yang dihasilkan dipertahankan untuk panggilan refleksi framework Android.
Jika Anda menulis class Parcelable
secara manual di Kotlin tanpa menggunakan @Parcelize
,
Anda bertanggung jawab untuk menyimpan kolom CREATOR
dan konstruktor yang
menerima Parcel
. Jika Anda lupa melakukannya, aplikasi Anda akan error saat
sistem mencoba mendeserialisasi objek Anda. Menggunakan @Parcelize
adalah praktik standar yang lebih aman.
Saat menggunakan plugin kotlin-parcelize
, perhatikan hal-hal berikut:
- Plugin ini otomatis membuat kolom
CREATOR
selama kompilasi. - File
proguard-android-optimize.txt
berisi aturankeep
yang diperlukan untuk mempertahankan kolom ini agar berfungsi dengan baik. - Developer aplikasi harus memverifikasi bahwa semua aturan
keep
yang diperlukan ada, terutama untuk penerapan kustom atau dependensi pihak ketiga.