تفعيل إمكانات أداة التحسين R8 الكاملة

توفّر أداة R8 وضعَين، هما وضع التوافق والوضع الكامل. يمنحك الوضع الكامل تحسينات فعّالة تعمل على تحسين أداء تطبيقك.

هذا الدليل مخصّص لمطوّري تطبيقات Android الذين يريدون استخدام أقوى عمليات التحسين التي يوفّرها 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، لا يتم الاحتفاظ بالسمات تلقائيًا إلا إذا تم الاحتفاظ بشكل صريح بالفئة أو الحقل أو الطريقة المرتبطة.

يوضّح المثال التالي سبب ضرورة استخدام السمات وقواعد keep التي يجب إضافتها عند الانتقال من وضع التوافق إلى الوضع الكامل.

لنأخذ المثال التالي الذي نلغي فيه تسلسل قائمة المستخدمين باستخدام مكتبة 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}")
}

أثناء التجميع، تزيل ميزة "محو الأنواع" في Java وسيطات الأنواع العامة. وهذا يعني أنّه في وقت التشغيل، يظهر كل من List<String> وList<User> كـ List أولي. لذلك، لا يمكن للمكتبات، مثل Gson، التي تعتمد على الانعكاس، تحديد أنواع العناصر المحدّدة التي تم الإعلان عن أنّ List يحتوي عليها عند إلغاء تسلسل قائمة JSON، ما قد يؤدي إلى حدوث مشاكل أثناء وقت التشغيل.

للاحتفاظ بمعلومات النوع، يستخدم Gson TypeToken. يحتفظ التغليف TypeToken بمعلومات إلغاء التسلسل اللازمة.

ينشئ تعبير Kotlin object:TypeToken<List<User>>() {}.type فئة داخلية مجهولة الهوية توسّع TypeToken وتلتقط معلومات النوع العام. في هذا المثال، اسم الفئة المجهولة هو $GsonRemoteJsonListExample$listType$1.

تحفظ لغة البرمجة Java التوقيع العام لفئة أساسية كبيانات وصفية، تُعرف باسم السمة Signature، ضمن ملف الفئة المترجَم. ثم تستخدم TypeToken بيانات Signature الوصفية هذه لاسترداد النوع في وقت التشغيل. يتيح ذلك لـ Gson استخدام الانعكاس لقراءة Signature والتعرّف بنجاح على نوع List<User> الكامل الذي يحتاج إليه لإلغاء التسلسل.

عند تفعيل R8 في وضع التوافق، يحتفظ بالسمة Signature للفئات، بما في ذلك الفئات الداخلية المجهولة مثل $GsonRemoteJsonListExample$listType$1، حتى إذا لم يتم تحديد قواعد الحفاظ على الفئات بشكل صريح. نتيجةً لذلك، لا يتطلّب وضع التوافق مع R8 أي قواعد إبقاء صريحة أخرى ليعمل هذا المثال على النحو المتوقّع.

// keep rule for compatibility mode
-keepattributes Signature

عند تفعيل R8 في الوضع الكامل، تتم إزالة السمة Signature لفئة $GsonRemoteJsonListExample$listType$1 الداخلية المجهولة. بدون معلومات النوع هذه في Signature، لا يمكن لـ Gson العثور على نوع التطبيق الصحيح، ما يؤدي إلى حدوث IllegalStateException. قواعد الاحتفاظ اللازمة لمنع حدوث ذلك هي:

// 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 بشكل صريح، لن يتضمّن R8 في الوضع الكامل نوع الفئة هذا في السمة Signature المطلوبة لإلغاء التسلسل.

  • -keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken: تحتفظ هذه القاعدة بمعلومات النوع للفئات المجهولة المصدر التي توسّع TypeToken، مثل $GsonRemoteJsonListExample$listType$1 في هذا المثال. بدون هذه القاعدة، يزيل R8 في الوضع الكامل معلومات النوع الضرورية، ما يؤدي إلى تعذُّر إلغاء التسلسل.

بدءًا من الإصدار 2.11.0 من Gson، تتضمّن المكتبة حِزمًا من قواعد الحفاظ الضرورية المطلوبة لإلغاء التسلسل في الوضع الكامل. عند إنشاء تطبيقك مع تفعيل R8، يعثر R8 تلقائيًا على هذه القواعد ويطبّقها من المكتبة. ويوفّر ذلك الحماية التي يحتاجها تطبيقك بدون الحاجة إلى إضافة هذه القواعد المحدّدة أو صيانتها يدويًا في مشروعك.

من المهم أن ندرك أنّ القواعد التي تمت مشاركتها سابقًا لا تحلّ سوى مشكلة اكتشاف النوع العام (مثل ‫List<User>). يعيد R8 أيضًا تسمية حقول الفئات. إذا لم تستخدِم تعليقات توضيحية في نماذج البيانات، سيتعذّر على Gson إلغاء تسلسل JSON لأنّ أسماء الحقول لن تتطابق مع مفاتيح JSON.@SerializedName

ومع ذلك، إذا كنت تستخدم إصدارًا من Gson أقدم من 2.11، أو إذا كانت نماذجك لا تستخدم التعليق التوضيحي @SerializedName، عليك إضافة قواعد إبقاء صريحة لهذه النماذج.

الاحتفاظ بالدالة الإنشائية التلقائية

في وضع R8 الكامل، لا يتم الاحتفاظ تلقائيًا بالدالة الإنشائية التلقائية أو التي لا تتضمّن وسيطات، حتى إذا تم الاحتفاظ بالفئة نفسها. إذا كنت تنشئ مثيلاً لفئة باستخدام class.getDeclaredConstructor().newInstance() أو class.newInstance()، عليك الاحتفاظ بشكل صريح بالدالة الإنشائية التي لا تتضمّن وسيطات في الوضع الكامل. في المقابل، يحتفظ وضع التوافق دائمًا بالدالة الإنشائية التي لا تتضمّن وسيطات.

لنفترض مثالاً يتم فيه إنشاء مثيل من PrecacheTask باستخدام الانعكاس لاستدعاء الطريقة run بشكل ديناميكي. مع أنّ هذا السيناريو لا يتطلّب قواعد إضافية في وضع التوافق، سيتم في الوضع الكامل إزالة الدالة الإنشائية التلقائية لـ PrecacheTask. لذلك، يجب توفّر قاعدة احتفاظ محدّدة.

// 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 محدّدة للحفاظ على الأعضاء، ما سيؤدي أيضًا إلى الحفاظ على مستوى ظهورهم الأصلي.

لمزيد من المعلومات، يمكنك الاطّلاع على هذا المثال لفهم سبب عدم يُنصح بالوصول إلى الأعضاء الخاصين باستخدام الانعكاس وقواعد الاحتفاظ التي يجب اتّباعها للاحتفاظ بهذه الحقول/الطرق.