নিয়ম ব্যবহারের উদাহরণ ও কেস স্টাডি

নিচে দেওয়া উদাহরণগুলি সাধারণ পরিস্থিতির উপর ভিত্তি করে তৈরি করা হয়েছে যেখানে আপনি অপ্টিমাইজেশনের জন্য R8 ব্যবহার করেন কিন্তু কীপ নিয়ম ড্রাফ্ট করার জন্য উন্নত নির্দেশিকা প্রয়োজন।

প্রতিফলন

সাধারণত, ভালো পারফর্ম্যান্সের জন্য, রিফ্লেকশন ব্যবহার না করার সাজেশন দেওয়া হয়। তবে, কিছু পরিস্থিতিতে এটি এড়ানো সম্ভব নাও হতে পারে। নিচে দেওয়া উদাহরণগুলি রিফ্লেকশন ব্যবহার করে এমন সাধারণ পরিস্থিতিতে কী কী নিয়ম মেনে চলতে হবে সেই বিষয়ে নির্দেশিকা প্রদান করে।

রিফ্লেকশন আপনার নিজের অ্যাপ বা লাইব্রেরি কোডে থাকলে, ম্যানুয়ালি কিপ করার নিয়ম লেখার পরিবর্তে আপনি অ্যানোটেশন কিপ করুন ব্যবহার করতে পারেন। একটি Gradle প্লাগ-ইন আপনার সোর্স কোডে অ্যানোটেশন থেকে রাখার নিয়ম তৈরি করে, যার মধ্যে লাইব্রেরির জন্য গ্রাহকের রাখার নিয়ম অন্তর্ভুক্ত।

নাম অনুযায়ী লোড করা ক্লাস সহ রিফ্লেকশন

লাইব্রেরি প্রায়ই 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>();
}

এইসব নিয়ম এই ধরনের রিফ্লেকশনের সাথে নিখুঁতভাবে কাজ করার জন্য ডিজাইন করা হয়েছে, এর ফলে কোড যাতে সঠিকভাবে কাজ করে তা নিশ্চিত করার পাশাপাশি সর্বাধিক অপ্টিমাইজেশন করা যায়। অ্যাপ যদি কখনও StartupTask ক্লাস ব্যবহার না করে, তাহলে নিয়মগুলি 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-কে নির্দেশ দেয় যে ক্লাসটি অব্যবহৃত থাকলে সেটি সরিয়ে দিতে পারে। StartupTask-এর এমন কোনও ইমপ্লিমেন্টেশন যা কখনও TaskRunner.execute()-এ পাস করা হয় না, R8 তা নিরাপদে মুছে দিতে সাহায্য করে। সংক্ষেপে, এই নিয়মের অর্থ হল: কোনও অ্যাপ যদি এমন ক্লাস ব্যবহার করে যা 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 সরিয়ে দেয় কারণ সেই মডিউলের কোনও কিছুই সরাসরি এটি ব্যবহার করে না। কিপ রুল ক্লাস ও এর কনস্ট্রাক্টরকে সংরক্ষণ করে, যার ফলে কোর লাইব্রেরি এটিকে সফলভাবে ইনস্ট্যান্টিয়েট করতে পারে।

ব্যক্তিগত মেম্বারদের অ্যাক্সেস করার জন্য রিফ্লেকশন

লাইব্রেরির পাবলিক API-এর অংশ নয় এমন ব্যক্তিগত বা সুরক্ষিত কোড অ্যাক্সেস করার জন্য রিফ্লেকশন ব্যবহার করলে গুরুতর সমস্যা হতে পারে। এই ধরনের কোড বিজ্ঞপ্তি ছাড়াই পরিবর্তন করা হতে পারে, যার ফলে আপনার অ্যাপ্লিকেশন অপ্রত্যাশিত আচরণ করতে পারে বা ক্র্যাশ হতে পারে।

আপনি যখন সর্বজনীন নয় এমন API-এর জন্য রিফ্লেকশনের উপর নির্ভর করেন, তখন আপনি নিম্নলিখিত সমস্যার সম্মুখীন হতে পারেন:

  • আপডেট ব্লক করা হয়েছে: ব্যক্তিগত বা সুরক্ষিত কোডে পরিবর্তন করা হলে, আপনি লাইব্রেরির আরও উন্নত ভার্সনে আপডেট করতে পারবেন না।
  • সুবিধা মিস করা: আপনি নতুন ফাংশনালিটি, গুরুত্বপূর্ণ ক্র্যাশ সংক্রান্ত সমস্যার সমাধান বা প্রয়োজনীয় নিরাপত্তা আপডেট মিস করতে পারেন।

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
}

R8 যাতে প্রাইভেট ফিল্ড সরিয়ে না দেয়, তার জন্য আপনার অ্যাপে -keep নিয়ম যোগ করুন:

-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)-এর মাধ্যমে নেটিভ কোডের সাথে ইন্টার‍্যাকশন

আপনার অ্যাপ যখন নেটিভ (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);: এটি সংরক্ষণ করার জন্য পদ্ধতির সঠিক জাভা সিগনেচার নির্দিষ্ট করে।
  • -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।

এছাড়াও, আপনি নেটিভ কোড কল করে এমন পদ্ধতিগুলি Jetpack Keep থেকে @UnconditionallyKeep অ্যানোটেশন দিয়ে অ্যানোটেট করতে পারেন, এর জন্য আপনাকে কিপ নিয়ম লিখতে হবে না। অ্যানোটেশন মেথডের সিগনেচারে ক্লাস রাখে না, যেমন NativeData, তাই আপনাকে এখনও সেই ক্লাসগুলি আলাদাভাবে রাখতে হবে।

ইনডাইরেক্ট প্ল্যাটফর্ম কল

Parcelable-এর প্রয়োগের মাধ্যমে ডেটা ট্রান্সফার করা

Android ফ্রেমওয়ার্ক আপনার Parcelable অবজেক্টের ইনস্ট্যান্স তৈরি করতে রিফ্লেকশন ব্যবহার করে। আধুনিক Kotlin ডেভেলপমেন্টে, আপনার kotlin-parcelize প্লাগ-ইন ব্যবহার করা উচিত, যা অটোমেটিক প্রয়োজনীয় Parcelable ইমপ্লিমেন্টেশন তৈরি করে, যার মধ্যে ফ্রেমওয়ার্কের প্রয়োজনীয় CREATOR ফিল্ড ও পদ্ধতি অন্তর্ভুক্ত থাকে।

যেমন, নিম্নলিখিত উদাহরণটি বিবেচনা করুন যেখানে kotlin-parcelize প্লাগ-ইন ব্যবহার করে 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

এই পরিস্থিতিতে, কোনও সাজেস্ট করা রাখার নিয়ম নেই। 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 এড়িয়ে যায়, এই কারণে keep নিয়মটি বিশেষভাবে নন-ট্রানজিয়েন্ট ফিল্ডকে (!transient) টার্গেট করে।

রেট্রোফিট

Retrofit হল একটি নেটওয়ার্কিং লাইব্রেরি যা HTTP অ্যানোটেশন (যেমন @GET বা @POST) সহ অ্যানোটেট করা পরিষেবা ইন্টারফেস পদ্ধতিগুলি পরীক্ষা করে, নেটওয়ার্ক অনুরোধ তৈরি করতে এবং প্রতিক্রিয়াগুলি রূপান্তর করতে প্রতিফলন ব্যবহার করে।

Retrofit, Proxy.newProxyInstance() ব্যবহার করে রানটাইমে আপনার API ইন্টারফেসের ইমপ্লিমেন্টেশন ডায়নামিকভাবে জেনারেট করে। R8 যেহেতু এই ইন্টারফেসগুলি প্রয়োগকারী কোনও ক্লাস স্ট্যাটিক্যালি দেখতে পায় না, তাই এটি মেথড বা সেগুলির জেনেরিক রিটার্ন টাইপ সরিয়ে দিতে পারে।

বান্ডেল করা রাখার নিয়ম

Retrofit, রানটাইম রিফ্লেকশনের উপর নির্ভর করে জেনেরিক প্যারামিটার, মেথড অ্যানোটেশন ও প্যারামিটার অ্যানোটেশন পরীক্ষা করে। সঠিক কনফিগারেশন না থাকলে, R8 full মোড রিটার্ন টাইপ, 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) দ্বারা প্রতিফলিতভাবে তৈরি করা হবে।

Coroutines

আপনি Kotlin করুটিন ব্যবহার করলে, Kotlin কম্পাইলার suspend ফাংশনগুলিকে কম্পাইল করা পদ্ধতির সিগনেচারে একটি Continuation প্যারামিটার যোগ করে ট্রান্সফর্ম করে।

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