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

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

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

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

  • قوانین تولیدشده مشروط هستند. 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 چندپلاتفرمی با هدف 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 Multiplatform با هدف Android نیز به هیچ راه‌اندازی اضافی نیاز ندارند. وقتی AAR را برای هدف Android می‌سازید، افزونه قوانین تولیدشده را به همان روشی که برای کتابخانه‌های Android انجام می‌شود به فایل proguard.txt مربوط به AAR اضافه می‌کند. افزایه یک تکلیف 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 درستی‌سنجی کنید.

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

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

Because the core library uses the add-on's class only through reflection, R8 might remove the class or its constructor from an app's release build, which makes the lookup fail. این خرابی فقط در ساخت‌های نسخه پخش برنامه‌هایی اتفاق می‌افتد که شامل برافزا هستند، بنابراین تشخیص آن در آزمایش دشوار است.

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

محدودیت‌ها

Jetpack Keep هنوز بازتابی را که کلاس‌ها و اعضا را براساس حاشیه‌نویسی‌هایشان پیدا می‌کند شناسایی نمی‌کند یا قوانین حفظ برای آن تولید نمی‌کند. این ویژگی برنامه‌ریزی شده است.

تا آن زمان، از قوانین نگهداری برای این نوع بازتاب استفاده کنید. برای مثال، قانون زیر همه اعضایی را که با @SomeAnnotation حاشیه‌نویسی شده‌اند در کلاس‌هایی که R8 نگه می‌دارد حفظ می‌کند:

-keepclassmembers class * {
    @com.example.annotation.SomeAnnotation <methods>;
    @com.example.annotation.SomeAnnotation <fields>;
}

اگر Jetpack Keep برای مورد استفاده شما کار نمی‌کند، یا اگر بازخورد دیگری دارید، مشکل را ثبت کنید.

منابع بیشتر