إنشاء قواعد الاحتفاظ من التعليقات التوضيحية

تتيح لك مكتبة Jetpack Keep تحديد استخدام الانعكاس وواجهة Java Native Interface (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، يحتاج مشروعك إلى الإصدار 8.7 من &quot;المكوّن الإضافي لنظام Gradle المتوافق مع Android&quot; (AGP) أو إصدار أحدث.

يتيح المكوّن الإضافي لنظام Gradle أنواع الوحدات التالية:

نوع الوحدة المكوّن الإضافي الذي تطبّقه الوحدة الإعداد الإضافي
تطبيق Android com.android.application بدون
مكتبة Android com.android.library بدون
مكتبة Kotlin Multiplatform مع هدف Android com.android.kotlin.multiplatform.library بدون
مكتبة JVM (Java أو Kotlin) java-library أو org.jetbrains.kotlin.jvm تسجيل عملية تحويل JAR

إعداد Jetpack Keep

لاستخدام Jetpack Keep، أضِف كلاً من المكتبة والمكوّن الإضافي Gradle إلى كل وحدة تستخدم الانعكاس أو JNI. توفّر المكتبة التعليقات التوضيحية وعمليات التحقّق من الأخطاء، ويحوّل المكوّن الإضافي التعليقات التوضيحية إلى قواعد احتفاظ.

  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 إعدادات إضافية.

العثور على الانعكاس باستخدام أداة lint

تتضمّن Jetpack Keep عمليات التحقّق من الأخطاء التالية:

ReflectionWithoutKeepAnnotations
يتم وضع علامة على الاستدعاءات الانعكاسية، مثل Class.forName وgetDeclaredConstructor وgetDeclaredMethod وgetDeclaredField، التي لا تتضمّن تعليقًا توضيحيًا مطابقًا للاحتفاظ بها.
MissingKeepAnnotationPlugin
يتم وضع علامة على الوحدات التي تعتمد على androidx.annotation.keep:keep ولكن لا تطبّق المكوّن الإضافي androidx.annotation.keep لنظام Gradle. بدون المكوّن الإضافي، لا تنشئ عملية الإنشاء قواعد الاحتفاظ من التعليقات التوضيحية.

لاستخدام أداة 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()
    

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

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 Multiplatform

ولا تحتاج وحدات مكتبة Kotlin Multiplatform التي تستهدف Android إلى أي إعدادات إضافية أيضًا. عند إنشاء ملف AAR لهدف Android، يضيف المكوّن الإضافي القواعد التي تم إنشاؤها إلى ملف proguard.txt الخاص بملف AAR بالطريقة نفسها المتّبعة مع مكتبات Android. يسجّل المكوّن الإضافي مهمة androidMainKeepRulesTransformAar لتنفيذ ذلك.

للتحقّق من القواعد، شغِّل هذه المهمة وافتح proguard.txt في ملف AAR الذي تنشئه. على نظامَي التشغيل 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.

مثال: تحميل مكتبة إضافات اختيارية

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

بما أنّ المكتبة الأساسية تستخدم فئة الإضافة من خلال الانعكاس فقط، قد يزيل 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. تطبّق التطبيقات التي تستخدم حزمة SDK هذه القاعدة تلقائيًا، حتى إذا لم تكن تستخدم Jetpack Keep، لذا لا تحتاج الإضافة إلى قاعدة خاصة بها. مقارنةً بقاعدة الإضافة، يتسم هذا النهج بالمزايا التالية:

  • تبقى القاعدة بجانب البحث. تمتلك المكتبة الأساسية كلاً من عملية البحث والقاعدة، لذا يمكنك تعديلهما معًا بدلاً من تنسيق تغيير في مكتبتَين.
  • سيظل بإمكان R8 إزالة الرموز غير المستخدَمة. إذا كان التطبيق يتضمّن الإضافة ولكنّه لا يستدعي initialize مطلقًا، لن تنطبق القاعدة التي تم إنشاؤها، ويمكن لـ R8 إزالة VideoEventTracker. تحتفظ قاعدة الإضافة بها في كل تطبيق يتضمن الإضافة.
  • تكتشف أداة Lint عمليات البحث غير الموضَّحة. إذا أضافت المكتبة الأساسية لاحقًا عملية بحث انعكاسي أخرى بدون تعليق توضيحي، ستضع أداة lint علامة عليها، حتى تتمكّن من إصلاحها قبل إصدار المكتبة.

للتأكّد من النتيجة، تحقَّق من ظهور القاعدة التي تم إنشاؤها في ملف proguard.txt الخاص بملف AAR، كما هو موضّح في مكتبات Android. بعد ذلك، اختبِر بنية إصدار لتطبيق يتضمّن الإضافة، لأنّ ذلك هو المكان الذي قد تؤدي فيه القاعدة المفقودة إلى تعطُّل إحصاءات الفيديو.

القيود

لا ترصد Jetpack Keep حتى الآن الانعكاس الذي يعثر على الفئات والعناصر من خلال تعليقاتها التوضيحية، ولا تنشئ قواعد احتفاظ لها. من المقرّر إطلاق هذه الميزة.

وحتى ذلك الحين، يمكنك استخدام قواعد الاحتفاظ لهذا النوع من الانعكاس. على سبيل المثال، تحتفظ القاعدة التالية بكل عضو تمّت إضافة تعليق توضيحي إليه باستخدام @SomeAnnotation في الفئات التي يحتفظ بها R8:

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

إذا لم تكن Jetpack Keep مناسبة لحالة الاستخدام لديك، أو إذا كانت لديك ملاحظات أخرى، يمكنك الإبلاغ عن مشكلة.

مراجع إضافية