تولید قوانین نگهداری از گزارمان‌ها

Jetpack Keep به شما امکان می‌دهد استفاده از بازتاب و «میانای بومی جاوا» (JNI) را با حاشیه‌نویسی مستقیماً در کد منبع Kotlin یا Java اعلام کنید، به‌جای اینکه در فایل‌های قوانین نگهداری جداگانه این کار را انجام دهید. افزایه Jetpack Keep Gradle این حاشیه‌نویسی‌ها را در زمان ساخت می‌خواند و قوانین نگهداری موردنیاز R8 را تولید می‌کند.

Jetpack Keep کمک می‌کند بهینه‌سازی R8 با تغییر پایگاه کد شما همچنان مؤثر باقی بماند:

  • قوانین نگهداری در کنار کدی که به آن‌ها نیاز دارد باقی می‌ماند. وقتی کدی را که از انعکاس استفاده می‌کند تغییر نام می‌دهید یا حذف می‌کنید، شرح را در همان مکان به‌روز می‌کنید. فایل قوانین جداگانه‌ای برای همگام‌سازی وجود ندارد.

  • قوانین تولیدشده مشروط هستند. ‫R8 هدف فراخوانی بازتابی را فقط درصورتی نگه می‌دارد که کدی را که فراخوانی را انجام می‌دهد نیز نگه دارد. اگر از کد فراخواننده استفاده نشود و R8 آن را بردارد (اغلب در ویژگی‌های اختیاری کتابخانه)، R8 می‌تواند هدف را نیز بردارد، بنابراین قوانین ویژگی‌های استفاده‌نشده باعث افزایش حجم نمی‌شوند.

  • Lint بازتاب را برای شما پیدا می‌کند. این کتابخانه شامل بررسی‌های lint است که تماس‌های انعکاسی را که حاشیه‌نویسی نشده‌اند نشان می‌دهد و می‌تواند بسیاری از حاشیه‌نویسی‌ها را به‌طور خودکار اضافه کند.

نحوه عملکرد Jetpack Keep

تابعی را درنظر بگیرید که کلاسی را با نام نمونه‌سازی می‌کند. اگر R8 نتواند تشخیص دهد که کد شما از کلاس استفاده می‌کند، سازنده کلاس را برمی‌دارد و برنامه در زمان اجرا ازکار می‌افتد. برای جلوگیری از خرابی، می‌توانید قانونی مانند قانون زیر به فایل قوانین نگهداری اضافه کنید:

# Unconditional: R8 keeps this constructor even if it removes the reflection that invokes it.
-keep class com.example.ResourceLocatorImpl {
    <init>();
}

با Jetpack Keep، تابعی را که بازتاب را انجام می‌دهد حاشیه‌نویسی می‌کنید به‌جای:

// ResourceLocatorImpl isn't on this module's compile classpath, so both the
// annotation and the code refer to it by name.
@UsesReflectionToConstruct(
    className = "com.example.ResourceLocatorImpl",
    // Omitting parameterTypes keeps every constructor. An empty array keeps
    // only the no-argument constructor, which the code calls.
    parameterTypes = [],
)
fun loadResourceLocator(): ResourceLocator {
    val implClass = Class.forName("com.example.ResourceLocatorImpl")
    return implClass.getDeclaredConstructor().newInstance() as ResourceLocator
}

در زمان ساخت، افزایه Gradle قانون نگهداری را برای سازنده تولید می‌کند. برخلاف قانون قبلی که به‌صورت دستی نوشته شده بود، قانون تولیدشده شرطی است: R8 سازنده را فقط درصورتی نگه می‌دارد که loadResourceLocator در برنامه بهینه‌شده باقی بماند.

الزامات

برای استفاده از Jetpack Keep، پروژه شما به «افزایه Android Gradle»‏ (AGP) نسخه ۸.۷ یا بالاتر نیاز دارد.

افزونه Gradle از انواع واحد زیر پشتیبانی می‌کند:

نوع واحد افزایه‌ای که واحد اعمال می‌کند راه‌اندازی تکمیلی
برنامه Android com.android.application هیچ‌کدام
کتابخانه Android com.android.library هیچ‌کدام
کتابخانه Kotlin Multiplatform با هدف Android com.android.kotlin.multiplatform.library هیچ‌کدام
کتابخانه JVM (جاوا یا Kotlin) java-library یا org.jetbrains.kotlin.jvm ثبت تبدیل JAR

راه‌اندازی Jetpack Keep

برای استفاده از Jetpack Keep، هم کتابخانه و هم افزایه Gradle را به هر واحدی که از انعکاس یا JNI استفاده می‌کند اضافه کنید. کتابخانه بررسی‌های lint و گزارمان‌ها را ارائه می‌دهد و افزایه گزارشمان‌ها را به قوانین نگهداری تبدیل می‌کند.

  1. در فایل libs.versions.toml، کتابخانه و افزونه را اضافه کنید. کتابخانه و افزونه نسخه یکسانی دارند:

    [versions]
    # The library and the plugin ship together. Use one version for both so
    # that they stay in sync when you upgrade.
    annotationKeep = "1.0.0-alpha01"
    
    [libraries]
    androidx-annotation-keep = { group = "androidx.annotation.keep", name = "keep", version.ref = "annotationKeep" }
    
    [plugins]
    androidx-annotation-keep = { id = "androidx.annotation.keep", version.ref = "annotationKeep" }
    

    ‫Gradle افزایه را از مخزن Maven متعلق به Google بارگیری می‌کند، بنابراین مطمئن شوید که مخزن‌های pluginManagement در فایل settings.gradle.kts شما شامل google() باشد.

  2. در فایل ریشه build.gradle.kts، افزایه را در کنار AGP بدون اعمال کردن آن تعریف کنید:

    plugins {
        alias(libs.plugins.android.application) apply false
        // Other plugins.
    
        // The plugin relies on AGP classes that it doesn't bundle, so declare
        // it next to AGP. Declaring it here also gives every module the same
        // plugin version.
        alias(libs.plugins.androidx.annotation.keep) apply false
    }
    
  3. در فایل build.gradle.kts هر مدولی که از بازتاب یا JNI استفاده می‌کند، افزونه را اعمال کنید و وابستگی کتابخانه را اضافه کنید:

    plugins {
        alias(libs.plugins.android.application)
        // The plugin reads annotations only from this module's own code, so
        // apply it in every module that uses Jetpack Keep.
        alias(libs.plugins.androidx.annotation.keep)
    }
    
    dependencies {
        implementation(libs.androidx.annotation.keep)
    }
    
  4. پروژه‌تان را با فایل‌های Gradle همگام‌سازی کنید.

برای هر گونه ساختار از واحد برنامه Android، افزایه یک تکلیف <variant>ExtractKeepRules، مانند releaseExtractKeepRules، را ثبت می‌کند و قوانین تولیدشده را به R8 برای آن گونه ارسال می‌کند. برای ماژول‌های کتابخانه، افزونه قوانین تولیدشده را به‌عنوان قوانین نگهداری مصرف‌کننده در خروجی کتابخانه بسته‌بندی می‌کند. مدول‌های کتابخانه JVM به راه‌اندازی اضافی نیاز دارند.

پیدا کردن بازتاب با پرز

Jetpack Keep شامل بررسی‌های lint زیر است:

ReflectionWithoutKeepAnnotations
فراخوانی‌های بازتابی را پرچم‌گذاری می‌کند، مانند Class.forName، getDeclaredConstructor، getDeclaredMethod، و getDeclaredField، که حاشیه‌نویسی حفظ مطابق ندارند.
MissingKeepAnnotationPlugin
واحدهایی را پرچم‌گذاری می‌کند که به androidx.annotation.keep:keep وابسته هستند اما افزونه Gradle‏ androidx.annotation.keep را اعمال نمی‌کنند. بدون افزونه، ساخت شما قوانین نگهداری را از حاشیه‌نویسی‌ها تولید نمی‌کند.

برای استفاده از lint برای حاشیه‌نویسی یک ماژول Android، این مراحل را دنبال کنید:

  1. lint را روی واحد اجرا کنید تا تماس‌های بازتابی را که به حاشیه‌نویسی نیاز دارند پیدا کنید:

    ./gradlew :app:lint
    
  2. اصلاحاتی را که lint می‌تواند به‌طور خودکار انجام دهد اعمال کنید:

    ./gradlew :app:lintFix
    

    Lint می‌تواند حاشیه‌نویسی را به‌طور خودکار اضافه کند، وقتی‌که بتواند تعیین کند کد به کدام کلاس و اعضا ارجاع می‌دهد.

  3. هر حاشیه‌نویسی اضافه‌شده را مرور کنید تا مطمئن شوید با عملکرد کد شما مطابقت دارد. برای مثال، بررسی کنید که انواع پارامتر با سازنده یا روشی که کد شما فراخوانی می‌کند مطابقت داشته باشد.

  4. یافته‌های باقی‌مانده را در «استودیو Android» حل‌وفصل کنید. وقتی lint نتواند کلاس هدف را تعیین کند، اصلاح سریع گزارمان را با جای‌بان TODO برای تکمیل شما درج می‌کند. همچنین می‌توانید حاشیه‌نویسی را به‌صورت دستی اضافه کنید.

    برای مثال، تابع زیر نام کلاس را به‌عنوان پارامتر دریافت می‌کند، بنابراین lint نمی‌تواند تعیین کند که کدام کلاس را می‌سازد. اصلاح سریع حاشیه‌نویسی‌ای با جای‌بان اضافه می‌کند:

    @UsesReflectionToConstruct(
        className = TODO(),
        parameterTypes = []
    )
    fun createInstance(className: String): Any =
        Class.forName(className).getDeclaredConstructor().newInstance()
    

    جای‌بان را با کلاسی که کدتان می‌سازد جایگزین کنید. اگر کلاس در زمان ترجمه دردسترس است، به‌جای className از classConstant استفاده کنید. در Kotlin، کد شما تا زمانی که TODO() را جایگزین نکنید کامپایل نمی‌شود.

  5. پیش‌از اینکه قوانین موجود را بردارید، بررسی کنید افزایه از گزارمان‌هایتان چه قوانین نگهداری‌ای تولید می‌کند. برای ماژول برنامه، تکلیف استخراج را برای نسخه پخش اجرا کنید:

    ./gradlew :app:releaseExtractKeepRules
    

    افزونه قوانین را در app/build/generated/release/androidx.annotation.keep.rules.pro می‌نویسد. برای مدول‌های کتابخانه، به درستی‌سنجی کردن قوانین تولیدشده مراجعه کنید.

  6. قوانین نگهداری را که گزارمان‌های جدید جایگزین آن‌ها می‌شوند از فایل‌های قوانین نگهداری‌تان بردارید. ممکن است یک قانون بازتابی را نیز پوشش دهد که lint پرچم‌گذاری نکرده است، مانند بازتاب در یک ماژول دیگر یا در یک کتابخانه، بنابراین هر قانون را قبل‌از حذف آن بررسی کنید:

    • برای قانون سراسری بسته، مثل -keep class com.example.** { *; }، مطمئن شوید که گزارمان‌ها یا قوانین دیگر همه بازتاب‌های مربوط به کلاس‌های بسته را پوشش دهند.
    • برای قانونی که کلاس‌ها یا اعضای خاصی را نگه می‌دارد، مطمئن شوید که با یکی از قوانین تولیدشده مطابقت داشته باشد، یا هیچ بازتاب دیگری به آن وابسته نباشد.
  7. ساخت نسخه پخش را آزمایش کنید.

حاشیه‌نویسی کد

حاشیه‌نویسی Jetpack Keep را به کدی که بازتاب را انجام می‌دهد اضافه کنید: تابع، سازنده، فیلد، یا کلاسی که حاوی فراخوانی بازتابی است. حاشیه‌نویسی هدف بازتاب را شرح می‌دهد و قانون تولیدشده فقط درصورتی آن هدف را نگه می‌دارد که R8 کد حاشیه‌نویسی‌شده را نگه دارد.

براساس کاری که کدتان انجام می‌دهد، حاشیه‌نویسی‌ای انتخاب کنید:

حاشیه‌نویسی استفاده از کد
@UsesReflectionToConstruct سازنده‌ای را ازطریق بازتاب فراخوانی می‌کند، برای مثال با getDeclaredConstructor().newInstance().
@UsesReflectionToAccessMethod متدی را ازطریق انعکاس فرا می‌خواند، برای مثال با getMethod یا getDeclaredMethod.
@UsesReflectionToAccessField فیلدی را ازطریق انعکاس می‌خواند یا می‌نویسد، برای مثال با getField یا getDeclaredField.
@UnconditionallyKeep از خارج از کد Kotlin یا Java برنامه شما فراخوانی می‌شود، برای مثال از کد بومی ازطریق JNI. فقط زمانی از این حاشیه‌نویسی استفاده کنید که نمی‌توانید از حاشیه‌نویسی‌های @UsesReflection* استفاده کنید، که قوانین شرطی تولید می‌کنند.

همه حاشیه‌نویسی‌های @UsesReflection* از این قراردادها پیروی می‌کنند:

  • کلاس هدف را با classConstant یا className مشخص کنید. اگر کلاس در زمان ترجمه دردسترس است، از classConstant استفاده کنید. کامپایلر مرجع را بررسی می‌کند و بازسازی‌های IDE مانند تغییر نام آن را به‌روز می‌کند. از className فقط زمانی استفاده کنید که کلاس در زمان ترجمه دردسترس نباشد، مثلاً کلاس در وابستگی اختیاری. این دو پارامتر مانعه‌الجمع هستند.
  • خاص‌ترین کلاس را هدف قرار دهید. قوانین تولیدشده اعضای کلاس مشخص‌شده و زیرکلاس‌ها یا پیاده‌سازی‌های رابط آن را مطابقت می‌دهد. هرچه کلاس عمومی‌تر باشد، R8 کد بیشتری را نگه می‌دارد.
  • انواع پارامتر را مشخص کنید تا یک سرریز واحد داشته باشید. اگر parameterTypes یا parameterTypeNames را تنظیم نکنید، قوانین همه سازنده‌ها یا همه سربارگذاری‌های روش را حفظ می‌کنند. برای نگه داشتن فقط سازنده یا روش بدون آرگومان، یک آرایه خالی ارسال کنید.
  • حاشیه‌نویسی را برای چند هدف تکرار کنید. اگر یک تابع روی بیش‌از یک کلاس یا عضو تأثیر می‌گذارد، برای هر هدف یک حاشیه‌نویسی اضافه کنید.

ساختن اشیا با بازتاب

وقتی کدتان کلاسی را ازطریق انعکاس نمونه‌سازی می‌کند از @UsesReflectionToConstruct استفاده کنید. در مثال زیر، یک اجراکننده تکلیف پیاده‌سازی‌های رابط StartupTask را براساس نام نمونه‌سازی می‌کند. چون حاشیه‌نویسی رابط را مشخص می‌کند، قوانین تولیدشده سازنده بدون آرگومان هر پیاده‌سازی StartupTask را حفظ می‌کند، تا زمانی که R8‏ execute را حفظ کند:

interface StartupTask {
    fun run()
}

object TaskRunner {
    // The task class is known only at runtime, so target the interface that
    // every task implements.
    @UsesReflectionToConstruct(classConstant = StartupTask::class, parameterTypes = [])
    fun execute(className: String) {
        val task = Class.forName(className)
            .getDeclaredConstructor()
            .newInstance() as StartupTask
        task.run()
    }
}

فراخوانی روش‌ها با انعکاس

وقتی کدتان متدی را ازطریق بازتاب فراخوانی می‌کند، از @UsesReflectionToAccessMethod استفاده کنید. ‫methodName را روی نام روش تنظیم کنید. در مثال زیر، کد یک روش ایستا را در یک وابستگی اختیاری فراخوانی می‌کند. کلاس در زمان ترجمه دردسترس نیست، بنابراین حاشیه‌نویسی به‌جای classConstant از className استفاده می‌کند:

object CrashReporting {
    @UsesReflectionToAccessMethod(
        className = "com.example.crashlib.CrashReporter",
        methodName = "log",
        // Omitting parameterTypes keeps every log overload. Keep only the one
        // that the code calls.
        parameterTypes = [String::class],
    )
    fun log(message: String) {
        val reporterClass = try {
            Class.forName("com.example.crashlib.CrashReporter")
        } catch (e: ClassNotFoundException) {
            // Crash reporting is optional, so skip it instead of failing.
            return
        }
        reporterClass.getMethod("log", String::class.java).invoke(null, message)
    }
}

دسترسی به فیلدها با بازتاب

وقتی کدتان فیلدی را ازطریق بازتاب می‌خواند یا می‌نویسد، از @UsesReflectionToAccessField استفاده کنید. ‫fieldName را روی نام فیلد تنظیم کنید:

class UserProfileFetcher {
    // User doesn't expose internalId, so reading it requires reflection.
    @UsesReflectionToAccessField(classConstant = User::class, fieldName = "internalId")
    fun readInternalId(user: User): Any? {
        val field = User::class.java.getDeclaredField("internalId")
        field.isAccessible = true
        return field.get(user)
    }
}

کد فراخوانی‌شده از خارج از برنامه را حفظ کنید

از @UnconditionallyKeep برای کدی استفاده کنید که از خارج از کد Kotlin یا Java برنامه‌تان فراخوانی می‌شود، مثلاً روشی که کد بومی ازطریق JNI فراخوانی می‌کند. عنصری را که فراخوانی می‌شود حاشیه‌نویسی کنید، نه کدی را که آن را فراخوانی می‌کند:

class NativeBridge {
    // Only native code calls this method, through a JNI lookup by name that
    // R8 can't see. Keep the method and its name unconditionally.
    @UnconditionallyKeep
    fun onNativeEvent(message: String) {
        Log.d("NativeBridge", "Received from native code: $message")
    }

    // The default proguard-android-optimize.txt file already keeps native
    // method names, so this method doesn't need an annotation.
    external fun startNativeProcess()
}

قانون تولیدشده روش حاشیه‌نویسی‌شده و کلاس حاوی آن را با نام‌های اصلی‌شان حفظ می‌کند. کلاس‌های دیگری را که امضای روش به آن‌ها ارجاع می‌دهد نگه نمی‌دارد. اگر کد بومی به آن کلاس‌ها وابسته است، آن‌ها را نیز حاشیه‌نویسی کنید یا قوانین نگهداری برای آن‌ها اضافه کنید. برای اطلاعات بیشتر درباره JNI و R8، به تعامل با کد بومی ازطریق JNI مراجعه کنید.

به‌طور پیش‌فرض، @UnconditionallyKeep نام عنصر را حفظ می‌کند. اگر تماس‌گیرنده خارجی به نام وابسته نیست، shouldPreserveName = false را تنظیم کنید تا ‫R8 بتواند نام عنصر را تغییر دهد.

درستی‌سنجی کردن قوانین تولیدشده

این بخش توضیح می‌دهد که چگونه قوانین تولیدشده را برای هر نوع واحد بررسی کنید. برای ماژول برنامه، افزونه قوانین را مستقیماً به R8 ارسال می‌کند. برای واحد کتابخانه، افزایه قوانین را به‌عنوان قوانین نگهداری مصرف‌کننده در برونداد کتابخانه بسته‌بندی می‌کند. برنامه‌هایی که به کتابخانه شما وابسته هستند، این قوانین را به‌طور خودکار هنگام اجرای R8 اعمال می‌کنند، حتی اگر برنامه‌ها از Jetpack Keep استفاده نکنند. برای اطلاعات بیشتر درباره قوانین نگهداری مصرف‌کننده، به بهینه‌سازی برای نویسندگان کتابخانه مراجعه کنید.

برنامه‌های Android

برای بررسی اینکه افزایه کدام قوانین نگهداری را برای واحد برنامه تولید می‌کند، وظیفه استخراج را برای گونه ساخت اجرا کنید:

./gradlew :app:releaseExtractKeepRules

افزونه قوانین هر متغیر را در app/build/generated/<variant>/androidx.annotation.keep.rules.pro می‌نویسد. یک # context: نظر قبل‌از هر قانون، کد حاشیه‌نویسی‌شده‌ای را که آن را تولید کرده است شناسایی می‌کند.

برای بررسی اینکه قوانین تولیدشده چگونه همراه با دیگر قوانین حفظ شما بر بهینه‌سازی تأثیر می‌گذارند، از تحلیل‌گر پیکربندی R8 استفاده کنید.

کتابخانه‌های Android

ماژول‌های کتابخانه Android به راه‌اندازی اضافی نیاز ندارند. وقتی AAR را می‌سازید، افزونه قوانین تولیدشده را به فایل proguard.txt مربوط به AAR اضافه می‌کند، بعداز هر قانون حفظ مصرف‌کننده که AAR ازقبل دارد. افزونه برای انجام این کار، یک تکلیف <variant>KeepRulesTransformAar برای هر تنوع ساخت ثبت می‌کند.

برای درستی‌سنجی قوانین، AAR را بسازید، مثلاً با ./gradlew :mylibrary:assembleRelease، و proguard.txt را در AAR تولیدشده باز کنید. قوانین تولیدشده در بخشی با سرصفحه زیر نشان داده می‌شود:

#################################################################
#   Rules generated from androidx.annotation.keep annotations   #
#################################################################

در macOS و Linux، می‌توانید قوانین را از خط فرمان نیز چاپ کنید:

unzip -p mylibrary/build/intermediates/aar/release/releaseKeepRulesTransformAar/mylibrary-release.aar proguard.txt

کتابخانه‌های Kotlin چندپلاتفرمی

مدول‌های کتابخانه Kotlin چندپلاتفرمی با هدف Android نیز به هیچ راه‌اندازی اضافی نیاز ندارند. وقتی AAR را برای هدف Android می‌سازید، افزایه قوانین تولیدشده را به فایل proguard.txt در AAR به همان روشی که برای کتابخانه‌های Android انجام می‌دهد اضافه می‌کند. افزایه یک تکلیف androidMainKeepRulesTransformAar برای انجام این کار ثبت می‌کند.

برای درستی‌سنجی قوانین، آن تکلیف را اجرا کنید و proguard.txt را در «گزارش پس‌از اقدام» که ایجاد می‌کند باز کنید. در macOS و Linux، می‌توانید قوانین را از خط فرمان چاپ کنید:

./gradlew :mylibrary:androidMainKeepRulesTransformAar
unzip -p mylibrary/build/intermediates/aar/androidMain/androidMainKeepRulesTransformAar/mylibrary.aar proguard.txt

کتابخانه‌های JVM

قبل‌از اینکه بتوانید قوانین واحد کتابخانه JVM را درستی‌سنجی کنید، واحد را پیکربندی کنید تا افزایه قوانین تولیدشده را به نسخه‌ای از JAR آن اضافه کند. سپس تکلیف تبدیل را که ثبت کرده‌اید اجرا کنید و فایل‌های قوانین را در JAR که ایجاد می‌کند بررسی کنید. برای مثال، اگر نام تکلیف را keepRulesJar گذاشته‌اید، می‌توانید قوانین را در macOS و Linux با فرمان‌های زیر چاپ کنید:

./gradlew :mylibrary:keepRulesJar
unzip -p mylibrary/build/intermediates/annotation-keep/keepRulesJar/mylibrary-keep-rules.jar \
    "META-INF/com.android.tools/*" "META-INF/proguard/*"

پیکربندی کتابخانه‌های JVM

برای ماژول‌های کتابخانه JVM که JAR تولید می‌کنند، افزونه به‌طور خودکار JAR را تغییر نمی‌دهد. به‌جای آن، از تابع annotationKeep.registerJavaArchiveTransform برای ثبت کردن تکلیفی استفاده کنید که نسخه‌ای از JAR را با قوانین تولیدشده ایجاد می‌کند، و سپس آن JAR را منتشر کنید. مورد زیر را به فایل build.gradle.kts واحد کتابخانه اضافه کنید:

plugins {
    id("java-library")
    alias(libs.plugins.jetbrains.kotlin.jvm)
    alias(libs.plugins.androidx.annotation.keep)
}

val jarTask = tasks.named<Jar>("jar")

// Apps run R8 on this library's code, so the generated rules must be inside
// the JAR. The plugin handles this automatically only for Android modules.
val keepRulesJarTask = annotationKeep.registerJavaArchiveTransform(
    taskName = "keepRulesJar",
    input = jarTask.flatMap { it.archiveFile },
    fileName = jarTask.flatMap { it.archiveFileName }.map { it.replace(".jar", "-keep-rules.jar") },
)

// Consumers of this library resolve apiElements and runtimeElements, so add
// the JAR that contains the generated rules to them.
configurations.configureEach {
    if (name == "apiElements" || name == "runtimeElements") {
        outgoing.artifact(keepRulesJarTask.flatMap { it.outputJar })
    }
}

dependencies {
    implementation(libs.androidx.annotation.keep)
}

تبدیل، قوانین تولیدشده را به مکان قوانین R8 هدف JAR اضافه می‌کند، META-INF/com.android.tools/r8/proguard.txt. اگر JAR ازقبل حاوی قوانینی در META-INF/com.android.tools/ یا META-INF/proguard/ باشد، تبدیل به‌جای آن قوانین تولیدشده را به فایل قوانین موجود اضافه می‌کند. برای اطلاعات بیشتر درباره این مکان‌ها، به پشتیبانی از نسخه‌های مختلف R8 مراجعه کنید.

پس‌از ثبت تبدیل، قوانین را در JAR درستی‌سنجی کنید.

مثال: بار کردن کتابخانه برافزای اختیاری

کتابخانه‌ها اغلب از انعکاس برای بار کردن برافزاهای اختیاری استفاده می‌کنند. برای مثال، یک کیت توسعه نرم‌افزار تجزیه‌وتحلیل ممکن است یک کتابخانه اصلی و یک برافزای ویدیویی ارائه دهد. برافزا به کتابخانه اصلی وابسته است، بنابراین کتابخانه اصلی نمی‌تواند مستقیماً به کلاس‌های برافزا ارجاع دهد. درعوض، کتابخانه اصلی کلاس برافزایی را در زمان اجرا براساس نام جستجو می‌کند و تنها درصورتی که کلاس را پیدا کند، تجزیه‌وتحلیل ویدیو را روشن می‌کند.

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

بدون Jetpack Keep، برافزا یک قانون نگهداری مصرف‌کننده برای کلاس خود ارسال می‌کند، همان‌طور که در انعکاس برای پشتیبانی از وابستگی‌های اختیاری نشان داده شده است:

# In the add-on's consumer keep rules file.
-keep class com.example.analytics.video.VideoEventTracker {
    <init>();
}

این قانون در کتابخانه‌ای متفاوت از جستجویی است که به آن نیاز دارد، بنابراین این دو می‌توانند از همگام‌سازی خارج شوند. با Jetpack Keep، به‌جای آن در کتابخانه اصلی به جستجو حاشیه‌نویسی می‌کنید:

// In the core library.
interface AnalyticsPlugin {
    fun start()
}

object AnalyticsManager {
    // The annotation and the lookup share this constant so that they can't
    // refer to different classes.
    private const val VIDEO_TRACKER_CLASS =
        "com.example.analytics.video.VideoEventTracker"

    @UsesReflectionToConstruct(
        className = VIDEO_TRACKER_CLASS,
        parameterTypes = [],
    )
    fun initialize() {
        val tracker = try {
            Class.forName(VIDEO_TRACKER_CLASS)
                .getDeclaredConstructor()
                .newInstance() as AnalyticsPlugin
        } catch (e: ClassNotFoundException) {
            // Apps that don't play video leave out the add-on, so a missing
            // class isn't an error.
            return
        }
        // This is an ordinary interface call, not a reflective one, so R8
        // keeps start() without an annotation.
        tracker.start()
    }
}

حاشیه‌نویسی از این دستورالعمل‌ها پیروی می‌کند:

  • کدی را که انعکاس را انجام می‌دهد حاشیه‌نویسی کنید. حاشیه‌نویسی در initialize است که Class.forName را فراخوانی می‌کند، نه در کلاس برافزا.
  • حاشیه‌نویسی را براساس کاری که کد انجام می‌دهد انتخاب کنید. جستجو نمونه‌ای از کلاس ایجاد می‌کند، بنابراین کد از @UsesReflectionToConstruct استفاده می‌کند.
  • از className برای کلاسی که نمی‌توانید به آن ارجاع دهید استفاده کنید. کلاس برافزا در مسیر کلاس کامپایل کتابخانه اصلی نیست، بنابراین حاشیه‌نویسی نمی‌تواند از classConstant استفاده کند.
  • فقط سازنده‌ای را که کد فراخوانی می‌کند نگه دارید. جستجو سازنده بدون آرگومان را فراخوانی می‌کند، بنابراین parameterTypes آرایه خالی است.

وقتی AAR کتابخانه اصلی را می‌سازید، افزونه قانونی اضافه می‌کند که VideoEventTracker و سازنده بدون آرگومان آن را هرگاه R8 initialize را نگه دارد، نگه می‌دارد. برنامه‌هایی که از کیت توسعه نرم‌افزار استفاده می‌کنند این قانون را به‌طور خودکار اعمال می‌کنند، حتی اگر از Jetpack Keep استفاده نکنند، بنابراین برافزا به قانون خاص خود نیاز ندارد. در مقایسه با قانون برافزا، این رویکرد مزایای زیر را دارد:

  • قانون در کنار جستجو باقی می‌ماند. کتابخانه اصلی هم جستجو و هم قانون را دراختیار دارد، بنابراین آن‌ها را باهم به‌روزرسانی می‌کنید به‌جای اینکه تغییری را در دو کتابخانه هماهنگ کنید.
  • R8 همچنان می‌تواند کد استفاده‌نشده را بردارد. اگر برنامه‌ای شامل برافزا باشد اما هرگز initialize را فراخوانی نکند، قانون تولیدشده اعمال نمی‌شود و R8 می‌تواند VideoEventTracker را بردارد. قانون برافزا آن را در هر برنامه‌ای که برافزا را دربرمی‌گیرد نگه می‌دارد.
  • ‫Lint جستجوهای حاشیه‌نویسی‌نشده را می‌گیرد. اگر کتابخانه اصلی بعداً جستجوی بازتابی دیگری بدون حاشیه‌نویسی اضافه کند، lint آن را پرچم‌گذاری می‌کند تا بتوانید قبل‌از انتشار کتابخانه آن را برطرف کنید.

برای تأیید نتیجه، بررسی کنید که قانون تولیدشده در فایل AAR proguard.txt ظاهر شود، همان‌طور که در کتابخانه‌های Android شرح داده شده است. سپس نسخه ساخت انتشار برنامه حاوی برافزا را آزمایش کنید، زیرا در آنجا است که قانون ازدست‌رفته باعث ازکارافتادن تجزیه‌وتحلیل ویدیو می‌شود.

منابع بیشتر