تستند الأمثلة التالية إلى سيناريوهات شائعة تستخدم فيها 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 العناصر التي يجب الحفاظ عليها.
استخدام الانعكاس لتوفير التبعيات الاختيارية
من حالات الاستخدام الشائعة للربط الديناميكي إنشاء تبعية ضعيفة بين مكتبة أساسية ومكتبة إضافية اختيارية. يمكن للمكتبة الأساسية التحقّق مما إذا كانت الإضافة مضمّنة في التطبيق، وإذا كانت كذلك، يمكنها تفعيل ميزات إضافية. يتيح لك ذلك شحن وحدات إضافية بدون فرض تبعية مباشرة على المكتبة الأساسية.
تستخدم المكتبة الأساسية الانعكاس (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 للوصول إلى الأعضاء الخاصين
يمكن أن يؤدي استخدام الانعكاس للوصول إلى رمز خاص أو محمي غير مضمّن في واجهة برمجة التطبيقات العامة لمكتبة إلى حدوث مشاكل كبيرة. ويخضع هذا الرمز البرمجي للتغيير بدون إشعار، ما قد يؤدي إلى حدوث سلوك غير متوقع أو أعطال في تطبيقك.
عند الاعتماد على الانعكاس لواجهات برمجة التطبيقات غير العامة، قد تواجه المشاكل التالية:
- التحديثات المحظورة: يمكن أن تمنع التغييرات في الرمز الخاص أو المحمي الترقية إلى إصدارات أحدث من المكتبة.
- المزايا التي قد تفوتك: قد تفوتك وظائف جديدة أو عمليات إصلاح مهمة للأعطال أو تحديثات أمان أساسية.
تحسينات 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
في هذا السيناريو، لا توجد قاعدة احتفاظ مُقترَحة. ينشئ مكوّن Gradle الإضافي تلقائيًا قواعد الاحتفاظ المطلوبة للفئات التي تضيف إليها التعليق التوضيحي @Parcelize
.kotlin-parcelize
وتتولّى هذه العملية التعامل مع التعقيدات، مع التأكّد من الحفاظ على CREATOR
والدوال الإنشائية التي تم إنشاؤها لطلبات الانعكاس في إطار عمل Android.
إذا كتبت فئة Parcelable
يدويًا في Kotlin بدون استخدام @Parcelize
،
تكون مسؤولاً عن الاحتفاظ بالحقل CREATOR
والدالة الإنشائية التي
تقبل Parcel
. ويؤدي عدم إجراء ذلك إلى تعطُّل تطبيقك عندما يحاول النظام إلغاء تسلسل العنصر. يُعد استخدام @Parcelize
الممارسة الاعتيادية والأكثر أمانًا.
عند استخدام المكوّن الإضافي kotlin-parcelize
، يُرجى الانتباه إلى ما يلي:
- تنشئ الإضافة تلقائيًا حقول
CREATOR
أثناء التجميع. - يحتوي الملف
proguard-android-optimize.txt
على قواعدkeep
اللازمة للاحتفاظ بهذه الحقول من أجل ضمان الأداء السليم. - على مطوّري التطبيقات التأكّد من توفّر جميع قواعد
keep
المطلوبة، خاصةً في ما يتعلّق بأي عمليات تنفيذ مخصّصة أو تبعيات تابعة لجهات خارجية.