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 होता है. इस समस्या को रोकने के लिए, ये नियम लागू करना ज़रूरी है:

// 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 फ़ुल मोड में ज़रूरी टाइप की जानकारी हटा देता है. इससे डिसिरियलाइज़ेशन पूरा नहीं हो पाता.

Gson के 2.11.0 वर्शन से, लाइब्रेरी ज़रूरी keep rules बंडल करती है, जो फ़ुल मोड में डिसिरियलाइज़ेशन के लिए ज़रूरी होते हैं. R8 की सुविधा चालू करके ऐप्लिकेशन बनाने पर, 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 के कुछ नियम जोड़ने होंगे. इससे सदस्यों की ओरिजनल विज़िबिलिटी भी बनी रहेगी.

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