توفّر أداة 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، يتم الاحتفاظ بالسمات فقط إذا تم الاحتفاظ بالفئة أو الحقل أو الطريقة المرتبطة بشكل صريح.
يوضّح المثال التالي سبب ضرورة استخدام السمات وقواعد الاحتفاظ التي عليك إضافتها عند الانتقال من وضع التوافق إلى الوضع الكامل. بالإضافة إلى الاحتفاظ بالفئات أو الحقول أو الطرق التي يتم الانعكاس عليها، عليك أيضًا الاحتفاظ بشكل صريح بالسمات التي تعتمد عليها.
لنأخذ المثال التالي حيث نزيل تسلسل قائمة بالمستخدمين باستخدام مكتبة 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.
إذا كنت تستخدم إصدارًا أقدم من 2.11.0 من Gson، فإنّ قواعد الاحتفاظ اللازمة لمنع حدوث ذلك هي:
// 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 في الوضع الكامل معلومات النوع اللازمة، ما يؤدي إلى فشل إزالة التسلسل.
من المهم أن تفهم أنّ القواعد التي تمت مشاركتها سابقًا لا تحلّ إلا مشكلة اكتشاف النوع العام (على سبيل المثال، List<User>). يعيد R8 أيضًا
تسمية حقول الفئات. إذا لم تستخدم التعليقات التوضيحية @SerializedName في نماذج البيانات، سيتعذّر على Gson إزالة تسلسل JSON لأنّ أسماء الحقول لن تتطابق بعد ذلك مع مفاتيح JSON.
@SerializedName على الحقول، لن تحتاج إلى تحديد قواعد احتفاظ إضافية للنماذج.
ومع ذلك، إذا كنت تستخدم إصدارًا أقدم من 2.11 من Gson، أو إذا لم تستخدم نماذجك التعليق التوضيحي @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 محدّدة للاحتفاظ بالأعضاء، ما سيؤدي أيضًا إلى الاحتفاظ بإذن الوصول الأصلي لهم.
لمزيد من المعلومات، يُرجى الاطّلاع على هذا المثال لفهم سبب عدم ننصح بالوصول إلى الأعضاء الخاصين باستخدام الانعكاس وقواعد الاحتفاظ بهذه الحقول أو الطرق.
البيانات الوصفية الخاصة بلغة Kotlin
عند تجميع رمز Kotlin البرمجي، يخزّن برنامج تجميع Kotlin بيانات وصفية خاصة باللغة (مثل إمكانية قبول القيم الفارغة، ودوال الإضافة، وتوقيعات الكوروتين) في علامة توضيح @kotlin.Metadata على كل ملف فئة.
إذا كان تطبيقك أو التبعيات التابعة له يستخدم الانعكاس في Kotlin (kotlin.reflect)، تحلّل مكتبة الانعكاس هذه البيانات الوصفية في وقت التشغيل لفحص بنية الفئة.
في الوضع الكامل من R8، يزيل R8 التعليقات التوضيحية تلقائيًا إذا لم يتم الاحتفاظ بها بشكل صريح. أيضًا، إذا كان R8 يقلّل حجم الفئات أو يزيلها بدون الاحتفاظ بالبيانات الوصفية وتعديلها، سيفشل الانعكاس في Kotlin في وقت التشغيل، ما يؤدي إلى سلوك غير متوقّع أو أعطال (مثل KotlinReflectionInternalError).
لمنع حدوث سلوك غير متوقّع وضمان عمل الانعكاس في Kotlin بشكل صحيح بعد تقليل الحجم، عليك الاحتفاظ بالتعليقات التوضيحية المرئية في وقت التشغيل والاحتفاظ بشكل صريح بالفئة 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 { *; }