R8 का फ़ुल मोड में इस्तेमाल करना

R8, कंपैटबिलिटी मोड और फ़ुल मोड, दो मोड में काम करता है. फ़ुल मोड में, आपको ऑप्टिमाइज़ेशन की ऐसी सुविधाएं मिलती हैं जिनसे आपके ऐप्लिकेशन की परफ़ॉर्मेंस बेहतर होती है.

यह गाइड, Android के उन डेवलपर के लिए है जो R8 के सबसे बेहतर ऑप्टिमाइज़ेशन का इस्तेमाल करना चाहते हैं. इसमें कंपैटबिलिटी मोड और फ़ुल मोड के बीच के मुख्य अंतरों के बारे में बताया गया है. साथ ही, इसमें आपके प्रोजेक्ट को सुरक्षित तरीके से माइग्रेट करने और रनटाइम में होने वाली सामान्य गड़बड़ियों से बचने के लिए, ज़रूरी कॉन्फ़िगरेशन के बारे में भी बताया गया है.

फ़ुल मोड चालू करना

फ़ुल मोड चालू करने के लिए, अपनी 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 चालू होने पर, यह क्लास के लिए Signature एट्रिब्यूट को सुरक्षित रखता है. इसमें एनॉनिमस इनर क्लास भी शामिल हैं. जैसे, $GsonRemoteJsonListExample$listType$1. भले ही, साफ़ तौर पर कीप रूल तय न किए गए हों. इसलिए, R8 कंपैटबिलिटी मोड में, इस उदाहरण को उम्मीद के मुताबिक काम करने के लिए, साफ़ तौर पर किसी अन्य कीप रूल की ज़रूरत नहीं होती.

// keep rule for compatibility mode
-keepattributes Signature

फ़ुल मोड में R8 चालू होने पर, एनॉनिमस इनर क्लास $GsonRemoteJsonListExample$listType$1 का Signature एट्रिब्यूट हटा दिया जाता है. Signature में टाइप की यह जानकारी न होने पर, Gson, ऐप्लिकेशन का सही टाइप नहीं ढूंढ पाता. इससे IllegalStateException गड़बड़ी होती है.

अगर Gson के 2.11.0 से पुराने वर्शन का इस्तेमाल किया जा रहा है, तो इससे बचने के लिए ज़रूरी कीप रूल ये हैं:

// 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: यह नियम, R8 को उस एट्रिब्यूट को सुरक्षित रखने का निर्देश देता है जिसे Gson को पढ़ना होता है. फ़ुल मोड में, R8 सिर्फ़ उन क्लास, फ़ील्ड या तरीकों के लिए Signature एट्रिब्यूट को सुरक्षित रखता है जो keep नियम से साफ़ तौर पर मैच होते हैं.

  • -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: यह नियम, एनॉनिमस क्लास के टाइप की जानकारी को सुरक्षित रखता है जो TypeToken को एक्सटेंड करती हैं. जैसे, इस उदाहरण में $GsonRemoteJsonListExample$listType$1. इस नियम के बिना, फ़ुल मोड में R8, ज़रूरी टाइप की जानकारी हटा देता है. इससे डीसीरियलाइज़ेशन नहीं हो पाता.

यह समझना ज़रूरी है कि पहले शेयर किए गए नियम, सिर्फ़ जेनरिक टाइप (उदाहरण के लिए, List<User>) को ढूंढने की समस्या को हल करते हैं. R8, क्लास के फ़ील्ड के नाम भी बदलता है. अगर अपने डेटा मॉडल पर @SerializedName एनोटेशन का इस्तेमाल नहीं किया जाता है, तो Gson, JSON को डीसीरियलाइज़ नहीं कर पाएगा. ऐसा इसलिए, क्योंकि फ़ील्ड के नाम अब JSON की के से मैच नहीं करेंगे.

हालांकि, अगर Gson के 2.11 से पुराने वर्शन का इस्तेमाल किया जा रहा है या आपके मॉडल में @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 आपके तरीकों और फ़ील्ड की विज़िबिलिटी में बदलाव करके, ऑप्टिमाइज़ेशन को बेहतर बनाता है. उदाहरण के लिए, प्राइवेट से पब्लिक. इससे ज़्यादा इनलाइनिंग की जा सकती है.

अगर आपके कोड में रिफ़्लेक्शन का इस्तेमाल किया जाता है और वह खास तौर पर उन सदस्यों पर निर्भर करता है जिनकी विज़िबिलिटी खास होती है, तो इस ऑप्टिमाइज़ेशन से समस्याएं आ सकती हैं. R8, इस तरह के इनडायरेक्ट इस्तेमाल को नहीं पहचान पाएगा. इससे ऐप्लिकेशन क्रैश हो सकते हैं. इससे बचने के लिए, आपको सदस्यों को सुरक्षित रखने के लिए, -keep के खास नियम जोड़ने होंगे. इससे उनकी ओरिजनल विज़िबिलिटी भी सुरक्षित रहेगी.

ज़्यादा जानकारी के लिए, यह उदाहरण देखें. इससे आपको यह समझने में मदद मिलेगी कि रिफ़्लेक्शन का इस्तेमाल करके, प्राइवेट सदस्यों को ऐक्सेस करने की सलाह क्यों नहीं दी जाती. साथ ही, उन फ़ील्ड/तरीकों को सुरक्षित रखने के लिए, कीप रूल भी देखें.

Kotlin के लिए खास मेटाडेटा

Kotlin कोड को कंपाइल करते समय, Kotlin कंपाइलर, भाषा के हिसाब से खास मेटाडेटा (जैसे, शून्यता, एक्सटेंशन फ़ंक्शन, और कोरूटीन सिग्नेचर) को हर क्लास फ़ाइल पर @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 { *; }