यहां दिए गए उदाहरण, उन सामान्य स्थितियों पर आधारित हैं जिनमें ऑप्टिमाइज़ेशन के लिए 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
इंटरफ़ेस को लागू करती हैं. साथ ही, उन्हें सुरक्षित रखता है. इसके अलावा, यह नियम उनके नो-आर्ग्युमेंट कंस्ट्रक्टर को भी सुरक्षित रखता है, ताकि लाइब्रेरी StartupTask
को इंस्टैंशिएट और एक्ज़ीक्यूट कर सके.PreCacheTask
::class.java
के साथ बातचीत
लाइब्रेरी, ऐप्लिकेशन से सीधे Class
ऑब्जेक्ट पास करके क्लास लोड कर सकती हैं. यह नाम के हिसाब से क्लास लोड करने के तरीके से ज़्यादा बेहतर है. इससे क्लास का एक ऐसा मज़बूत रेफ़रंस बनता है जिसे R8 पहचान सकता है. हालांकि, इससे R8 को क्लास हटाने से रोका जा सकता है. इसके बावजूद, आपको keep नियम का इस्तेमाल करना होगा, ताकि यह बताया जा सके कि क्लास को रिफ़्लेक्टिव तरीके से इंस्टैंटिएट किया गया है. साथ ही, उन सदस्यों को सुरक्षित रखा जा सके जिन्हें रिफ़्लेक्टिव तरीके से ऐक्सेस किया जाता है. जैसे, कंस्ट्रक्टर.
उदाहरण के लिए, यहां दिए गए परिदृश्य पर विचार करें. इसमें आपके पास एक लाइब्रेरी और एक ऐसा ऐप्लिकेशन है जो लाइब्रेरी का इस्तेमाल करता है. लाइब्रेरी लोडर, क्लास रेफ़रंस को सीधे तौर पर पास करके 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
क्लास का इस्तेमाल कभी नहीं किया जाता है, तो R8 इसके साइज़ को कम कर सकता है या इसे हटा सकता है. हालांकि, किसी भी लागू करने के लिए, जैसे कि उदाहरण में इस्तेमाल किया गया 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>;
: इससे उन सभी क्लास को सुरक्षित रखा जाता है जिनमें@com.example.library.OnEvent
के साथ एनोटेट किए गए एक या उससे ज़्यादा तरीके (<methods>
) मौजूद हैं. साथ ही, इससे एनोटेट किए गए तरीकों को भी सुरक्षित रखा जाता है.
क्लास एनोटेशन के आधार पर रिफ़्लेक्शन
लाइब्रेरी, रिफ़्लेक्शन का इस्तेमाल करके उन क्लास को स्कैन कर सकती हैं जिनमें कोई खास एनोटेशन मौजूद है. इस मामले में, टास्क रनर क्लास, रिफ़्लेक्शन का इस्तेमाल करके 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
को हटा देता है, क्योंकि उस मॉड्यूल में सीधे तौर पर इसका इस्तेमाल नहीं किया जाता. keep नियम, क्लास और उसके कंस्ट्रक्टर को सुरक्षित रखता है. इससे कोर लाइब्रेरी, क्लास को इंस्टैंटिएट कर पाती है.
निजी सदस्यों को ऐक्सेस करने के लिए रिफ़्लेक्शन
निजी या सुरक्षित कोड को ऐक्सेस करने के लिए रिफ़्लेक्शन का इस्तेमाल करने से गंभीर समस्याएं हो सकती हैं. यह कोड, लाइब्रेरी के सार्वजनिक एपीआई का हिस्सा नहीं होता. इस तरह के कोड में बिना किसी सूचना के बदलाव किया जा सकता है. इससे आपके ऐप्लिकेशन में अनचाहा व्यवहार हो सकता है या वह क्रैश हो सकता है.
जब नॉन-पब्लिक एपीआई के लिए रिफ़्लेक्शन का इस्तेमाल किया जाता है, तो आपको ये समस्याएं आ सकती हैं:
- अपडेट ब्लॉक किए गए हैं: निजी या सुरक्षित किए गए कोड में बदलाव करने से, आपको लाइब्रेरी के नए वर्शन पर अपडेट करने में समस्या आ सकती है.
- फ़ायदे नहीं मिल पाएंगे: ऐसा हो सकता है कि आपको नई सुविधाओं, क्रैश से जुड़ी ज़रूरी समस्याओं को ठीक करने वाले अपडेट या सुरक्षा से जुड़े ज़रूरी अपडेट न मिलें.
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 नेटिव इंटरफ़ेस (JNI)
नेटिव (C/C++ कोड) से Java या Kotlin में अपकॉल के साथ काम करते समय, R8 के ऑप्टिमाइज़ेशन में समस्याएं आ सकती हैं. हालांकि, इसका उल्टा भी हो सकता है. यानी, Java या Kotlin से नेटिव कोड में डाउनकॉल करने पर समस्याएं आ सकती हैं. डिफ़ॉल्ट फ़ाइल proguard-android-optimize.txt
में डाउनकॉल को काम करने के लिए, यह नियम शामिल होता है. इस नियम से, नेटिव तरीकों को ट्रिम होने से बचाया जाता है.
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
Java नेटिव इंटरफ़ेस (जेएनआई) के ज़रिए नेटिव कोड के साथ इंटरैक्शन
जब आपका ऐप्लिकेशन, नेटिव (C/C++) कोड से Java या Kotlin में अपकॉल करने के लिए JNI का इस्तेमाल करता है, तो 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 फ़्रेमवर्क के रिफ़्लेक्शन कॉल के लिए सुरक्षित रखे जाएं.
अगर Kotlin में @Parcelize
का इस्तेमाल किए बिना, Parcelable
क्लास को मैन्युअल तरीके से लिखा जाता है, तो CREATOR
फ़ील्ड और Parcel
को स्वीकार करने वाले कंस्ट्रक्टर को बनाए रखने की ज़िम्मेदारी आपकी होती है. ऐसा न करने पर, जब सिस्टम आपके ऑब्जेक्ट को डीसीरियलाइज़ करने की कोशिश करता है, तो आपका ऐप्लिकेशन क्रैश हो जाता है. @Parcelize
का इस्तेमाल करना, स्टैंडर्ड और ज़्यादा सुरक्षित तरीका है.
kotlin-parcelize
प्लगिन का इस्तेमाल करते समय, इन बातों का ध्यान रखें:
- यह प्लगिन, कंपाइल करने के दौरान
CREATOR
फ़ील्ड अपने-आप बनाता है. proguard-android-optimize.txt
फ़ाइल में, इन फ़ील्ड को सही तरीके से काम करने के लिए ज़रूरीkeep
नियम शामिल होते हैं.- ऐप्लिकेशन डेवलपर को यह पुष्टि करनी होगी कि सभी ज़रूरी
keep
नियम मौजूद हैं. खास तौर पर, कस्टम तरीके से लागू किए गए नियमों या तीसरे पक्ष की डिपेंडेंसी के लिए.