تستند الأمثلة التالية إلى سيناريوهات شائعة تستخدم فيها R8 لتحسين الأداء، ولكنك تحتاج إلى إرشادات متقدّمة لصياغة قواعد الاحتفاظ.
الانعكاس
بشكل عام، لا يُنصح باستخدام الانعكاس لتحقيق الأداء الأمثل. ومع ذلك، قد يكون ذلك أمرًا لا يمكن تجنّبه في بعض السيناريوهات. تقدّم الأمثلة التالية إرشادات حول قواعد الاحتفاظ في السيناريوهات الشائعة التي تستخدم الانعكاس.
انعكاس مع تحميل الفئات حسب الاسم
غالبًا ما يتم تحميل الفئات ديناميكيًا في المكتبات باستخدام اسم الفئة كـ 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>();
}
تم تصميم هذه القواعد لتعمل بشكل مثالي مع هذا النوع من الانعكاس، ما يتيح تحقيق أقصى قدر من التحسين مع ضمان عمل الرمز بشكل صحيح. تسمح القواعد لأداة R8 بإخفاء اسم الفئة وتقليل حجم تنفيذ الفئة StartupTask أو إزالته إذا لم يستخدمه التطبيق مطلقًا. ومع ذلك، في أي عملية تنفيذ، مثل 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>هو الاسم الداخلي الخاص للدالة الإنشائية في رمز بايت Java. يستهدف هذا الجزء بشكل خاص الدالة الإنشائية التلقائية التي لا تتضمّن وسيطًا. - هذه القاعدة مهمة لأنّ الرمز البرمجي يستدعي
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>;: يؤدي ذلك إلى الاحتفاظ بأي فئة تحتوي على طريقة واحدة أو أكثر (<methods>) تم وضع تعليق توضيحي عليها باستخدام@com.example.library.OnEvent، كما يؤدي إلى الاحتفاظ بالطرق التي تم وضع تعليق توضيحي عليها.
التفكير الذاتي استنادًا إلى التعليقات التوضيحية في الصف
يمكن للمكتبات استخدام الانعكاس للبحث عن الفئات التي تتضمّن تعليقًا توضيحيًا محدّدًا. في هذه الحالة، يعثر صف مشغّل المهام على جميع الصفوف التي تمّت إضافة التعليق التوضيحي 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 العناصر التي يجب الحفاظ عليها.
Reflection to support optional dependencies
تتمثل إحدى حالات الاستخدام الشائعة للانعكاس في إنشاء تبعية غير مباشرة بين مكتبة أساسية ومكتبة إضافية اختيارية. يمكن للمكتبة الأساسية التحقّق مما إذا كانت الإضافة مضمّنة في التطبيق، وإذا كانت كذلك، يمكنها تفعيل ميزات إضافية. يتيح لك ذلك إرسال وحدات إضافية بدون فرض اعتماد مباشر على المكتبة الأساسية.
تستخدم المكتبة الأساسية ميزة الانعكاس (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 من المكتبة الاختيارية لأنّ ما مِن أي شيء في هذا الوحدة يستخدمها مباشرةً. تحافظ قاعدة Keep على الفئة والدالة الإنشائية الخاصة بها، ما يتيح للمكتبة الأساسية إنشاء مثيل لها بنجاح.
Reflection to access private members
يمكن أن يؤدي استخدام الانعكاس للوصول إلى رمز خاص أو محمي غير مضمّن في واجهة برمجة التطبيقات العامة لإحدى المكتبات إلى حدوث مشاكل كبيرة. ويخضع هذا الرمز البرمجي للتغيير بدون إشعار، ما قد يؤدي إلى حدوث سلوك غير متوقع أو أعطال في تطبيقك.
عند الاعتماد على الانعكاس لواجهات برمجة التطبيقات غير العامة، قد تواجه المشاكل التالية:
- التحديثات المحظورة: يمكن أن تمنع التغييرات في الرمز الخاص أو المحمي الترقية إلى إصدارات أحدث من المكتبة.
- عدم الاستفادة من المزايا: قد لا تستفيد من الوظائف الجديدة أو إصلاحات الأعطال المهمة أو تحديثات الأمان الأساسية.
تحسينات 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
}
أضِف قاعدة -keep في تطبيقك لمنع R8 من إزالة الحقل الخاص:
-keepclassmembers class com.example.LibraryClass {
private java.lang.String secretMessage;
}
-keepclassmembers: يتم الاحتفاظ بأعضاء محددين في صف معيّن فقط في حال تم الاحتفاظ بالصف نفسه.-
class com.example.LibraryClass: يستهدف هذا النوع الفئة المحدّدة التي تحتوي على الحقل. -
private java.lang.String secretMessage;: يحدّد هذا الحقل الخاص المحدّد من خلال اسمه ونوعه.
واجهة Java الأصلية (JNI)
قد تحدث مشاكل في عمليات التحسين التي يجريها R8 عند استخدام عمليات استدعاء من الرمز البرمجي الأصلي (C/C++) إلى Java أو Kotlin. مع أنّ العكس صحيح أيضًا، أي أنّ عمليات الاستدعاء من Java أو Kotlin إلى الرمز البرمجي الأصلي يمكن أن تواجه مشاكل، إلّا أنّ الملف التلقائي proguard-android-optimize.txt يتضمّن القاعدة التالية للحفاظ على عمل عمليات الاستدعاء. تحمي هذه القاعدة من إزالة الطرق الأصلية.
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
التفاعل مع الرموز البرمجية الأصلية من خلال Java Native Interface (JNI)
عندما يستخدم تطبيقك واجهة JNI لإجراء عمليات استدعاء من الرمز البرمجي الأصلي (C/C++) إلى Java أو Kotlin، لا يمكن لأداة R8 معرفة الطرق التي يتم استدعاؤها من الرمز البرمجي الأصلي. إذا لم تكن هناك مراجع مباشرة لهذه الطرق في تطبيقك، يفترض R8 بشكل غير صحيح أنّ هذه الطرق غير مستخدَمة ويزيلها، ما يؤدي إلى تعطُّل تطبيقك.
يوضّح المثال التالي فئة Kotlin تتضمّن طريقة مخصّصة للاستدعاء من مكتبة مجمّعة من رموز برمجية أصلية. تنشئ المكتبة المجمَّعة من رموز برمجية أصلية نوع تطبيق وتمرِّر البيانات من الرموز البرمجية الأصلية إلى رموز Kotlin البرمجية.
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);}: يؤدي ذلك إلى الحفاظ على أعضاء محدّدين من فئة معيّنة فقط إذا تم إنشاء مثيل للفئة في رمز Kotlin أو Java أولاً، ما يشير إلى أداة R8 بأنّ التطبيق يستخدم الفئة وأنّه يجب الحفاظ على أعضاء محدّدين من الفئة.-
-keepclassmembers: يحافظ هذا الخيار على عناصر معيّنة من الفئة فقط إذا تم إنشاء مثيل للفئة في رمز Kotlin أو Java أولاً، ويشير إلى R8 بأنّ التطبيق يستخدم الفئة وأنّه يجب الحفاظ على عناصر معيّنة من الفئة. -
class com.example.JniBridge: يستهدف هذا النوع الفئة المحدّدة التي تحتوي على الحقل. includedescriptorclasses: يحافظ هذا المعدِّل أيضًا على أي فئات موجودة في توقيع الطريقة أو واصفها. في هذه الحالة، يمنع R8 إعادة تسمية الفئةcom.example.models.NativeDataأو إزالتها، وهي الفئة المستخدَمة كمعلَمة. إذا تمت إعادة تسميةNativeData(على سبيل المثال، إلىa.a)، لن يتطابق توقيع الطريقة مع ما يتوقّعه الرمز البرمجي الأصلي، ما يؤدي إلى حدوث عطل.public void onNativeEvent(com.example.models.NativeData);: يحدّد هذا العنصر توقيع Java الدقيق للطريقة التي سيتم الاحتفاظ بها.
-
-keep class NativeData{<init>(java.lang.Integer, java.lang.String);}: في حين يضمنincludedescriptorclassesالحفاظ على فئةNativeDataنفسها، يجب أن تتضمّن أي عناصر (حقول أو طرق) داخلNativeDataيتم الوصول إليها مباشرةً من رمز JNI الأصلي قواعد الحفاظ الخاصة بها.-keep class NativeData: يستهدف هذا العنصر الفئة المسماةNativeDataويحدّد الحظر الأعضاء الذين يجب الاحتفاظ بهم داخل الفئةNativeData.<init>(java.lang.Integer, java.lang.String): هذا هو توقيع الدالة الإنشائية. يحدّد هذا الرمز بشكل فريد الدالة الإنشائية التي تأخذ مَعلمتَين، الأولى هيIntegerوالثانية هيString.
المكالمات غير المباشرة على المنصة
نقل البيانات باستخدام عملية تنفيذ Parcelable
يستخدم إطار عمل Android الانعكاس لإنشاء مثيلات من Parcelableالكائنات. في عملية تطوير Kotlin الحديثة، يجب استخدام المكوّن الإضافي kotlin-parcelize الذي ينشئ تلقائيًا عملية تنفيذ Parcelable اللازمة، بما في ذلك الحقل CREATOR والطُرق التي يحتاجها إطار العمل.
على سبيل المثال، إليك المثال التالي حيث يتم استخدام المكوّن الإضافي kotlin-parcelize لإنشاء فئة Parcelable:
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مكوّن Gradle الإضافي تلقائيًا قواعد الاحتفاظ المطلوبة للفئات التي تضيف إليها التعليق التوضيحي @Parcelize. يتعامل هذا المحوّل مع التعقيد نيابةً عنك، ويحرص على الاحتفاظ بالرمز CREATOR والدوال الإنشائية لطلبات الانعكاس في إطار عمل Android.
إذا كتبت فئة Parcelable يدويًا في Kotlin بدون استخدام @Parcelize،
تكون مسؤولاً عن الاحتفاظ بالحقل CREATOR والدالة الإنشائية التي
تقبل Parcel. ويؤدي عدم إجراء ذلك إلى تعطُّل تطبيقك عندما يحاول النظام إلغاء تسلسل العنصر. استخدام @Parcelize هو الإجراء العادي والأكثر أمانًا.
عند استخدام المكوّن الإضافي kotlin-parcelize، يُرجى الانتباه إلى ما يلي:
- ينشئ المكوّن الإضافي تلقائيًا حقول
CREATORأثناء التجميع. - يحتوي الملف
proguard-android-optimize.txtعلى قواعدkeepالضرورية للاحتفاظ بهذه الحقول من أجل ضمان الأداء السليم. - على مطوّري التطبيقات التأكّد من توفّر جميع قواعد
keepالمطلوبة، خاصةً في ما يتعلّق بأي عمليات تنفيذ مخصّصة أو تبعيات تابعة لجهات خارجية.
المكتبات الرائجة
تصل المكتبات التي تستخدم الانعكاس أو عمليات تحويل رمز البايت إلى الرمز بشكل ديناميكي في وقت التشغيل. إذا أزالت أداة R8 أو أعادت تسمية الفئات أو الحقول أو الطرق التي يتم الوصول إليها بهذه الطريقة، قد يتعطّل تطبيقك.
ومع ذلك، فإنّ المكتبات الشائعة التابعة لجهات خارجية (مثل Gson وRetrofit وKotlinx Serialization) تضمّ تلقائيًا قواعد الاحتفاظ بالمستهلكين في R8. عند استخدام أحدث إصدارات هذه المكتبات، لن تحتاج إلى إضافة قواعد الاحتفاظ يدويًا إلى مشروعك.
Gson
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
تتجاهل مكتبة Gson الحقول التي تحمل المعدِّل transient أثناء عملية التسلسل وإلغاء التسلسل، ولهذا السبب تستهدف قاعدة الاحتفاظ الحقول غير المؤقتة (!transient) على وجه التحديد.
التحديث
Retrofit هي مكتبة شبكات تفحص طرق واجهة الخدمة
المشروحة باستخدام تعليقات توضيحية خاصة ببروتوكول HTTP (مثل @GET أو @POST) باستخدام الانعكاس
لإنشاء طلبات الشبكة وتحويل الردود.
تنشئ Retrofit بشكل ديناميكي عمليات تنفيذ لواجهات برمجة التطبيقات في وقت التشغيل باستخدام Proxy.newProxyInstance(). وبما أنّ R8 لا يرى أي فئة تنفّذ هذه الواجهات بشكل ثابت، قد يزيل الطرق أو أنواع الإرجاع العامة الخاصة بها.
قواعد الاحتفاظ المجمّعة
تعتمد Retrofit على انعكاس وقت التشغيل لفحص المَعلمات العامة والتعليقات التوضيحية الخاصة بالطرق والمَعلمات. بدون إعدادات صحيحة، يمكن أن يزيل وضع R8 الكامل التواقيع العامة من أنواع الإرجاع وعمليات الاستمرار في Kotlin وفئات الاستجابة، أو حتى يستبدل قيم الواجهة بقيمة فارغة، لأنّه يتم إنشاء واجهات Retrofit بشكل ديناميكي باستخدام وكيل.
بدءًا من الإصدار 2.10.0 من Retrofit، تجمّع المكتبة تلقائيًا قواعد keep الرسمية المطلوبة للحفاظ على القيم التلقائية للتعليقات التوضيحية ومعلَمات طرق الخدمة والبيانات الوصفية الضرورية للفئات. لمزيد من المعلومات، اطّلِع على القواعد التي تستخدمها Retrofit.
الحفاظ على أنواع الإرجاع العامة
يفحص Retrofit التوقيع العام لنوع القيمة التي تم إرجاعها (على سبيل المثال،
Observable<Data>) لإلغاء تسلسل استجابة الشبكة بشكل صحيح. إذا أزالت أداة R8 التوقيع العام، ستستبدل أداة Retrofit الكائن الذي تم إنشاء مثيل له بالقيمة 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>
يجب أيضًا الاحتفاظ بفئة نموذج البيانات الفعلي الذي يتم عرضه (على سبيل المثال، Data في Observable<Data>)، لأنّ المحوّل البرمجي (مثل Gson) سينشئها بشكل انعكاسي.
الكوروتينات
عند استخدام إجراءات Kotlin الفرعية، يحوّل برنامج الترجمة البرمجية في Kotlin دوال suspend
من خلال إضافة المَعلمة Continuation إلى توقيع الطريقة المترجمة.
عندما تقرأ مكتبات مثل Retrofit بشكل انعكاسي التوقيع العام لدالة، فإنها تعتمد على المَعلمة Continuation.suspend عند استخدام الوضع الكامل، يتم الاحتفاظ بالسمة 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