تمام پتانسیل بهینه‌ساز R8 را فعال کنید

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

کتابخانه‌هایی که به شدت از reflection استفاده می‌کنند (مانند 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 که به بازتاب متکی هستند، نمی‌توانند انواع شیء خاصی را که List هنگام deserialize کردن یک لیست JSON شامل می‌شود، تعیین کنند، که می‌تواند منجر به مشکلات زمان اجرا شود.

برای حفظ اطلاعات نوع، Gson از TypeToken استفاده می‌کند. بسته‌بندی TypeToken اطلاعات لازم برای deserialization را حفظ می‌کند.

عبارت Kotlin به نام object:TypeToken<List<User>>() {}.type یک کلاس داخلی ناشناس ایجاد می‌کند که TypeToken ارث‌بری می‌کند و اطلاعات نوع ژنریک را دریافت می‌کند. در این مثال، کلاس ناشناس $GsonRemoteJsonListExample$listType$1 نام دارد.

زبان برنامه‌نویسی جاوا امضای عمومی یک کلاس بالا را به عنوان فراداده، که به عنوان ویژگی Signature شناخته می‌شود، در فایل کلاس کامپایل شده ذخیره می‌کند. سپس TypeToken از این فراداده Signature برای بازیابی نوع در زمان اجرا استفاده می‌کند. این به Gson اجازه می‌دهد تا از reflection برای خواندن Signature استفاده کند و با موفقیت نوع کامل List<User> مورد نیاز برای deserialization را کشف کند.

وقتی R8 در حالت سازگاری فعال باشد، ویژگی Signature را برای کلاس‌ها، از جمله کلاس‌های داخلی ناشناس مانند $GsonRemoteJsonListExample$listType$1 ، حفظ می‌کند، حتی اگر قوانین keep خاص به صراحت تعریف نشده باشند. در نتیجه، حالت سازگاری R8 برای اینکه این مثال طبق انتظار کار کند، نیازی به هیچ قانون keep صریح دیگری ندارد.

// keep rule for compatibility mode
-keepattributes Signature

وقتی R8 در حالت کامل فعال می‌شود، ویژگی Signature از کلاس داخلی ناشناس $GsonRemoteJsonListExample$listType$1 حذف می‌شود. بدون این اطلاعات نوع در Signature ، Gson نمی‌تواند نوع برنامه صحیح را پیدا کند، که منجر به خطای IllegalStateException می‌شود. قوانین keep لازم برای جلوگیری از این امر عبارتند از:

// 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 نوع شیء در حال deserialization را در بر می‌گیرد. پس از پاک کردن نوع، یک کلاس داخلی ناشناس برای حفظ اطلاعات نوع عمومی ایجاد می‌شود. بدون نگه داشتن صریح com.google.gson.reflect.TypeToken ، R8 در حالت کامل، این نوع کلاس را در ویژگی Signature مورد نیاز برای deserialization قرار نمی‌دهد.

  • -keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken : این قانون اطلاعات نوع کلاس‌های ناشناسی که TypeToken ارث‌بری می‌کنند، مانند $GsonRemoteJsonListExample$listType$1 در این مثال، را حفظ می‌کند. بدون این قانون، R8 در حالت کامل، اطلاعات نوع لازم را حذف می‌کند و باعث می‌شود deserialization با شکست مواجه شود.

با شروع از نسخه ۲.۱۱.۰ Gson، بسته‌های کتابخانه‌ای لازم، قوانین مورد نیاز برای deserialization را در حالت کامل نگه می‌دارند . وقتی برنامه خود را با R8 فعال می‌سازید، R8 به طور خودکار این قوانین را از کتابخانه پیدا کرده و اعمال می‌کند. این امر محافظتی را که برنامه شما نیاز دارد، بدون نیاز به اضافه کردن یا حفظ دستی این قوانین خاص در پروژه شما، فراهم می‌کند.

درک این نکته مهم است که قوانینی که قبلاً به اشتراک گذاشته شدند، فقط مشکل کشف نوع عمومی (مثلاً List<User> ) را حل می‌کنند. R8 همچنین فیلدهای کلاس‌ها را تغییر نام می‌دهد. اگر از حاشیه‌نویسی‌های @SerializedName در مدل‌های داده خود استفاده نکنید، Gson در deserialize کردن JSON با شکست مواجه خواهد شد زیرا نام فیلدها دیگر با کلیدهای JSON مطابقت نخواهند داشت.

با این حال، اگر از نسخه Gson قدیمی‌تر از ۲.۱۱ استفاده می‌کنید، یا اگر مدل‌های شما از حاشیه‌نویسی @SerializedName استفاده نمی‌کنند، باید قوانین keep را به طور صریح برای آن مدل‌ها اضافه کنید.

سازنده پیش‌فرض را حفظ کنید

در حالت کامل R8، سازنده‌ی بدون آرگومان/پیش‌فرض به طور ضمنی حفظ نمی‌شود، حتی زمانی که خود کلاس حفظ می‌شود. اگر در حال ایجاد نمونه‌ای از یک کلاس با استفاده از class.getDeclaredConstructor().newInstance() یا class.newInstance() هستید، باید صریحاً سازنده‌ی بدون آرگومان را در حالت کامل حفظ کنید. در مقابل، حالت سازگاری همیشه سازنده‌ی بدون آرگومان را حفظ می‌کند.

مثالی را در نظر بگیرید که در آن یک نمونه از PrecacheTask با استفاده از reflection برای فراخوانی پویای متد 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 با تغییر قابلیت مشاهده متدها و فیلدهای شما، به عنوان مثال، از خصوصی به عمومی، بهینه‌سازی را بهبود می‌بخشد. این امر امکان درون‌خطی‌سازی بیشتری را فراهم می‌کند.

اگر کد شما از reflection استفاده کند که به طور خاص به اعضایی با قابلیت مشاهده خاص متکی است، این بهینه‌سازی می‌تواند مشکلاتی ایجاد کند. R8 این استفاده غیرمستقیم را تشخیص نمی‌دهد و به طور بالقوه منجر به خرابی برنامه می‌شود. برای جلوگیری از این امر، باید قوانین خاص -keep را برای حفظ اعضا اضافه کنید، که این امر باعث حفظ قابلیت مشاهده اصلی آنها نیز می‌شود.

برای اطلاعات بیشتر، به این مثال مراجعه کنید تا بفهمید چرا دسترسی به اعضای خصوصی با استفاده از reflection توصیه نمی‌شود و قوانین keep برای حفظ آن فیلدها/متدها چیست.