R8 দুটি মোড প্রদান করে, কম্প্যাটিবিলিটি মোড এবং ফুল মোড। ফুল মোড আপনাকে শক্তিশালী অপটিমাইজেশন দেয় যা আপনার অ্যাপের পারফরম্যান্স উন্নত করে।
এই নির্দেশিকাটি সেইসব অ্যান্ড্রয়েড ডেভেলপারদের জন্য, যারা R8-এর সবচেয়ে শক্তিশালী অপটিমাইজেশনগুলো ব্যবহার করতে চান। এতে কম্প্যাটিবিলিটি ও ফুল মোডের মধ্যকার মূল পার্থক্যগুলো তুলে ধরা হয়েছে এবং আপনার প্রজেক্ট নিরাপদে মাইগ্রেট করতে ও সাধারণ রানটাইম ক্র্যাশগুলো এড়াতে প্রয়োজনীয় সুস্পষ্ট কনফিগারেশনগুলো প্রদান করা হয়েছে।
সম্পূর্ণ মোড সক্রিয় করুন
সম্পূর্ণ মোড চালু করতে, আপনার gradle.properties ফাইল থেকে নিম্নলিখিত লাইনটি মুছে ফেলুন:
android.enableR8.fullMode=false // Remove this line to enable full mode
অ্যাট্রিবিউটের সাথে যুক্ত ক্লাসগুলি ধরে রাখুন
অ্যাট্রিবিউট হলো কম্পাইল করা ক্লাস ফাইলের মধ্যে সংরক্ষিত মেটাডেটা, যা এক্সিকিউটেবল কোডের অংশ নয়। তবে, নির্দিষ্ট কিছু ধরনের রিফ্লেকশনের জন্য এগুলোর প্রয়োজন হতে পারে। সাধারণ উদাহরণগুলোর মধ্যে রয়েছে Signature (যা টাইপ ইরেজারের পরেও জেনেরিক টাইপের তথ্য সংরক্ষণ করে), InnerClasses ও EnclosingMethod (ক্লাসের গঠন নিয়ে রিফ্লেকশনের জন্য) এবং রানটাইমে দৃশ্যমান অ্যানোটেশন।
নিম্নলিখিত কোডটি দেখায় যে বাইটকোডে একটি ফিল্ডের জন্য Signature অ্যাট্রিবিউটটি কেমন দেখতে হয়। একটি ফিল্ডের জন্য:
List<User> users;
কম্পাইল করা ক্লাস ফাইলে নিম্নলিখিত বাইটকোড থাকবে:
.field public static final users:Ljava/util/List;
.annotation system Ldalvik/annotation/Signature;
value = {
"Ljava/util/List<",
"Lcom/example/package/User;",
">;"
}
.end annotation
.end field
যেসব লাইব্রেরি ব্যাপকভাবে রিফ্লেকশন ব্যবহার করে (যেমন Gson), তারা প্রায়শই আপনার কোডের কাঠামোকে গতিশীলভাবে পরিদর্শন ও বোঝার জন্য এই অ্যাট্রিবিউটগুলোর উপর নির্ভর করে। ডিফল্টরূপে R8-এর ফুল মোডে, অ্যাট্রিবিউটগুলো কেবল তখনই সংরক্ষিত হয়, যখন সংশ্লিষ্ট ক্লাস, ফিল্ড বা মেথডটি স্পষ্টভাবে রাখা হয়।
নিম্নলিখিত উদাহরণটি দেখায় কেন অ্যাট্রিবিউটগুলি প্রয়োজনীয় এবং কম্প্যাটিবিলিটি মোড থেকে ফুল মোডে মাইগ্রেট করার সময় আপনাকে কোন নিয়মগুলি রাখতে হবে। যে ক্লাস, ফিল্ড বা মেথডগুলি প্রতিফলিত হচ্ছে সেগুলি রাখার পাশাপাশি, আপনাকে অবশ্যই সেই অ্যাট্রিবিউটগুলিও স্পষ্টভাবে রাখতে হবে যার উপর সেগুলি নির্ভর করে।
নিম্নলিখিত উদাহরণটি বিবেচনা করুন যেখানে আমরা Gson লাইব্রেরি ব্যবহার করে ব্যবহারকারীদের একটি তালিকা ডিসিরিয়ালাইজ করছি।
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
data class User(
@SerializedName("username")
var username: String? = null,
@SerializedName("age")
var age: Int = 0
)
fun GsonRemoteJsonListExample() {
val gson = Gson()
// 1. The JSON string for a list of users returned from remote
val jsonOutput = """[{"username":"alice","age":30}, {"username":"bob","age":25}]"""
// 2. Deserialize the JSON string into a List<User>
// We must use TypeToken for generic types like List
val listType = object : TypeToken<List<User>>() {}.type
val deserializedList: List<User> = gson.fromJson(jsonOutput, listType)
// Print the list
println("First user from list: ${deserializedList}")
}
কম্পাইলেশনের সময়, জাভার টাইপ ইরেজার জেনেরিক টাইপ আর্গুমেন্টগুলো মুছে ফেলে। এর মানে হলো, রানটাইমে List<String> এবং List<User> উভয়ই একটি সাধারণ List হিসেবে প্রদর্শিত হয়। তাই, Gson-এর মতো লাইব্রেরিগুলো, যেগুলো রিফ্লেকশনের উপর নির্ভর করে, একটি JSON লিস্ট ডিসিরিয়ালাইজ করার সময় নির্ধারণ করতে পারে না যে List ঠিক কোন অবজেক্ট টাইপ ধারণ করার জন্য ডিক্লেয়ার করা হয়েছিল, যা রানটাইম সমস্যার কারণ হতে পারে।
টাইপ তথ্য সংরক্ষণ করার জন্য, Gson TypeToken ব্যবহার করে। TypeToken র্যাপ করলে প্রয়োজনীয় ডিসিরিয়ালাইজেশন তথ্য অক্ষুণ্ণ থাকে।
Kotlin এক্সপ্রেশন ` object:TypeToken<List<User>>() {}.type একটি অ্যানোনিমাস ইনার ক্লাস তৈরি করে যা TypeToken এক্সটেন্ড করে এবং জেনেরিক টাইপের তথ্য ধারণ করে। এই উদাহরণে, অ্যানোনিমাস ক্লাসটির নাম $GsonRemoteJsonListExample$listType$1 ।
জাভা প্রোগ্রামিং ভাষা একটি সুপারক্লাসের জেনেরিক সিগনেচারকে মেটাডেটা হিসেবে সংরক্ষণ করে, যা কম্পাইল করা ক্লাস ফাইলের মধ্যে Signature অ্যাট্রিবিউট নামে পরিচিত। এরপর TypeToken রানটাইমে টাইপটি পুনরুদ্ধার করার জন্য এই Signature মেটাডেটা ব্যবহার করে। এর ফলে Gson রিফ্লেকশন ব্যবহার করে Signature পড়তে পারে এবং ডিসিরিয়ালাইজেশনের জন্য প্রয়োজনীয় সম্পূর্ণ List<User> টাইপটি সফলভাবে খুঁজে বের করতে পারে।
যখন R8 কম্প্যাটিবিলিটি মোডে সক্রিয় করা হয়, তখন এটি ক্লাসগুলির জন্য Signature অ্যাট্রিবিউটটি ধরে রাখে, যার মধ্যে $GsonRemoteJsonListExample$listType$1 এর মতো অ্যানোনিমাস ইনার ক্লাসও অন্তর্ভুক্ত, এমনকি যদি নির্দিষ্ট কিপ রুলস স্পষ্টভাবে সংজ্ঞায়িত না করা হয়। ফলস্বরূপ, এই উদাহরণটি প্রত্যাশিতভাবে কাজ করার জন্য R8 কম্প্যাটিবিলিটি মোডে আর কোনো সুস্পষ্ট কিপ রুলসের প্রয়োজন হয় না।
// keep rule for compatibility mode
-keepattributes Signature
যখন R8 ফুল মোডে সক্রিয় করা হয়, তখন $GsonRemoteJsonListExample$listType$1 অ্যানোনিমাস ইনার ক্লাসের Signature অ্যাট্রিবিউটটি মুছে ফেলা হয়। Signature এ এই টাইপ তথ্য না থাকায়, Gson সঠিক অ্যাপ্লিকেশন টাইপ খুঁজে পায় না, যার ফলে একটি IllegalStateException দেখা দেয়।
আপনি যদি 2.11.0-এর চেয়ে পুরোনো কোনো Gson সংস্করণ ব্যবহার করেন, তবে এটি প্রতিরোধ করার জন্য প্রয়োজনীয় keep rules গুলো হলো:
// keep rule required for full mode
-keepattributes Signature
-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken { *; }
-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken
-keepattributes Signature: এই নিয়মটি R8-কে সেই অ্যাট্রিবিউটটি ধরে রাখতে নির্দেশ দেয় যা Gson-এর পড়ার প্রয়োজন। ফুল মোডে, R8 শুধুমাত্র সেইসব ক্লাস, ফিল্ড বা মেথডের জন্যSignatureঅ্যাট্রিবিউটটি ধরে রাখে, যেগুলোকে একটিkeepনিয়মের মাধ্যমে স্পষ্টভাবে মেলানো হয়।-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken: এই নিয়মটি প্রয়োজনীয় কারণTypeTokenযে অবজেক্টটিকে ডি-সিরিয়ালাইজ করা হচ্ছে তার টাইপকে র্যাপ করে। টাইপ ইরেজারের পরে, জেনেরিক টাইপের তথ্য ধরে রাখার জন্য একটি অ্যানোনিমাস ইনার ক্লাস তৈরি করা হয়।com.google.gson.reflect.TypeTokenস্পষ্টভাবে 'keep' না করলে, R8 তার ফুল মোডে ডি-সিরিয়ালাইজেশনের জন্য প্রয়োজনীয়Signatureঅ্যাট্রিবিউটে এই ক্লাস টাইপটিকে অন্তর্ভুক্ত করবে না।-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken: এই নিয়মটিTypeTokenএক্সটেন্ড করা অ্যানোনিমাস ক্লাসগুলির টাইপ তথ্য ধরে রাখে, যেমন এই উদাহরণে$GsonRemoteJsonListExample$listType$1। এই নিয়মটি ছাড়া, ফুল মোডে R8 প্রয়োজনীয় টাইপ তথ্য মুছে ফেলে, যার ফলে ডিসিরিয়ালাইজেশন ব্যর্থ হয়।
এটা বোঝা গুরুত্বপূর্ণ যে পূর্বে শেয়ার করা নিয়মগুলো শুধুমাত্র জেনেরিক টাইপ (উদাহরণস্বরূপ, List<User> ) খুঁজে বের করার সমস্যার সমাধান করে। R8 ক্লাসের ফিল্ডগুলোর নামও পরিবর্তন করে। আপনি যদি আপনার ডেটা মডেলে @SerializedName অ্যানোটেশন ব্যবহার না করেন, তাহলে Gson JSON ডিসিরিয়ালাইজ করতে ব্যর্থ হবে, কারণ ফিল্ডের নামগুলো আর JSON কী-গুলোর সাথে মিলবে না।
তবে, যদি আপনি 2.11-এর চেয়ে পুরোনো কোনো Gson সংস্করণ ব্যবহার করেন, অথবা যদি আপনার মডেলগুলিতে @SerializedName অ্যানোটেশনটি ব্যবহৃত না হয়, তাহলে আপনাকে অবশ্যই সেই মডেলগুলির জন্য সুস্পষ্ট keep নিয়ম যোগ করতে হবে।
ডিফল্ট কনস্ট্রাক্টর বজায় রাখুন
R8 ফুল মোডে, ক্লাসটি রিটেইন করা থাকলেও নো-আর্গস/ডিফল্ট কনস্ট্রাক্টরটি স্বয়ংক্রিয়ভাবে সংরক্ষিত হয় না। আপনি যদি class.getDeclaredConstructor().newInstance() বা class.newInstance() ব্যবহার করে কোনো ক্লাসের ইনস্ট্যান্স তৈরি করেন, তাহলে ফুল মোডে আপনাকে অবশ্যই নো-আর্গস কনস্ট্রাক্টরটি সুস্পষ্টভাবে রিটেইন করতে হবে। এর বিপরীতে, কম্প্যাটিবিলিটি মোড সর্বদা নো-আর্গস কনস্ট্রাক্টরটি রিটেইন করে।
এমন একটি উদাহরণ বিবেচনা করুন যেখানে রিফ্লেকশন ব্যবহার করে PrecacheTask এর একটি ইনস্ট্যান্স তৈরি করা হয় এর run মেথডটিকে ডাইনামিকভাবে কল করার জন্য। যদিও এই সিনারিওটির জন্য কম্প্যাটিবিলিটি মোডে অতিরিক্ত নিয়মের প্রয়োজন হয় না, ফুল মোডে PrecacheTask এর ডিফল্ট কনস্ট্রাক্টরটি সরিয়ে ফেলা হবে। তাই, একটি নির্দিষ্ট keep রুল প্রয়োজন।
// In library
interface StartupTask {
fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
fun execute(taskClass: Class<out StartupTask>) {
// The class isn't removed, but its constructor might be.
val task = taskClass.getDeclaredConstructor().newInstance()
task.run()
}
}
// In app
class PreCacheTask : StartupTask {
override fun run() {
Log.d("Pre cache task", "Warming up the cache...")
}
}
fun runTaskRunner() {
// The library is given a direct reference to the app's task class.
TaskRunner.execute(PreCacheTask::class.java)
}
# Full mode keep rule
# default constructor needs to be specified
-keep class com.example.fullmoder8.PreCacheTask {
<init>();
}
অ্যাক্সেস পরিবর্তন ডিফল্টরূপে সক্রিয় থাকে
কম্প্যাটিবিলিটি মোডে, R8 একটি ক্লাসের ভেতরের মেথড এবং ফিল্ডের ভিজিবিলিটি পরিবর্তন করে না। তবে, ফুল মোডে, R8 আপনার মেথড এবং ফিল্ডের ভিজিবিলিটি পরিবর্তন করে অপটিমাইজেশন বাড়ায়, যেমন—প্রাইভেট থেকে পাবলিক-এ পরিবর্তন করে। এর ফলে আরও বেশি ইনলাইনিং করা সম্ভব হয়।
এই অপ্টিমাইজেশনটি সমস্যা সৃষ্টি করতে পারে, যদি আপনার কোড এমন রিফ্লেকশন ব্যবহার করে যা মেম্বারদের একটি নির্দিষ্ট ভিজিবিলিটির উপর বিশেষভাবে নির্ভর করে। R8 এই পরোক্ষ ব্যবহারটি চিনতে পারবে না, যার ফলে অ্যাপ ক্র্যাশ হওয়ার সম্ভাবনা থাকে। এটি প্রতিরোধ করার জন্য, আপনাকে মেম্বারদের সংরক্ষণ করতে নির্দিষ্ট -keep রুল যোগ করতে হবে, যা তাদের মূল ভিজিবিলিটিও বজায় রাখবে।
আরও তথ্যের জন্য, এই উদাহরণটি দেখুন, যা আপনাকে বুঝতে সাহায্য করবে কেন রিফ্লেকশন ব্যবহার করে প্রাইভেট মেম্বার অ্যাক্সেস করা উচিত নয় এবং সেই ফিল্ড/মেথডগুলো ধরে রাখার নিয়মগুলো কী।
কোটলিন-নির্দিষ্ট মেটাডেটা
কোটলিন কোড কম্পাইল করার সময়, কোটলিন কম্পাইলার প্রতিটি ক্লাস ফাইলে @kotlin.Metadata অ্যানোটেশনের মধ্যে ভাষা-নির্দিষ্ট মেটাডেটা (যেমন নালিবিলিটি, এক্সটেনশন ফাংশন এবং কো-রুটিন সিগনেচার) সংরক্ষণ করে।
যদি আপনার অ্যাপ বা এর ডিপেন্ডেন্সিগুলো কোটলিন রিফ্লেকশন ( kotlin.reflect ) ব্যবহার করে, তাহলে রিফ্লেকশন লাইব্রেরিটি ক্লাসের গঠন পরীক্ষা করার জন্য রানটাইমে এই মেটাডেটা পার্স করে। R8 ফুল মোডে, অ্যানোটেশনগুলো স্পষ্টভাবে রাখা না হলে R8 ডিফল্টভাবে সেগুলো সরিয়ে দেয়। এছাড়াও, R8 যদি মেটাডেটা সংরক্ষণ ও আপডেট না করে আপনার ক্লাসগুলোকে মিনিফাই বা সঙ্কুচিত করে, তাহলে রানটাইমে কোটলিন রিফ্লেকশন ব্যর্থ হবে, যার ফলে অপ্রত্যাশিত আচরণ বা ক্র্যাশ (যেমন KotlinReflectionInternalError ) হতে পারে।
অপ্রত্যাশিত আচরণ রোধ করতে এবং মিনিফিকেশনের পরে কোটলিন রিফ্লেকশন সঠিকভাবে কাজ করে তা নিশ্চিত করতে, আপনাকে অবশ্যই রানটাইম-দৃশ্যমান অ্যানোটেশনগুলি রাখতে হবে এবং kotlin.Metadata ক্লাসটি স্পষ্টভাবে সংরক্ষণ করতে হবে:
# Preserve runtime-visible annotations required for inspecting metadata
-keepattributes RuntimeVisibleAnnotations
# Keep Kotlin metadata to ensure kotlin.reflect functions correctly
-keep class kotlin.Metadata { *; }