حالات استخدام القواعد وأمثلة عليها

تستند الأمثلة التالية إلى سيناريوهات شائعة تستخدم فيها R8 لتحسين الأداء، ولكنّك تحتاج إلى إرشادات متقدّمة لصياغة قواعد الاحتفاظ.

الانعكاس

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

إذا كان الانعكاس في رمز التطبيق أو المكتبة، يمكنك استخدام التعليقات التوضيحية للاحتفاظ بالبيانات بدلاً من كتابة قواعد الاحتفاظ بالبيانات يدويًا. تنشئ إضافة Gradle قواعد الاحتفاظ بالبيانات من التعليقات التوضيحية في الرمز المصدري، بما في ذلك قواعد الاحتفاظ بالبيانات الخاصة بالمستهلكين في المكتبات.

Reflection with classes loaded by name

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

انعكاس للوصول إلى الأعضاء الخاصين

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

عند الاعتماد على واجهات برمجة التطبيقات غير العامة، قد تواجه المشاكل التالية:

  • التحديثات المحظورة: يمكن أن تمنع التغييرات في الرمز الخاص أو المحمي الترقية إلى إصدارات أحدث من المكتبة.
  • المزايا التي قد تفوتك: قد تفوتك الميزات الجديدة أو الإصلاحات المهمة للأعطال أو تحديثات الأمان الأساسية.

تحسينات 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 Native Interface‏ (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.

يمكنك أيضًا إضافة تعليقات توضيحية إلى الطرق التي يستدعيها الرمز البرمجي الأصلي باستخدام التعليق التوضيحي @UnconditionallyKeep من Jetpack Keep بدلاً من كتابة قواعد keep لها. لا يحتفظ التعليق التوضيحي بالفئات في توقيع الطريقة، مثل NativeData، لذا عليك الاحتفاظ بهذه الفئات بشكل منفصل.

طلبات المنصة غير المباشرة

نقل البيانات باستخدام عملية تنفيذ 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 أثناء عملية التسلسل وإلغاء التسلسل، ولهذا السبب تستهدف قاعدة keep على وجه التحديد الحقول غير المؤقتة (!transient).

التحديث

‫Retrofit هي مكتبة شبكات تفحص طرق واجهة الخدمة التي تمّت إضافة تعليقات توضيحية إليها باستخدام تعليقات توضيحية خاصة ببروتوكول HTTP (مثل @GET أو @POST) باستخدام انعكاس لإنشاء طلبات الشبكة وتحويل الردود.

تنشئ Retrofit بشكل ديناميكي عمليات تنفيذ لواجهات برمجة التطبيقات في وقت التشغيل باستخدام Proxy.newProxyInstance(). بما أنّ R8 لا يرى أي فئة تنفّذ هذه الواجهات بشكل ثابت، قد يزيل الطرق أو أنواع القيم العامة التي تم إرجاعها.

قواعد الاحتفاظ المجمّعة

تعتمد Retrofit على انعكاس وقت التشغيل لفحص المَعلمات العامة والتعليقات التوضيحية الخاصة بالطرق والمَعلمات. بدون الإعدادات الصحيحة، يمكن أن يزيل الوضع الكامل في R8 التوقيعات العامة بالكامل من أنواع القيم التي تم إرجاعها، وعمليات المتابعة في Kotlin، وفئات الاستجابة، أو حتى استبدال قيم الواجهة بقيمة فارغة لأنّ واجهات Retrofit يتم إنشاء مثيل لها بشكل ديناميكي باستخدام خادم وكيل.

بدءًا من الإصدار 2.10.0 من 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 coroutines، يحوّل برنامج تجميع Kotlin دوال suspend من خلال إلحاق مَعلمة Continuation بتوقيع الطريقة المجمّعة.

عندما تقرأ المكتبات، مثل Retrofit، التوقيع العام لدالة suspend بشكل انعكاسي، فإنّها تعتمد على مَعلمة Continuation. عند استخدام الوضع الكامل، يتم الاحتفاظ بالسمة 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