নিয়ম ব্যবহারের ক্ষেত্রে এবং উদাহরণ রাখুন

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

প্রতিফলন

সাধারণত, সর্বোত্তম পারফরম্যান্সের জন্য রিফ্লেকশন ব্যবহার করার পরামর্শ দেওয়া হয় না। তবে, কিছু নির্দিষ্ট পরিস্থিতিতে এটি অপরিহার্য হতে পারে। নিম্নলিখিত উদাহরণগুলো রিফ্লেকশন ব্যবহৃত হয় এমন সাধারণ পরিস্থিতিগুলোতে কিপ রুলস (keep rules) ব্যবহারের জন্য নির্দেশনা প্রদান করে।

নাম অনুসারে লোড করা ক্লাসগুলির সাথে প্রতিফলন

লাইব্রেরিগুলো প্রায়শই ক্লাসের নামটিকে একটি 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-কে ক্লাসটি মুছে ফেলা থেকে বিরত রাখে, তবুও ক্লাসটি যে রিফ্লেক্টিভভাবে ইনস্ট্যানশিয়েট করা হয়েছে তা ঘোষণা করতে এবং কনস্ট্রাক্টরের মতো রিফ্লেক্টিভভাবে অ্যাক্সেস করা মেম্বারগুলোকে সুরক্ষিত রাখতে আপনাকে একটি '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>();
}

এই নিয়মগুলো এই ধরনের রিফ্লেকশনের সাথে নিখুঁতভাবে কাজ করার জন্য ডিজাইন করা হয়েছে, যা কোডের সঠিক কার্যকারিতা নিশ্চিত করার পাশাপাশি সর্বোচ্চ অপটিমাইজেশনের সুযোগ দেয়। অ্যাপটি যদি 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> হলো একটি বিশেষ অভ্যন্তরীণ নাম। এই অংশটি বিশেষভাবে ডিফল্ট, আর্গুমেন্টবিহীন কনস্ট্রাক্টরকে টার্গেট করে।
    • এই নিয়মটি অত্যন্ত গুরুত্বপূর্ণ, কারণ আপনার কোড কোনো আর্গুমেন্ট ছাড়াই 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 সরিয়ে দিত, কারণ সেই মডিউলের কোনো কিছুই এটিকে সরাসরি ব্যবহার করে না। ‘কিপ’ নিয়মটি ক্লাস এবং এর কনস্ট্রাক্টরকে অক্ষুণ্ণ রাখে, ফলে কোর লাইব্রেরি সফলভাবে এটিকে ইনস্ট্যানশিয়েট করতে পারে।

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

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

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

  • অবরুদ্ধ আপডেট: ব্যক্তিগত বা সুরক্ষিত কোডের পরিবর্তন আপনাকে লাইব্রেরির উচ্চতর সংস্করণে আপডেট করতে বাধা দিতে পারে।
  • সুবিধা থেকে বঞ্চিত হওয়া: আপনি নতুন কার্যকারিতা, গুরুত্বপূর্ণ ক্র্যাশ ফিক্স বা অপরিহার্য নিরাপত্তা আপডেট থেকে বঞ্চিত হতে পারেন।

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; : এটি নাম এবং টাইপের মাধ্যমে নির্দিষ্ট প্রাইভেট ফিল্ডটিকে শনাক্ত করে।

জাভা নেটিভ ইন্টারফেস (JNI)

নেটিভ (C/C++ কোড) থেকে জাভা বা কোটলিনে আপকল করার সময় R8-এর অপটিমাইজেশনে সমস্যা হতে পারে। যদিও এর বিপরীতটিও সত্য—জাভা বা কোটলিন থেকে নেটিভ কোডে ডাউনকল করার ক্ষেত্রেও সমস্যা হতে পারে—ডাউনকলগুলো সচল রাখার জন্য ডিফল্ট ফাইল proguard-android-optimize.txt নিম্নলিখিত নিয়মটি অন্তর্ভুক্ত রয়েছে। এই নিয়মটি নেটিভ মেথড ট্রিম হওয়া থেকে রক্ষা করে।

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

জাভা নেটিভ ইন্টারফেস (JNI) এর মাধ্যমে নেটিভ কোডের সাথে মিথস্ক্রিয়া

যখন আপনার অ্যাপ নেটিভ (C/C++) কোড থেকে জাভা বা কোটলিনে আপকল করার জন্য JNI ব্যবহার করে, তখন R8 দেখতে পায় না যে আপনার নেটিভ কোড থেকে কোন মেথডগুলো কল করা হচ্ছে। যদি আপনার অ্যাপে এই মেথডগুলোর কোনো সরাসরি রেফারেন্স না থাকে, তাহলে R8 ভুলভাবে ধরে নেয় যে এই মেথডগুলো অব্যবহৃত এবং সেগুলোকে সরিয়ে দেয়, যার ফলে আপনার অ্যাপ ক্র্যাশ করে।

নিম্নলিখিত উদাহরণটি একটি কোটলিন ক্লাস দেখায়, যেখানে এমন একটি মেথড রয়েছে যা একটি নেটিভ লাইব্রেরি থেকে কল করার জন্য তৈরি। নেটিভ লাইব্রেরিটি একটি অ্যাপ্লিকেশন টাইপ ইনস্ট্যানশিয়েট করে এবং নেটিভ কোড থেকে কোটলিন কোডে ডেটা পাস করে।

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);} : এটি একটি ক্লাসের নির্দিষ্ট মেম্বারগুলোকে শুধুমাত্র তখনই সংরক্ষণ করে, যখন ক্লাসটি কোটলিন বা জাভা কোড ফার্স্ট পদ্ধতিতে ইনস্ট্যানশিয়েট করা হয়—এটি R8-কে জানিয়ে দেয় যে অ্যাপটি ক্লাসটি ব্যবহার করছে এবং অ্যাপটির উচিত ক্লাসটির নির্দিষ্ট মেম্বারগুলোকে সংরক্ষণ করা।
    • -keepclassmembers : এটি কোনো ক্লাসের নির্দিষ্ট মেম্বারগুলোকে কেবল তখনই সংরক্ষণ করে, যখন ক্লাসটি কোটলিন বা জাভা কোড ফার্স্ট পদ্ধতিতে ইনস্ট্যানশিয়েট করা হয়—এটি R8-কে জানিয়ে দেয় যে অ্যাপটি ক্লাসটি ব্যবহার করছে এবং তাই ক্লাসটির নির্দিষ্ট মেম্বারগুলোকে সংরক্ষণ করা উচিত।
    • class com.example.JniBridge : এটি ফিল্ডটি ধারণকারী সুনির্দিষ্ট ক্লাসটিকে টার্গেট করে।
    • includedescriptorclasses : এই মডিফায়ারটি মেথডের সিগনেচার বা ডেসক্রিপ্টরে থাকা যেকোনো ক্লাসকেও অক্ষুণ্ণ রাখে। এক্ষেত্রে, এটি R8-কে com.example.models.NativeData ক্লাসটির নাম পরিবর্তন বা অপসারণ করা থেকে বিরত রাখে, যা একটি প্যারামিটার হিসেবে ব্যবহৃত হয়। যদি NativeData নাম পরিবর্তন করা হতো (উদাহরণস্বরূপ, aa তে), তাহলে মেথড সিগনেচারটি আর নেটিভ কোডের প্রত্যাশার সাথে মিলত না, যার ফলে ক্র্যাশ ঘটত।
    • public void onNativeEvent(com.example.models.NativeData); : এটি সংরক্ষণ করার জন্য মেথডটির সঠিক জাভা সিগনেচার নির্দিষ্ট করে।
  • -keep class NativeData{<init>(java.lang.Integer, java.lang.String);} : যদিও includedescriptorclasses নিশ্চিত করে যে NativeData ক্লাসটি নিজেই সংরক্ষিত থাকে, NativeData মধ্যে থাকা যেকোনো মেম্বার (ফিল্ড বা মেথড) যা আপনার নেটিভ JNI কোড থেকে সরাসরি অ্যাক্সেস করা হয়, সেগুলোর জন্য নিজস্ব keep নিয়মের প্রয়োজন হয়।
    • -keep class NativeData : এটি NativeData নামের ক্লাসটিকে টার্গেট করে এবং এই ব্লকটি নির্দিষ্ট করে দেয় যে NativeData ক্লাসের ভেতরের কোন মেম্বারগুলোকে রাখা হবে।
    • <init>(java.lang.Integer, java.lang.String) : এটি হলো কনস্ট্রাক্টরের সিগনেচার। এটি সেই কনস্ট্রাক্টরটিকে অনন্যভাবে শনাক্ত করে, যেটি দুটি প্যারামিটার গ্রহণ করে: প্রথমটি একটি Integer এবং দ্বিতীয়টি একটি String

পরোক্ষ প্ল্যাটফর্ম কল

Parcelable এর একটি বাস্তবায়নের মাধ্যমে ডেটা স্থানান্তর করুন

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

উদাহরণস্বরূপ, নিম্নলিখিত উদাহরণটি বিবেচনা করুন যেখানে একটি 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 গ্রেডল প্লাগইনটি আপনার @Parcelize দিয়ে অ্যানোটেট করা ক্লাসগুলোর জন্য প্রয়োজনীয় কিপ রুলগুলো স্বয়ংক্রিয়ভাবে তৈরি করে। এটি আপনার জন্য জটিলতা সামলে নেয় এবং নিশ্চিত করে যে তৈরি হওয়া CREATOR ও কনস্ট্রাক্টরগুলো অ্যান্ড্রয়েড ফ্রেমওয়ার্কের রিফ্লেকশন কলের জন্য সংরক্ষিত থাকে।

আপনি যদি @Parcelize ব্যবহার না করে কোটলিনে ম্যানুয়ালি একটি Parcelable ক্লাস লেখেন, তাহলে আপনাকে এর CREATOR ফিল্ড এবং Parcel গ্রহণকারী কনস্ট্রাক্টরটি অবশ্যই রাখতে হবে। এটি করতে ভুলে গেলে, সিস্টেম যখন আপনার অবজেক্টটিকে ডিসিরিয়ালাইজ করার চেষ্টা করে, তখন আপনার অ্যাপটি ক্র্যাশ করে। @Parcelize ব্যবহার করাই হলো আদর্শ এবং নিরাপদ পদ্ধতি।

kotlin-parcelize প্লাগইনটি ব্যবহার করার সময় নিম্নলিখিত বিষয়গুলি সম্পর্কে সচেতন থাকুন:

  • প্লাগইনটি কম্পাইলেশনের সময় স্বয়ংক্রিয়ভাবে CREATOR ফিল্ড তৈরি করে।
  • proguard-android-optimize.txt ফাইলটিতে সঠিক কার্যকারিতার জন্য এই ফিল্ডগুলো ধরে keep প্রয়োজনীয় নিয়মগুলো রয়েছে।
  • অ্যাপ ডেভেলপারদের অবশ্যই যাচাই করতে হবে যে সমস্ত প্রয়োজনীয় keep রুল উপস্থিত আছে কিনা, বিশেষ করে যেকোনো কাস্টম ইমপ্লিমেন্টেশন বা থার্ড-পার্টি ডিপেন্ডেন্সির ক্ষেত্রে।

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

তবে, জনপ্রিয় থার্ড-পার্টি লাইব্রেরিগুলো (যেমন Gson, Retrofit, এবং Kotlinx Serialization) স্বয়ংক্রিয়ভাবে তাদের নিজস্ব R8 কনজিউমার কিপ রুলস অন্তর্ভুক্ত করে। এই লাইব্রেরিগুলোর সাম্প্রতিক সংস্করণ ব্যবহার করার সময়, আপনার প্রোজেক্টে ম্যানুয়ালি কিপ রুলস যোগ করার প্রয়োজন নেই।

জিএসওএন

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 ) লক্ষ্য করে।

রেট্রোফিট

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

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

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

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

Retrofit 2.10.0 থেকে শুরু করে, লাইব্রেরিটি অ্যানোটেশনের ডিফল্ট, সার্ভিস মেথডের প্যারামিটার এবং প্রয়োজনীয় ক্লাস মেটাডেটা সংরক্ষণের জন্য আবশ্যক অফিসিয়াল কিপ রুলগুলো স্বয়ংক্রিয়ভাবে বান্ডল করে। আরও তথ্যের জন্য, Retrofit দ্বারা ব্যবহৃত রুলসমূহ দেখুন।

জেনেরিক রিটার্ন প্রকারগুলি সংরক্ষণ করুন

নেটওয়ার্ক রেসপন্সকে সঠিকভাবে ডিসিরিয়ালাইজ করার জন্য রেট্রোফিট রিটার্ন টাইপের জেনেরিক সিগনেচার (যেমন, Observable<Data> ) পরীক্ষা করে। যদি R8 জেনেরিক সিগনেচারটি বাদ দেয়, তাহলে রেট্রোফিট ইনস্ট্যানসিয়েটেড অবজেক্টটিকে 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>

যে প্রকৃত ডেটা মডেল ক্লাসটি রিটার্ন করা হচ্ছে (উদাহরণস্বরূপ, Observable<Data> এর মধ্যে Data ), সেটিও অবশ্যই রাখতে হবে, কারণ এটি কনভার্টার দ্বারা রিফ্লেক্টিভভাবে কনস্ট্রাক্ট করা হবে (যেমন Gson)।

কোরাউটিন

আপনি যখন কোটলিন কো-রুটিন ব্যবহার করেন, তখন কোটলিন কম্পাইলার কম্পাইল করা মেথড সিগনেচারের সাথে একটি Continuation প্যারামিটার যুক্ত করে suspend ফাংশনগুলোকে রূপান্তরিত করে।

যখন রেট্রোফিটের মতো লাইব্রেরিগুলো কোনো 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