نگه‌داشتن موارد استفاده و نمونه‌های قانون

مثال‌های زیر براساس سناریوهای رایجی است که در آن‌ها از R8 برای بهینه‌سازی استفاده می‌کنید، اما برای پیش‌نویس کردن قوانین نگهداری به راهنمایی پیشرفته نیاز دارید.

بازتاب

به‌طور کلی، برای عملکرد بهینه، استفاده از بازتاب توصیه نمی‌شود. بااین‌حال، در برخی سناریوها، ممکن است اجتناب‌ناپذیر باشد. نمونه‌های زیر برای حفظ قوانین در سناریوهای رایجی که از انعکاس استفاده می‌کنند راهنمایی ارائه می‌دهند.

اگر انعکاس در کد برنامه یا کتابخانه خودتان است، می‌توانید به‌جای نوشتن دستی قوانین نگهداری، از keep annotations استفاده کنید. افزونه Gradle قوانین نگهداری را از حاشیه‌نویسی‌ها در کد منبع شما تولید می‌کند، ازجمله قوانین نگهداری مصرف‌کننده برای کتابخانه‌ها.

بازتاب با کلاس‌های بارشده براساس نام

کتابخانه‌ها اغلب کلاس‌ها را به‌طور پویا بااستفاده از نام کلاس به‌عنوان 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 می‌شود، همچنان باید از قانون حفظ استفاده کنید تا اعلام کنید که کلاس به‌صورت بازتابی نمونه‌سازی شده است و از اعضایی که به‌صورت بازتابی به آن‌ها دسترسی پیدا شده است، مانند سازنده، محافظت کنید.

برای مثال، سناریو زیر را درنظر بگیرید که در آن کتابخانه و برنامه‌ای دارید که از کتابخانه استفاده می‌کند. بارکننده کتابخانه با ارسال مستقیم مرجع کلاس، رابط 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> نام داخلی ویژه برای سازنده در بایت‌کد جاوا است. این بخش به‌طور خاص سازنده پیش‌فرض بدون آرگومان را هدف‌یابی می‌کند.
    • این قانون بسیار مهم است زیرا کد شما 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;: این فیلد خصوصی خاص را براساس نام و نوع آن شناسایی می‌کند.

میانای بومی جاوا (JNI)

بهینه‌سازی‌های R8 هنگام کار با فراخوانی‌های بالادستی از کد بومی (C/C++) به Java یا Kotlin ممکن است مشکلاتی داشته باشد. درحالی‌که عکس این موضوع نیز صادق است—فراخوانی‌های پایین‌رونده از Java یا Kotlin به کد بومی می‌تواند مشکلاتی داشته باشد—فایل پیش‌فرض proguard-android-optimize.txt شامل قانون زیر است تا فراخوانی‌های پایین‌رونده همچنان کار کنند. این قانون از حذف روش‌های بومی جلوگیری می‌کند.

-keepclasseswithmembernames,includedescriptorclasses class * {
  native <methods>;
}

تعامل با کد بومی ازطریق «میانای بومی جاوا» (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);
}

این قوانین حفظ مانع از حذف یا تغییر نام روش onNativeEvent و (به‌طور بحرانی) نوع پارامتر آن توسط R8 می‌شود.

  • -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: این اصلاح‌گر همچنین هر کلاسی را که در امضای روش یا توصیف‌گر پیدا شود حفظ می‌کند. در این مورد، از تغییر نام یا برداشتن کلاس 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 حاشیه‌نویسی کنید. این گزارمان کلاس‌ها را در امضای روش نگه نمی‌دارد، مثل 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

در این سناریو، قانون نگهداری توصیه‌شده‌ای وجود ندارد. افزونه Gradle kotlin-parcelize به‌طور خودکار قوانین نگهداری لازم را برای کلاس‌هایی که با @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

فیلدهای علامت‌گذاری‌شده با اصلاح‌کننده transient توسط Gson درطول سریال‌سازی و واگرداندن سریال‌سازی نادیده گرفته می‌شوند، به همین دلیل است که قانون حفظ به‌طور خاص فیلدهای غیرگذرا (!transient) را هدف قرار می‌دهد.

بازجورسازی

Retrofit یک کتابخانه شبکه‌سازی است که روش‌های میانای سرویس را که با حاشیه‌نویسی‌های HTTP (مانند @GET یا @POST) حاشیه‌نویسی شده‌اند، بااستفاده از بازتاب برای ساخت درخواست‌های شبکه و تبدیل پاسخ‌ها بررسی می‌کند.

Retrofit به‌طور پویا پیاده‌سازی‌های میاناهای API شما را در زمان اجرا بااستفاده از Proxy.newProxyInstance() تولید می‌کند. چون R8 هیچ کلاسی را نمی‌بیند که به‌صورت ایستا این رابط‌ها را پیاده‌سازی کند، ممکن است روش‌ها یا انواع برگشتی عمومی آن‌ها را حذف کند.

قوانین نگهداری دسته‌بندی‌شده

‫Retrofit برای بازرسی پارامترهای عمومی، حاشیه‌نویسی‌های روش، و حاشیه‌نویسی‌های پارامتر به انعکاس زمان اجرا متکی است. بدون پیکربندی مناسب، حالت کامل R8 می‌تواند امضاهای عمومی را به‌طور کامل از انواع برگشتی، ادامه Kotlin، و کلاس‌های پاسخ حذف کند، یا حتی مقادیر میانای را با مقدار تهی جایگزین کند، زیرا میاناهای Retrofit به‌صورت پویا با یک پراکسی نمونه‌سازی می‌شوند.

از Retrofit 2.10.0 به بعد، کتابخانه به‌طور خودکار قوانین رسمی 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 امضای عمومی تابع 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