नियम के इस्तेमाल के उदाहरण और उदाहरणों को सेव करना

यहां दिए गए उदाहरण, उन सामान्य स्थितियों पर आधारित हैं जिनमें ऑप्टिमाइज़ेशन के लिए R8 का इस्तेमाल किया जाता है. हालांकि, कीप रूल का ड्राफ़्ट तैयार करने के लिए, आपको ज़्यादा जानकारी की ज़रूरत होती है.

रिफ़्लेक्शन

आम तौर पर, बेहतर परफ़ॉर्मेंस के लिए, रिफ़्लेक्शन का इस्तेमाल करने का सुझाव नहीं दिया जाता. हालांकि, कुछ स्थितियों में इसका इस्तेमाल करना ज़रूरी हो सकता है. यहां दिए गए उदाहरणों में, रिफ़्लेक्शन का इस्तेमाल करने वाली सामान्य स्थितियों में कीप रूल के बारे में जानकारी दी गई है.

नाम से लोड की गई क्लास के साथ रिफ़्लेक्शन

लाइब्रेरी अक्सर, क्लास के नाम को String के तौर पर इस्तेमाल करके, क्लास को डाइनैमिक तरीके से लोड करती हैं. हालांकि, R8 इस तरीके से लोड की गई क्लास का पता नहीं लगा सकता. साथ ही, यह उन क्लास को हटा सकता है जिन्हें वह इस्तेमाल नहीं की गई मानता है.

उदाहरण के लिए, यहां दी गई स्थिति देखें. इसमें आपके पास एक लाइब्रेरी और एक ऐप्लिकेशन है, जो लाइब्रेरी का इस्तेमाल करता है. कोड में, लाइब्रेरी लोडर दिखाया गया है. यह लोडर, ऐप्लिकेशन में लागू किए गए StartupTask इंटरफ़ेस का इंस्टेंस बनाता है.

लाइब्रेरी का कोड यहां दिया गया है:

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

लाइब्रेरी का इस्तेमाल करने वाले ऐप्लिकेशन का कोड यहां दिया गया है:

// 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")
}

इस स्थिति में, आपकी लाइब्रेरी में, उपयोगकर्ताओं के लिए बनाए गए कीप रूल वाली फ़ाइल में ये कीप रूल शामिल होने चाहिए:

-keep class * implements com.example.library.StartupTask {
    <init>();
}

इस नियम के बिना, R8 ऐप्लिकेशन से PreCacheTask को हटा देता है, क्योंकि ऐप्लिकेशन सीधे तौर पर इस क्लास का इस्तेमाल नहीं करता. इससे इंटिग्रेशन टूट जाता है. यह नियम, आपकी लाइब्रेरी के StartupTask इंटरफ़ेस को लागू करने वाली क्लास को ढूंढता है और उन्हें सुरक्षित रखता है. साथ ही, उनके नो-आर्गुमेंट कंस्ट्रक्टर को भी सुरक्षित रखता है. इससे लाइब्रेरी, PreCacheTask का इंस्टेंस बना पाती है और उसे लागू कर पाती है.

::class.java के साथ रिफ़्लेक्शन

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

उदाहरण के लिए, यहां दी गई स्थिति देखें. इसमें आपके पास एक लाइब्रेरी और एक ऐप्लिकेशन है, जो लाइब्रेरी का इस्तेमाल करता है. लाइब्रेरी लोडर, क्लास रेफ़रंस को सीधे तौर पर पास करके, StartupTask इंटरफ़ेस का इंस्टेंस बनाता है.

लाइब्रेरी का कोड यहां दिया गया है:

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

लाइब्रेरी का इस्तेमाल करने वाले ऐप्लिकेशन का कोड यहां दिया गया है:

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

इस स्थिति में, आपकी लाइब्रेरी में, उपयोगकर्ताओं के लिए बनाए गए कीप रूल वाली फ़ाइल में ये कीप रूल शामिल होने चाहिए:

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

इन नियमों को इस तरह के रिफ़्लेक्शन के साथ सही तरीके से काम करने के लिए डिज़ाइन किया गया है. इससे ज़्यादा से ज़्यादा ऑप्टिमाइज़ेशन किया जा सकता है. साथ ही, यह पक्का किया जा सकता है कि कोड सही तरीके से काम करे. इन नियमों की मदद से, R8 क्लास के नाम को अस्पष्ट कर सकता है. साथ ही, StartupTask क्लास के लागू करने के तरीके को छोटा या हटाया जा सकता है. हालांकि, ऐसा तब किया जा सकता है, जब ऐप्लिकेशन इसका इस्तेमाल न करता हो. हालांकि, किसी भी लागू करने के तरीके के लिए, जैसे कि उदाहरण में इस्तेमाल किया गया PrecacheTask, ये डिफ़ॉल्ट कंस्ट्रक्टर (<init>()) को सुरक्षित रखते हैं. आपकी लाइब्रेरी को इसे कॉल करने की ज़रूरत होती है.

  • -keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask: यह नियम, इंटरफ़ेस को लागू करने वाली किसी भी क्लास को टारगेट करता है.StartupTask
    • -keep class * implements com.example.library.StartupTask: यह, आपके इंटरफ़ेस को लागू करने वाली किसी भी क्लास (*) को सुरक्षित रखता है.
    • ,allowobfuscation: इससे R8 को निर्देश मिलता है कि क्लास को सुरक्षित रखने के बावजूद, वह उसका नाम बदल सकता है या उसे अस्पष्ट कर सकता है. यह सुरक्षित है, क्योंकि आपकी लाइब्रेरी, क्लास के नाम पर निर्भर नहीं करती. इसे सीधे Class ऑब्जेक्ट मिलता है.
    • ,allowshrinking: इस मॉडिफ़ायर से R8 को निर्देश मिलता है कि अगर क्लास का इस्तेमाल नहीं किया जाता है, तो वह उसे हटा सकता है. इससे R8 को StartupTask के ऐसे लागू करने के तरीके को सुरक्षित तरीके से हटाने में मदद मिलती है जिसे कभी भी TaskRunner.execute() में पास नहीं किया जाता. संक्षेप में, इस नियम का मतलब यह है: अगर कोई ऐप्लिकेशन, StartupTask को लागू करने वाली किसी क्लास का इस्तेमाल करता है, तो R8 उस क्लास को सुरक्षित रखता है. R8, क्लास का साइज़ कम करने के लिए उसका नाम बदल सकता है. साथ ही, अगर ऐप्लिकेशन उसका इस्तेमाल नहीं करता है, तो उसे हटाया जा सकता है.
  • -keepclassmembers class * implements com.example.library.StartupTask { <init>(); }: यह नियम, पहली क्लास में पहचाने गए क्लास के खास सदस्यों को टारगेट करता है. इस मामले में, कंस्ट्रक्टर.
    • -keepclassmembers class * implements com.example.library.StartupTask: यह, StartupTask इंटरफ़ेस को लागू करने वाली क्लास के खास सदस्यों (मेथड, फ़ील्ड) को सुरक्षित रखता है. हालांकि, ऐसा सिर्फ़ तब होता है, जब लागू की गई क्लास को सुरक्षित रखा जा रहा हो.
    • { <init>(); }: यह सदस्य चुनने वाला टूल है. <init>, Java बाइटकोड में कंस्ट्रक्टर के लिए खास इंटरनल नाम है. यह हिस्सा, खास तौर पर डिफ़ॉल्ट, नो-आर्गुमेंट कंस्ट्रक्टर को टारगेट करता है.
    • यह नियम ज़रूरी है, क्योंकि आपका कोड, बिना किसी आर्ग्युमेंट के getDeclaredConstructor().newInstance() को कॉल करता है. इससे डिफ़ॉल्ट कंस्ट्रक्टर को रिफ़्लेक्शन के ज़रिए लागू किया जाता है. इस नियम के बिना, R8 को दिखता है कि कोई भी कोड सीधे तौर पर new PreCacheTask() को कॉल नहीं करता. इससे वह मान लेता है कि कंस्ट्रक्टर का इस्तेमाल नहीं किया गया है और उसे हटा देता है. इससे रनटाइम में आपका ऐप्लिकेशन, InstantiationException के साथ क्रैश हो जाता है.

मेथड एनोटेशन के आधार पर रिफ़्लेक्शन

लाइब्रेरी अक्सर, एनोटेशन तय करती हैं. डेवलपर इनका इस्तेमाल, मेथड या फ़ील्ड को टैग करने के लिए करते हैं. इसके बाद, लाइब्रेरी, रनटाइम में एनोटेट किए गए इन सदस्यों को ढूंढने के लिए रिफ़्लेक्शन का इस्तेमाल करती है. उदाहरण के लिए, रनटाइम में ज़रूरी मेथड ढूंढने के लिए, @OnLifecycleEvent एनोटेशन का इस्तेमाल किया जाता है.

उदाहरण के लिए, यहां दी गई स्थिति देखें. इसमें आपके पास एक लाइब्रेरी और एक ऐप्लिकेशन है, जो लाइब्रेरी का इस्तेमाल करता है. उदाहरण में, एक इवेंट बस दिखाई गई है. यह बस, @OnEvent के साथ एनोटेट किए गए मेथड को ढूंढती है और उन्हें लागू करती है.

लाइब्रेरी का कोड यहां दिया गया है:

@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) { /* ... */ }
            }
        }
    }
}

लाइब्रेरी का इस्तेमाल करने वाले ऐप्लिकेशन का कोड यहां दिया गया है:

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)
}

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

-keepattributes RuntimeVisibleAnnotations
-keep @interface com.example.library.OnEvent;
-keepclassmembers class * {
    @com.example.library.OnEvent <methods>;
}
  • -keepattributes RuntimeVisibleAnnotations: यह नियम, रनटाइम में पढ़े जाने वाले एनोटेशन को सुरक्षित रखता है.
  • -keep @interface com.example.library.OnEvent: यह नियम, OnEvent एनोटेशन क्लास को सुरक्षित रखता है.
  • -keepclassmembers class * {@com.example.library.OnEvent <methods>;}: यह नियम, किसी क्लास और उसके खास सदस्यों को सिर्फ़ तब सुरक्षित रखता है, जब क्लास का इस्तेमाल किया जा रहा हो और क्लास में वे सदस्य शामिल हों.
    • -keepclassmembers: यह नियम, किसी क्लास और उसके खास सदस्यों को सिर्फ़ तब सुरक्षित रखता है, जब क्लास का इस्तेमाल किया जा रहा हो और क्लास में वे सदस्य शामिल हों.
    • class *: यह नियम, किसी भी क्लास पर लागू होता है.
    • @com.example.library.OnEvent <methods>;: यह, किसी भी क्लास को सुरक्षित रखता है जिसमें एक या उससे ज़्यादा मेथड (<methods>) @com.example.library.OnEvent के साथ एनोटेट किए गए हैं. साथ ही, एनोटेट किए गए मेथड को भी सुरक्षित रखता है.

क्लास एनोटेशन के आधार पर रिफ़्लेक्शन

लाइब्रेरी, रिफ़्लेक्शन का इस्तेमाल करके, किसी खास एनोटेशन वाली क्लास को स्कैन कर सकती हैं. इस मामले में, टास्क रनर क्लास, रिफ़्लेक्शन का इस्तेमाल करके, ReflectiveExecutor के साथ एनोटेट की गई सभी क्लास को ढूंढती है और execute मेथड को लागू करती है.

उदाहरण के लिए, यहां दी गई स्थिति देखें. इसमें आपके पास एक लाइब्रेरी और एक ऐप्लिकेशन है, जो लाइब्रेरी का इस्तेमाल करता है.

लाइब्रेरी का कोड यहां दिया गया है:

@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)
        }
    }
}

लाइब्रेरी का इस्तेमाल करने वाले ऐप्लिकेशन का कोड यहां दिया गया है:

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

चूंकि लाइब्रेरी, रिफ़्लेक्शन का इस्तेमाल करके, खास क्लास को रिफ़्लेक्शन के ज़रिए ऐक्सेस करती है, इसलिए लाइब्रेरी में, उपयोगकर्ताओं के लिए बनाए गए कीप रूल वाली फ़ाइल में ये कीप रूल शामिल होने चाहिए:

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

यह कॉन्फ़िगरेशन बहुत कारगर है, क्योंकि इससे R8 को यह पता चलता है कि उसे किन चीज़ों को सुरक्षित रखना है.

ज़रूरी नहीं वाली डिपेंडेंसी के लिए रिफ़्लेक्शन

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

मुख्य लाइब्रेरी, नाम के हिसाब से किसी खास क्लास को ढूंढने के लिए, रिफ़्लेक्शन (Class.forName) का इस्तेमाल करती है. अगर क्लास मिल जाती है, तो सुविधा चालू हो जाती है. अगर नहीं मिलती है, तो यह सुविधा काम नहीं करती.

उदाहरण के लिए, यहां दिया गया कोड देखें. इसमें, मुख्य AnalyticsManager, वीडियो के आंकड़े की सुविधा चालू करने के लिए, ज़रूरी नहीं वाली VideoEventTracker क्लास की जांच करता है.

मुख्य लाइब्रेरी का कोड यहां दिया गया है:

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

ज़रूरी नहीं वाली वीडियो लाइब्रेरी का कोड यहां दिया गया है:

package com.example.analytics.video

class VideoEventTracker {
    // This constructor must be kept for the reflection call to succeed.
    init { /* ... */ }
}

ज़रूरी नहीं वाली लाइब्रेरी के डेवलपर की ज़िम्मेदारी है कि वह उपयोगकर्ताओं के लिए बनाए गए ज़रूरी कीप रूल उपलब्ध कराए. इस कीप रूल से यह पक्का होता है कि ज़रूरी नहीं वाली लाइब्रेरी का इस्तेमाल करने वाला कोई भी ऐप्लिकेशन, उस कोड को सुरक्षित रखे जिसे मुख्य लाइब्रेरी को ढूंढने की ज़रूरत होती है.

# In the video library's consumer keep rules file
-keep class com.example.analytics.video.VideoEventTracker {
    <init>();
}

इस नियम के बिना, R8, ज़रूरी नहीं वाली लाइब्रेरी से VideoEventTracker को हटा सकता है, क्योंकि उस मॉड्यूल में सीधे तौर पर इसका इस्तेमाल नहीं किया जाता. कीप रूल, क्लास और उसके कंस्ट्रक्टर को सुरक्षित रखता है. इससे मुख्य लाइब्रेरी, इसका इंस्टेंस बना पाती है.

प्राइवेट सदस्यों को ऐक्सेस करने के लिए रिफ़्लेक्शन

रिफ़्लेक्शन का इस्तेमाल करके, प्राइवेट या सुरक्षित किए गए ऐसे कोड को ऐक्सेस करने से बड़ी समस्याएं हो सकती हैं जो लाइब्रेरी के सार्वजनिक एपीआई का हिस्सा नहीं है. इस तरह के कोड में, बिना सूचना दिए बदलाव किया जा सकता है. इससे आपके ऐप्लिकेशन में अनचाहा व्यवहार हो सकता है या वह क्रैश हो सकता है.

गैर-सार्वजनिक एपीआई के लिए रिफ़्लेक्शन पर निर्भर रहने पर, आपको ये समस्याएं आ सकती हैं:

  • अपडेट ब्लॉक होना: प्राइवेट या सुरक्षित किए गए कोड में बदलाव होने पर, लाइब्रेरी के नए वर्शन पर अपडेट नहीं किया जा सकता.
  • फ़ायदों से चूकना: हो सकता है कि आपको नई सुविधाओं, क्रैश से जुड़ी अहम समस्याओं को ठीक करने वाले अपडेट या सुरक्षा से जुड़े ज़रूरी अपडेट न मिलें.

R8 के ऑप्टिमाइज़ेशन और रिफ़्लेक्शन

अगर आपको लाइब्रेरी के प्राइवेट या सुरक्षित किए गए कोड में रिफ़्लेक्शन का इस्तेमाल करना है, तो R8 के ऑप्टिमाइज़ेशन पर ध्यान दें. अगर इन सदस्यों के सीधे रेफ़रंस नहीं हैं, तो R8 मान सकता है कि इनका इस्तेमाल नहीं किया गया है. इसके बाद, वह इन्हें हटा सकता है या इनका नाम बदल सकता है. इससे रनटाइम में ऐप्लिकेशन क्रैश हो सकता है. साथ ही, अक्सर NoSuchMethodException या NoSuchFieldException जैसे गलत गड़बड़ी के मैसेज दिख सकते हैं.

उदाहरण के लिए, यहां दी गई स्थिति देखें. इससे पता चलता है कि लाइब्रेरी क्लास से प्राइवेट फ़ील्ड को कैसे ऐक्सेस किया जा सकता है.

आपके पास मौजूद नहीं है, ऐसी लाइब्रेरी का कोड यहां दिया गया है:

class LibraryClass {
    private val secretMessage = "R8 will remove me"
}

आपके ऐप्लिकेशन का कोड यहां दिया गया है:

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
}

अपने ऐप्लिकेशन में -keep नियम जोड़ें, ताकि R8, प्राइवेट फ़ील्ड को न हटाए:

-keepclassmembers class com.example.LibraryClass {
    private java.lang.String secretMessage;
}
  • -keepclassmembers: यह, किसी क्लास के खास सदस्यों को सिर्फ़ तब सुरक्षित रखता है, जब क्लास को सुरक्षित रखा गया हो.
  • class com.example.LibraryClass: यह, फ़ील्ड वाली क्लास को टारगेट करता है.
  • private java.lang.String secretMessage;: यह, नाम और टाइप के हिसाब से खास प्राइवेट फ़ील्ड की पहचान करता है.

Java Native Interface (JNI)

नेटिव (C/C++ कोड) से Java या Kotlin में अपकॉल के साथ काम करते समय, R8 के ऑप्टिमाइज़ेशन में समस्याएं आ सकती हैं. हालांकि, इसका उल्टा भी सही है. Java या Kotlin से नेटिव कोड में डाउनकॉल के साथ समस्याएं आ सकती हैं. डिफ़ॉल्ट फ़ाइल proguard-android-optimize.txt में, डाउनकॉल को काम करने के लिए, यह नियम शामिल है. यह नियम, नेटिव मेथड को ट्रिम होने से बचाता है.

-keepclasseswithmembernames,includedescriptorclasses class * {
  native <methods>;
}

Java Native Interface (JNI) के ज़रिए नेटिव कोड के साथ इंटरैक्शन

जब आपका ऐप्लिकेशन, JNI का इस्तेमाल करके, नेटिव (C/C++) कोड से Java या Kotlin में अपकॉल करता है, तो R8 यह नहीं देख पाता कि आपके नेटिव कोड से किन मेथड को कॉल किया गया है. अगर आपके ऐप्लिकेशन में इन मेथड के सीधे रेफ़रंस नहीं हैं, तो R8 गलत तरीके से मान लेता है कि इन मेथड का इस्तेमाल नहीं किया गया है और उन्हें हटा देता है. इससे आपका ऐप्लिकेशन क्रैश हो जाता है.

यहां दिए गए उदाहरण में, Kotlin क्लास दिखाई गई है. इसमें एक ऐसा मेथड है जिसे नेटिव लाइब्रेरी से कॉल किया जाना है. नेटिव लाइब्रेरी, ऐप्लिकेशन टाइप का इंस्टेंस बनाती है और नेटिव कोड से 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")
        }
    }
}

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

अपने ऐप्लिकेशन में ये कीप रूल जोड़ें:

-keepclassmembers,includedescriptorclasses class com.example.JniBridge {
    public void onNativeEvent(com.example.model.NativeData);
}

-keep class NativeData{
        <init>(java.lang.Integer, java.lang.String);
}

इन कीप रूल से, R8 को onNativeEvent मेथड और उसके पैरामीटर टाइप को हटाने या उसका नाम बदलने से रोका जा सकता है.

  • -keepclassmembers,includedescriptorclasses class com.example.JniBridge{ public void onNativeEvent(com.example.model.NativeData);}: यह, किसी क्लास के खास सदस्यों को सिर्फ़ तब सुरक्षित रखता है, जब क्लास को इंस्टैंशिएट किया गया हो Kotlin या Java कोड में पहले—इससे R8 को यह पता चलता है कि ऐप्लिकेशन, क्लास का इस्तेमाल कर रहा है और उसे क्लास के खास सदस्यों को सुरक्षित रखना चाहिए.
    • -keepclassmembers: यह, किसी क्लास के खास सदस्यों को सिर्फ़ तब सुरक्षित रखता है, जब क्लास को Kotlin या Java कोड में पहले इंस्टैंशिएट किया गया हो. इससे R8 को यह पता चलता है कि ऐप्लिकेशन, क्लास का इस्तेमाल कर रहा है और उसे क्लास के खास सदस्यों को सुरक्षित रखना चाहिए.
    • class com.example.JniBridge: यह, फ़ील्ड वाली क्लास को टारगेट करता है.
    • includedescriptorclasses: यह मॉडिफ़ायर, मेथड के सिग्नेचर या डिस्क्रिप्टर में मौजूद किसी भी क्लास को सुरक्षित रखता है. इस मामले में, यह R8 को com.example.models.NativeData क्लास का नाम बदलने या उसे हटाने से रोकता है. इसका इस्तेमाल, पैरामीटर के तौर पर किया जाता है. अगर NativeData का नाम बदला जाता है (उदाहरण के लिए, a.a), तो मेथड सिग्नेचर, नेटिव कोड की उम्मीद के मुताबिक नहीं होगा. इससे ऐप्लिकेशन क्रैश हो जाएगा.
    • public void onNativeEvent(com.example.models.NativeData);: इससे, सुरक्षित रखने के लिए, मेथड का सटीक Java सिग्नेचर तय किया जाता है.
  • -keep class NativeData{<init>(java.lang.Integer, java.lang.String);}: हालांकि, includedescriptorclasses से यह पक्का होता है कि NativeData क्लास को सुरक्षित रखा गया है. NativeData में मौजूद किसी भी सदस्य (फ़ील्ड या मेथड) को सीधे तौर पर आपके नेटिव JNI कोड से ऐक्सेस करने के लिए, उनके अपने कीप रूल की ज़रूरत होती है.
    • -keep class NativeData: यह, NativeData नाम की क्लास को टारगेट करता है. साथ ही, ब्लॉक यह तय करता है कि NativeData क्लास में किन सदस्यों को सुरक्षित रखना है.
    • <init>(java.lang.Integer, java.lang.String): यह कंस्ट्रक्टर का सिग्नेचर है. इससे, दो पैरामीटर लेने वाले कंस्ट्रक्टर की पहचान होती है. पहला पैरामीटर Integer है और दूसरा String है.

प्लेटफ़ॉर्म के लिए इनडायरेक्ट कॉल

Parcelable को लागू करने के तरीके से डेटा ट्रांसफ़र करना

Android फ़्रेमवर्क, आपके Parcelable ऑब्जेक्ट के इंस्टेंस बनाने के लिए रिफ़्लेक्शन का इस्तेमाल करता है. Kotlin में आधुनिक डेवलपमेंट के लिए, आपको kotlin-parcelize प्लगिन का इस्तेमाल करना चाहिए. यह, ज़रूरी Parcelable को लागू करने का तरीका अपने-आप जनरेट करता है. इसमें, CREATOR फ़ील्ड और फ़्रेमवर्क को ज़रूरी मेथड शामिल हैं.

उदाहरण के लिए, यहां दिया गया उदाहरण देखें. इसमें, Parcelable क्लास बनाने के लिए, kotlin-parcelize प्लगिन का इस्तेमाल किया गया है:

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

इस स्थिति में, कोई सुझाया गया कीप रूल नहीं है. kotlin-parcelize Gradle प्लगिन, @Parcelize के साथ एनोटेट की गई क्लास के लिए, ज़रूरी कीप रूल अपने-आप जनरेट करता है. यह आपके लिए जटिलता को हैंडल करता है. साथ ही, यह पक्का करता है कि जनरेट किए गए CREATOR और कंस्ट्रक्टर, Android फ़्रेमवर्क के रिफ़्लेक्शन कॉल के लिए सुरक्षित रखे जाएं.

अगर आपने @Parcelize का इस्तेमाल किए बिना, Kotlin में Parcelable क्लास को मैन्युअल तरीके से लिखा है, तो CREATOR फ़ील्ड और Parcel को स्वीकार करने वाले कंस्ट्रक्टर को सुरक्षित रखने की ज़िम्मेदारी आपकी है. ऐसा न करने पर, सिस्टम के आपके ऑब्जेक्ट को डीसीरियलाइज़ करने की कोशिश करने पर, आपका ऐप्लिकेशन क्रैश हो जाएगा. @Parcelize का इस्तेमाल करना, स्टैंडर्ड और सुरक्षित तरीका है.

kotlin-parcelize प्लगिन का इस्तेमाल करते समय, इन बातों का ध्यान रखें:

  • प्लगिन, कंपाइलेशन के दौरान CREATOR फ़ील्ड अपने-आप बनाता है.
  • proguard-android-optimize.txt फ़ाइल में, इन फ़ील्ड को सही तरीके से काम करने के लिए, ज़रूरी keep नियम शामिल हैं.
  • ऐप्लिकेशन डेवलपर को यह पुष्टि करनी होगी कि सभी ज़रूरी keep नियम मौजूद हैं. खास तौर पर, कस्टम लागू करने के तरीके या तीसरे पक्ष की डिपेंडेंसी के लिए.

रिफ़्लेक्शन या बाइटकोड ट्रांसफ़ॉर्मेशन का इस्तेमाल करने वाली लाइब्रेरी, रनटाइम में डाइनैमिक तरीके से कोड को ऐक्सेस करती हैं. अगर R8, क्लास, फ़ील्ड या मेथड को हटाता है या उनका नाम बदलता है, तो आपका ऐप्लिकेशन क्रैश हो सकता है.

हालांकि, तीसरे पक्ष की मशहूर लाइब्रेरी (जैसे, Gson, Retrofit, और Kotlinx Serialization) में, R8 के लिए उपयोगकर्ताओं के कीप रूल अपने-आप शामिल होते हैं. इन लाइब्रेरी के नए वर्शन का इस्तेमाल करते समय, आपको अपने प्रोजेक्ट में मैन्युअल तरीके से कीप रूल जोड़ने की ज़रूरत नहीं होती.

Gson

Gson, JSON सीरियलाइज़ेशन और डीसीरियलाइज़ेशन लाइब्रेरी है. यह रिफ़्लेक्शन पर काफ़ी निर्भर करती है. अपने ऐप्लिकेशन को ऑप्टिमाइज़ करने के लिए, फ़ुल मोड का इस्तेमाल करने पर, यह जेनेरिक टाइप सिग्नेचर, डिफ़ॉल्ट कंस्ट्रक्टर, और एनोटेट नहीं किए गए फ़ील्ड को हटा देता है. हालांकि, ऐसा तब तक नहीं करता, जब तक साफ़ तौर पर निर्देश न दिया जाए.

यह पक्का करने के लिए कि Gson सही तरीके से काम करे, अपने डेटा मॉडल क्लास में नॉन-ट्रांज़िएंट फ़ील्ड को सुरक्षित रखने और TypeToken के क्रम को सुरक्षित रखने के लिए, खास नियम जोड़ें:

# Preserve generic type information required for deserialization
-keepattributes Signature

# Keep all non-transient fields in your data model classes for reflection
-keepclassmembers class com.example.models.** {
    !transient <fields>;
}

# Keep TypeToken itself and any anonymous classes extending it
-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken { *; }
-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken

transient मॉडिफ़ायर के साथ मार्क किए गए फ़ील्ड को, Gson सीरियलाइज़ेशन और डीसीरियलाइज़ेशन के दौरान अनदेखा कर देता है. इसलिए, कीप रूल खास तौर पर नॉन-ट्रांज़िएंट फ़ील्ड (!transient) को टारगेट करता है.

Retrofit

Retrofit, नेटवर्किंग लाइब्रेरी है. यह, नेटवर्क के अनुरोध बनाने और जवाबों को बदलने के लिए, रिफ़्लेक्शन का इस्तेमाल करके, HTTP एनोटेशन (जैसे, @GET या @POST) के साथ एनोटेट किए गए सर्विस इंटरफ़ेस मेथड की जांच करती है.

Retrofit, Proxy.newProxyInstance() का इस्तेमाल करके, रनटाइम में आपके एपीआई इंटरफ़ेस के लागू करने के तरीके को डाइनैमिक तरीके से जनरेट करता है. चूंकि R8 को ये इंटरफ़ेस, स्टैटिक तरीके से लागू करने वाली कोई क्लास नहीं दिखती, इसलिए वह मेथड या उनके जेनेरिक रिटर्न टाइप को हटा सकता है.

बंडल किए गए कीप रूल

Retrofit, रनटाइम रिफ़्लेक्शन पर निर्भर करता है, ताकि जेनेरिक पैरामीटर, मेथड एनोटेशन, और पैरामीटर एनोटेशन की जांच की जा सके. सही कॉन्फ़िगरेशन के बिना, R8 का फ़ुल मोड, रिटर्न टाइप, Kotlin कंटीन्यूएशन, और रिस्पॉन्स क्लास से जेनेरिक सिग्नेचर को पूरी तरह हटा सकता है. साथ ही, इंटरफ़ेस की वैल्यू को null से बदल सकता है, क्योंकि Retrofit इंटरफ़ेस को प्रॉक्सी के साथ डाइनैमिक तरीके से इंस्टैंशिएट किया जाता है.

Retrofit 2.10.0 से, लाइब्रेरी में, एनोटेशन डिफ़ॉल्ट, सर्विस मेथड पैरामीटर, और ज़रूरी क्लास मेटाडेटा को सुरक्षित रखने के लिए, आधिकारिक कीप रूल अपने-आप शामिल होते हैं. ज़्यादा जानकारी के लिए, Retrofit के इस्तेमाल किए गए नियम देखें.

जेनेरिक रिटर्न टाइप सुरक्षित रखना

Retrofit, नेटवर्क के जवाब को सही तरीके से डीसीरियलाइज़ करने के लिए, रिटर्न टाइप के जेनेरिक सिग्नेचर (उदाहरण के लिए, Observable<Data>) की जांच करता है. अगर R8, जेनेरिक सिग्नेचर को हटा देता है, तो Retrofit, इंस्टैंशिएट किए गए ऑब्जेक्ट को null से बदल देगा.

R8 के फ़ुल मोड को, रिटर्न टाइप के जेनेरिक सिग्नेचर को हटाने से रोकने के लिए, इस शर्त के साथ लागू होने वाले नियम का इस्तेमाल करें:

# Preserve generic type information for Call/Observable return types
-keepattributes Signature

# If an interface has a Retrofit HTTP annotation, keep its return type (class <3>)
-if interface * {
    @retrofit2.http.* public *** *(...);
}
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>

असल डेटा मॉडल क्लास को भी सुरक्षित रखना होगा (उदाहरण के लिए, Data में Observable<Data>), क्योंकि इसे कनवर्टर (जैसे, Gson) रिफ़्लेक्शन के ज़रिए बनाएगा.

कोरूटीन

Kotlin कोरूटीन का इस्तेमाल करने पर, Kotlin कंपाइलर, कंपाइल किए गए मेथड सिग्नेचर में Continuation पैरामीटर जोड़कर, suspend फ़ंक्शन को बदल देता है.

जब Retrofit जैसी लाइब्रेरी, suspend फ़ंक्शन के जेनेरिक सिग्नेचर को रिफ़्लेक्शन के ज़रिए पढ़ती हैं, तो वे उस Continuation पैरामीटर पर निर्भर करती हैं. फ़ुल मोड का इस्तेमाल करने पर, Signature एट्रिब्यूट को सिर्फ़ उन क्लास के लिए सुरक्षित रखा जाता है जिन्हें साफ़ तौर पर सुरक्षित रखा गया है. Continuation एक सिंथेटिक पैरामीटर है. इसलिए, R8 डिफ़ॉल्ट रूप से इसके सिग्नेचर को हटा देता है. इससे रिफ़्लेक्शन टूट जाता है.

सिग्नेचर को हटाने से रोकने और फ़ुल मोड में रनटाइम कंपैटिबिलिटी पक्का करने के लिए, यह नियम शामिल करें:

# Keep the signature attribute globally
-keepattributes Signature

# Explicitly keep the Continuation class so its signature is not stripped
-keep class kotlin.coroutines.Continuation