تحسين رموز النظام الأساسي وموارده باستخدام R8

يشغّل نظام تصميم نظام Android الأساسي (Soong) برنامج تجميع R8 على الاستهدافات التي تجمع رمز بايت DEX، بما في ذلك تطبيقات Android (android_app) والاختبارات (android_test) ومكتبات Java القابلة للتثبيت مع compile_dex: true (مثل services.jar)، وذلك لتقليل حجمها وتحسينها وإزالة الرموز والموارد غير المستخدَمة. في ما يتعلق بالمكتبات الثابتة (android_library والثابتة java_library)، لا يشغّل Soong أداة R8 مباشرةً، بل يستخدم كتلة السمة optimize لإرفاق قواعد الاحتفاظ بالمستهلكين (export_proguard_flags_files: true) ونشرها إلى أهداف DEX النهائية التي تربطها بشكل ثابت.

بالنسبة إلى مهندسي المنصات الذين ينشئون صور نظام أو حِزم مورّدين، فإنّ إلغاء قواعد الحفاظ الواسعة النطاق بشكل مفرط يوفّر فوائد مباشرة لصحة النظام:

  • تقليل حجم مساحة قسم النظام: يزيل R8 الفئات والطُرق والموارد غير المستخدَمة قبل تجميع حِزم APK وملفات JAR في /system و/system_ext و/product و/vendor.
  • عناصر أصغر تم تجميعها: يعني عدد أقل من طرق DEX ملفات .odex و.vdex أصغر حجمًا يتم إنشاؤها بواسطة dex2oat أثناء وقت الإنشاء أو التجميع على الجهاز فقط.
  • انخفاض ضغط الذاكرة أثناء وقت التشغيل: بما أنّ نظام التشغيل Android يربط .odex و.vdex وجداول موارد حِزم APK بذاكرة العمليات باستخدام طلبات mmap التي يتم تقسيمها إلى صفحات عند الطلب، فإنّ الملفات الثنائية الأصغر حجمًا تقلّل من أخطاء الصفحات الرئيسية أثناء بدء تشغيل التطبيق، كما تقلّل من ذاكرة الرموز والموارد المقيمة في جميع العمليات. لمزيد من المعلومات حول تأثير رمز التطبيق المجمَّع على ذاكرة الجهاز، يُرجى الاطّلاع على رمز التطبيق هو الذاكرة.
  • تحسين أكثر فعالية للبرنامج بأكمله: تتيح قيود الاحتفاظ الدقيق لبرنامج R8 تضمين الطرق، وإزالة التجريد من الواجهات والمكالمات الافتراضية، وإزالة الحقول غير المستخدَمة، ونشر الثوابت على مستوى الفئات.

ضبط تحسين R8 في Soong

في ملفات Android.bp، اضبط تحسين R8 باستخدام حظر السمة optimize على أهداف android_app وjava_library القابلة للتثبيت (أو على وحدات android_library وjava_library الثابتة لتصدير قواعد الاحتفاظ بالبيانات الخاصة بالمستهلك):

android_app {
    name: "MySystemApp",
    srcs: ["src/**/*.java"],
    optimize: {
        obfuscate: true,
        shrink_resources: true,
    },
}

يلخّص الجدول التالي سمات optimize الأكثر شيوعًا في Soong (المحدّدة في build/soong/java/dex.go):

الخاصية الوصف
enabled تتحكّم هذه السمة في ما إذا كان سيتم تشغيل R8 على الهدف. القيمة التلقائية هي true لجميع أهداف android_app.
shrink تتحكّم هذه السمة في عملية إزالة الأجزاء غير المستخدَمة من الرمز البرمجي لإزالة الفئات والحقول والطرق التي لا يمكن الوصول إليها. يتم ضبط القيمة التلقائية على true لأهداف android_app (false لوحدات الاختبار وjava_library المستقلة التي تمرِّر -dontshrink ما لم يتم ضبطها بشكل صريح).
optimize تتحكّم هذه السمة في تحسينات الرمز الثانوي، مثل تضمين الدوال البرمجية، ودمج الفئات، ونشر الثوابت، وإزالة الفروع غير النشطة. القيمة التلقائية هي true بالنسبة إلى أهداف android_app (يتم التحكّم فيها من خلال علامة بنية الإصدار RELEASE_R8_OPTIMIZE_BY_DEFAULT).
obfuscate تتحكّم هذه السياسة في تصغير المعرّفات وإعادة تسميتها. القيمة التلقائية هي false لتحقيق التوافق مع الإصدارات القديمة، ولكن يجب ضبطها على true كلما أمكن ذلك لتقليل حجم ملف DEX وإتاحة تحسينات أكثر تفصيلاً (راجِع تفعيل التشويش كلما أمكن ذلك).
shrink_resources تزيل هذه السمة الموارد غير المستخدَمة (res/ إدخالات) من حِزمة APK بعد تخفيض حجم الرموز. القيمة التلقائية هي false.
optimized_shrink_resources تشغيل مسار أداة تقليص حجم الرموز والموارد المدمجة R8، ما يتيح لأداة R8 تتبُّع مراجع الرموز والموارد بشكل مشترك في عملية واحدة عند ضبط shrink_resources: true، يتم تلقائيًا ضبط RELEASE_USE_OPTIMIZED_RESOURCE_SHRINKING_BY_DEFAULT (true في إصدارات المنصة العادية).
proguard_flags_files تعرض هذه السمة ملفات .flags أو .pro خاصة بالوحدة النمطية وتحتوي على قواعد مخصّصة للاحتفاظ بالبيانات. تجنَّب إضافة ملفات مخصّصة عندما تكون التعليقات التوضيحية أو الإعدادات التلقائية العادية كافية.
export_proguard_flags_files تنقل هذه السمة قيمة proguard_flags_files الخاصة بالمكتبة إلى الوحدات التابعة التي تعتمد عليها بشكل ثابت.
trace_references_from تعرض هذه السمة قوائم بأهداف مصاحبة لمكتبة Java، وتتتبّع تلقائيًا R8 وتحتفظ بها عندما تشير رموزها البايتية إلى هذا الهدف.

تفعيل ميزة "إخفاء مفاتيح فك التشفير" حيثما أمكن

في Soong، يتم ضبط obfuscate تلقائيًا على false لتحقيق التوافق مع الإصدارات القديمة، ولكن عليك ضبط obfuscate: true بشكل صريح على android_app واستهدافات DEX المستقلة كلما أمكن ذلك:

  • حجم أصغر لملف DEX و.vdex: يؤدي تغيير أسماء الحِزم والفئات والحقول والطُرق إلى معرّفات قصيرة (a وb) إلى تقليل حجم مجموعة السلاسل في ملف DEX (string_ids وstring_data_item) ومواصفات الأنواع. بما أنّ نظام التشغيل Android يربط ملفات .vdex و.odex بذاكرة العملية، فإنّ جداول الرموز الأصغر حجمًا تقلّل بشكل مباشر من حجم قسم النظام ومن استهلاك الذاكرة أثناء وقت التشغيل.
  • تحسينات أعمق على البرنامج بأكمله: يتيح R8 إعادة تسمية المعرّفات، ما يؤدي إلى إتاحة دمج الفئات وتسوية الحِزم وإزالة تكرار الفئات الاصطناعية في الحِزم التي يجب أن يتجاهلها R8 لتجنُّب تعارض الأسماء.
  • تحويل رموز تتبُّع تسلسل استدعاء الدوال البرمجية الكامل: لا يؤدي تفعيل التعتيم في Soong إلى تقليل إمكانية تصحيح الأخطاء. لكل هدف تم تجميعه باستخدام R8، ينشئ Soong ملف ربط proguard_dictionary في الدليل الوسيط للوحدة، ويجمّع كل قواميس الوحدات في العنصر proguard-dict.zip من عملية الإنشاء، ويضمّن تجزئة ربط (--map-id-template) في عنوان DEX، ويعيد كتابة سمات الفئة SourceFile (--source-file-template) كي يتمكّن retrace من تحويل تتبُّع تسلسل استدعاء الدوال البرمجية إلى رموز تلقائيًا.

يجب الاحتفاظ بالرمز obfuscate: false (أو الحفاظ على أسماء واجهات برمجة التطبيقات العامة بشكل صريح) فقط في الحالات التالية:

  • المكتبات المشترَكة في bootclasspath أو system_server classpath: يجب أن تحتفظ الوحدات (java_library أو java_sdk_library) التي تعرض مساحة خارجية لواجهة برمجة التطبيقات (API) مرتبطة بشكل ديناميكي بوحدات أخرى في وقت التشغيل (مثل أهداف framework.jar أو services.jar أو <uses-library>) إما بـ obfuscate: false أو تحافظ صراحةً على مساحة واجهة برمجة التطبيقات (API) باستخدام قواعد @KeepForApi أو -keep (بالإضافة إلى protect_api_surface: true لأهداف bootclasspath) حتى تتمكّن البرامج التي تم تجميعها مقابل نماذجها من حلّ أسماء الفئات والأعضاء في وقت التشغيل.

قبل تفعيل obfuscate: true على أحد التطبيقات، تحقّق من المتطلبات الأساسية التالية لنقل البيانات:

  • التبعيات في حِزم APK للاختبار الخارجي: إذا كانت مجموعة حِزم APK خارجية android_test تتضمّن instrumentation_for: "MyApp" وتستدعي مباشرةً فئات أو طرقًا داخلية من MyApp، سيؤدي تغيير أسماء هذه الرموز إلى حدوث NoSuchMethodError أو NoClassDefFoundError أثناء وقت تشغيل الاختبار. ننصحك بتنظيم الاختبار على شكل حِزمة APK ذاتية التجهيز يتم ربطها MyApp.impl بشكل ثابت، أو إضافة تعليقات توضيحية إلى نقاط ربط الاختبار باستخدام @VisibleForTesting، أو ضبط trace_references_from (راجِع الخطوة 2: نقل قواعد الاحتفاظ المستندة إلى الاختبار) حتى يتم الاحتفاظ بالرموز الداخلية التي تحتاج إليها الاختبارات مع تشويش بقية التطبيق.
  • الاسترجاع المستند إلى السلسلة وواجهة JNI: إذا كان أحد الوحدات يبحث عن الفئات أو الطرق أو الحقول من خلال أسماء السلاسل الحرفية (Class.forName أو getDeclaredMethod أو واجهة JNI FindClass وGetMethodID) بدون قواعد أو تعليقات توضيحية خاصة بالاحتفاظ، فإنّ عملية التشويش تعيد تسمية هذه العناصر المستهدَفة وتؤدي إلى تعطيل عمليات البحث في وقت التشغيل. (يُرجى العِلم أنّ الانعكاس غير المشروح يتعذّر أيضًا في ظل shrink: true وoptimize: true). أضِف شرحًا إلى نقاط الدخول هذه باستخدام دليل الحفاظ على التعليقات التوضيحية (@UsesReflection أو @UsedByReflection أو @UsedByNative) لكي يحتفظ R8 بأسمائها ويشوّش بقية الوحدة.

اتّباع مبدأ عدم استخدام أي علامات مخصّصة

لا تحتوي وحدة نظام Android الأساسي المثالية على ملفات proguard.flags أو keep.xml مخصّصة. في إصدار نظام Android الأساسي، تكون الغالبية العظمى من قواعد الاحتفاظ المخصّصة غير ضرورية:

  • خطوط الأساس للمنصة وAAPT2: يتم الاحتفاظ بمعظم نقاط الدخول تلقائيًا من خلال خطوط الأساس العامة للمنصة (مثل @Keep وطُرق JNI native و@VisibleForTesting) أو يتم إنشاؤها بواسطة AAPT2 من AndroidManifest.xml وموارد التنسيق (راجِع التعرّف على قواعد الاحتفاظ التلقائية بالمنصة).
  • البدائل المستهدَفة: في الحالات التي يجب فيها الحفاظ على نقاط الدخول، يُفضّل استخدام تعليقات توضيحية خاصة بالموقع الإلكتروني الذي تم فيه التصريح (keepanno و@VisibleForTesting) أو قواعد المكتبة التي تم تصديرها (export_proguard_flags_files: true) أو الربط الثابت للاختبار بدلاً من ملفات .flags المنفصلة (راجِع مقالة التدقيق في قواعد الاحتفاظ الحالية ونقلها).

قبل إضافة ملف proguard.flags أو keep.xml مخصّص أو الاحتفاظ به، تحقَّق مما إذا كانت القاعدة معالَجة تلقائيًا من خلال خطوط الأساس التلقائية أو ما إذا كان يمكن نقلها إلى تعليقات توضيحية للرمز.

التعرّف على قواعد الاحتفاظ التلقائية في المنصة

تُمرِّر أداة Soong تلقائيًا قواعد خط الأساس العامة التالية إلى كل عملية استدعاء لأداة R8 على الاستهدافات التي يتم فيها تجميع DEX (android_app وandroid_test ووحدات java_library القابلة للتثبيت)، والتي تم ضبطها في build/soong/java/dex.go:

  • ‫build/make/core/proguard.flags: تحافظ على الفئات والعناصر التي تمّت إضافة التعليق التوضيحي @com.android.internal.annotations.VisibleForTesting إليها في جميع الحِزم، وعلى تلك التي تمّت إضافة التعليق التوضيحي @VisibleForTesting إليها (androidx.annotation.VisibleForTesting أو com.google.common.annotations.VisibleForTesting) في الحِزم android.** وcom.android.** وcom.google.android.**. ويحتفظ أيضًا @TestApi و@Keep (androidx.annotation وandroid.support.annotation وcom.android.internal.annotations) و@KeepForWeakReference و@WeaklyReferencedCallback و@dalvik.annotation.optimization.**.
  • ‫build/make/core/proguard_basic_keeps.flags: تحتفظ هذه السمة بسمات SourceFile، مثل سمات تتبُّع تسلسل استدعاء الدوال البرمجية، والتعليقات التوضيحية الخاصة بإمكانية رصد وقت التشغيل (RuntimeVisible*Annotations)، وسمات Exceptions وAnnotationDefault، وطُرق native، وعناصر Serializable، وطُرق @JavascriptInterface، ومنشئات Throwable(String)، وحقول Parcelable$CREATOR، وحقول MessageLite في بروتوكول Buffer.
  • ‫build/make/core/proguard/kotlin.flags: يتم تجاهل التحذيرات غير الضارة لبعض التعليقات التوضيحية الوصفية في Kotlin (kotlin.Metadata وkotlin.annotation.{AnnotationRetention,AnnotationTarget,Retention,Target})، كما تتم إزالة التعليقات التوضيحية DebugMetadata في Kotlin من إصدارات الإنشاء.
  • build/make/core/proguard/checknotnull.flags: يستبدل عمليات الاستدعاء الشائعة للمساعد في التحقّق من القيمة الفارغة (com.google.common.base.Preconditions.checkNotNull وdagger.internal.Preconditions.checkNotNull*) بعمليات تحقّق موجزة من القيمة الفارغة في رمز البايت. يحذف هذا الملف Objects.requireNonNull عمدًا للحفاظ على رسائل الاستثناءات الواضحة على مستوى حدود واجهة برمجة التطبيقات الخاصة بالإطار.
  • build/make/core/proguard/enumvalues.flags: تحتفظ بالطريقتَين values وvalueOf لأنواع enum ما لم يتم إيقافهما من خلال إعدادات التصميم.
  • قواعد AAPT2 التي يتم إنشاؤها تلقائيًا: تفحص أداة AAPT2 ملفات AndroidManifest.xml المدمجة وملفات XML الخاصة بالتصميم وملفات XML الخاصة بالإعدادات المفضّلة لإنشاء قواعد إبقاء دقيقة لكل Activity وService وBroadcastReceiver وContentProvider وBackupAgent وApplication وView وPreference وandroid:onClick مسجّلة.

تدقيق قواعد الاحتفاظ الحالية ونقلها

عند تدقيق ملفات proguard.flags أو keep.xml الحالية في مستودع منصة، قيِّم كل قاعدة بالترتيب وفقًا للتسلسل الهرمي التالي المكوّن من أربع خطوات:

الخطوة 1: حذف القواعد الزائدة أو القديمة

احذف القواعد التي تغطيها خطوط الأساس العامة أو بيان AAPT2 وقواعد التنسيق أو الإعدادات التلقائية في R8، بالإضافة إلى القواعد التي تشير إلى فئات أو حِزم لم تعُد متوفرة.

إذا كانت جميع القواعد في ملف .flags مكرّرة، احذف الملف وأزِل proguard_flags_files من Android.bp. إذا كان القسم المتبقي optimize يعيد فقط ذكر الإعدادات التلقائية، أزِل القسم المكرّر ونسِّق ملف الإنشاء باستخدام bpfmt -w Android.bp. عند تنظيف حزمة، تحقَّق مما إذا كانت الوحدات المصاحبة في الأدلة الفرعية (مثل صيغ استهداف Kotlin) تشير إلى ملف العلامات نفسه وعدِّلها معًا.

الخطوة 2: نقل قواعد الاحتفاظ المستندة إلى الاختبار

نقل قواعد الاحتفاظ المستندة إلى الاختبارات من ملفات الإنتاج المخصّصة .flags باستخدام أحد الأنماط التالية:

  • ربط مكتبة التنفيذ بالاختبارات (static_libs): عندما تستخدم android_test تفاصيل التنفيذ الخاصة بالحزمة أو الداخلية في أحد التطبيقات، تكون بنية النظام الأساسي المقترَحة هي وضع ملفات مصدر التطبيق في android_library (MyApp.impl) وربط MyApp.impl بشكل ثابت بكل من MyApp وMyAppTests:

    android_library {
        name: "MyApp.impl",
        srcs: ["src/**/*.java"],
        manifest: "AndroidManifest.xml",
    }
    
    android_app {
        name: "MyApp",
        static_libs: ["MyApp.impl"],
        optimize: {
            obfuscate: true,
            shrink_resources: true,
        },
    }
    
    android_test {
        name: "MyAppTests",
        srcs: ["tests/**/*.java"],
        static_libs: ["MyApp.impl"],
    }
    

    يتيح نمط الوحدات الثلاث هذا تشغيل MyAppTests كاختبار لقياس حالة التطبيق مزوَّد بأدوات ذاتية مع إمكانية الوصول الكامل إلى الفئات الداخلية، كما يتيح MyApp تفعيل obfuscate: true بحرية بدون إيقاف الاختبارات، ويتجنّب إرسال نقاط دخول خاصة بالاختبار فقط في حِزمة APK النهائية، ويزيل الحاجة إلى إعادة إنشاء MyApp عند تغيير رمز الاختبار.

  • إضافة تعليقات توضيحية إلى نقاط ربط الاختبار باستخدام @VisibleForTesting: إذا كان ملف APK خارجي للاختبار يستدعي عددًا صغيرًا من الطرق أو الدوال الإنشائية الداخلية على android_app مستهدف وكان من غير العملي إعادة هيكلة الملف إلى مكتبة .impl، أضِف تعليقات توضيحية إلى تلك التعريفات باستخدام @VisibleForTesting. تحتفظ قاعدة بيانات build/make/core/proguard.flags العالمية @VisibleForTesting بالعناصر في حِزم android.** وcom.android.** وcom.google.android.** تلقائيًا بدون ملفات .flags مخصّصة. (بالنسبة إلى وحدات المورّد خارج مساحات الأسماء هذه، استخدِم @UsedByReflection أو قواعد المكتبة التي تم تصديرها).

  • استخدام trace_references_from على مستوى حدود المكتبة بين الأنظمة الأساسية: عندما يتعذّر على الاختبارات الربط بشكل ثابت بالتنفيذ المستهدف (على سبيل المثال، الاختبارات التي تستخدم خدمات خادم النظام أو ملفات JAR الخاصة بالنظام الأساسي، مثل framework-connectivity)، اضبط trace_references_from على الوحدة النمطية المستهدَفة التي تشير إلى وحدة نمطية مصاحبة لمكتبة Java تحتوي على مصادر الاختبار. يتتبّع R8 جميع الفئات والعناصر التي يشير إليها الرمز الثانوي ويحتفظ بها.

الخطوة 3: تصدير القواعد من المكتبة المالكة

إذا كان java_library أو android_library مشتركًا يتطلّب قواعد الاحتفاظ بالبيانات من أجل الاستخدام الداخلي أو عمليات معاودة الاتصال عبر JNI، عليك تحديد القواعد في هدف المكتبة وضبط export_proguard_flags_files: true:

java_library {
    name: "my-shared-library",
    srcs: ["src/**/*.java"],
    optimize: {
        proguard_flags_files: ["proguard.flags"],
        export_proguard_flags_files: true,
    },
}

تتضمّن جميع أهداف android_app وأهداف المكتبة التي يتم ربطها بشكل ثابت my-shared-library هذه القواعد تلقائيًا، وبالتالي لا تحتاج التطبيقات التابعة إلى تكرارها. عندما تحتاج وحدات متعدّدة في أدلة مختلفة إلى مشاركة مجموعة قواعد غير مرتبطة بمكتبة رموز برمجية واحدة، يمكنك نشر ملف .flags من خلال filegroup صريح في Android.bp حتى تتمكّن الوحدات من الإشارة إلى :my-shared-flags بشكل واضح على مستوى حدود الحزمة. للحصول على إرشادات عامة حول إنشاء قواعد الاحتفاظ بالمستهلكين في المكتبة بدون فرض قيود على تحسين التطبيقات التابعة، يُرجى الاطّلاع على التحسين لمؤلفي المكتبات.

الخطوة 4: إضافة تعليقات توضيحية إلى التصريحات في الرمز المصدر

عند الوصول إلى فئة أو طريقة أو أداة إنشاء أو حقل من خلال الانعكاس أو JNI ولم يتم تضمينها في AAPT2 أو خطوط الأساس العامة، استبدِل قواعد -keep المنفصلة في ملفات .flags بالتعليقات التوضيحية للمصدر من دليل التعليقات التوضيحية الخاصة بالاحتفاظ (راجِع مرجع Javadoc الخاص بـ keepanno):

  • ‫@UsesReflection: ننصحك باستخدام هذه التعليقات التوضيحية عندما تتحكّم في الرمز البرمجي الذي ينفّذ الانعكاس. ضَعها في الموقع الإلكتروني الذي يتم فيه استدعاء الانعكاس لتحديد الفئات أو الطرق أو الحقول المستهدَفة التي يتم الوصول إليها بشكل ديناميكي. بما أنّ @UsesReflection يشفّر تلقائيًا شرطًا مسبقًا بأنّ موقع الاتصال الذي تمت إضافة التعليقات التوضيحية إليه يمكن الوصول إليه، يزيل R8 كلاً من المتصل والهدف الانعكاسي إذا لم يتم استخدام موقع الاتصال.
  • @UsedByReflection: ضعها على الفئات أو الطرق أو الحقول أو الدوال الإنشائية التي يتم إنشاء مثيل لها أو استدعاؤها بشكل انعكاسي بواسطة رمز أو مكتبات خارجية، مثل الفئات التي يتم تحميلها باستخدام Class.forName من Bundle أو Settings، أو فئات المكوّنات الإضافية التي يتم تحميلها عبر أدوات تحميل الفئات الديناميكية. حدِّد kind (مثل KeepItemKind.CLASS_AND_METHODS) وpreconditions (@KeepCondition) وقيود المَعلمات لكي يحتفظ R8 بالعقد الانعكاسي الدقيق فقط ويظل بإمكانه تحسين أو إزالة الأعضاء غير المستخدَمين من الفئة. لا تُضِف @UsedByReflection إلى المكوّنات المسجّلة في البيان، مثل JobService أو BroadcastReceiver، لأنّ أداة AAPT2 تحتفظ بها تلقائيًا.
  • @UsedByNative: يتم وضعها على الطرق أو الحقول التي يتم الوصول إليها من رمز JNI للغة C أو C++‎ باستخدام GetMethodID أو GetStaticMethodID أو GetFieldID.
  • استبدِل @KeepForApi: ضعها على فئات أو عناصر واجهة برمجة التطبيقات في المكتبة التي يجب أن تظل سليمة عند تصغير المكتبة نفسها قبل توزيعها.
  • ‫@Keep (androidx.annotation.Keep): تُستخدَم كبديل عندما لا يكون keepanno مناسبًا. يؤدي تطبيق @Keep على فئة إلى الاحتفاظ بالفئة وجميع عناصرها بدون شروط. يؤدي تطبيق @Keep على طريقة أو حقل إلى إنشاء نقطة دخول غير مشروطة تحتفظ بالعنصر والفئة التي يتضمّنها حتى إذا لم يتم إنشاء مثيل للفئة مطلقًا، بينما يعبّر keepanno عن إمكانية الوصول الشرطية.

لاستخدام تعليقات R8 التوضيحية keepanno في وحدة Soong، اتّبِع الخطوات التالية:

  1. أضِف "keepanno-annotations" إلى libs في Android.bp:

    libs: [
        "keepanno-annotations",
    ],
    
  2. استورِد com.android.tools.r8.keepanno.annotations.* وأضِف تعليقًا توضيحيًا إلى تعريف أو موقع استدعاء في رمز المصدر Java أو Kotlin:

    import com.android.tools.r8.keepanno.annotations.KeepItemKind;
    import com.android.tools.r8.keepanno.annotations.UsedByReflection;
    
    public final class CustomPluginController {
        @UsedByReflection(
            description = "Instantiated via Class.forName from plugin config",
            kind = KeepItemKind.CLASS_AND_METHODS)
        public CustomPluginController(Context context) {
            // ...
        }
    }
    

    يحوّل R8 تعليقات keepanno التوضيحية مباشرةً إلى نموذج قاعدة الاحتفاظ الداخلية، ويزيل التعليقات التوضيحية من ناتج DEX النهائي، ما يؤدي إلى عدم إضافة أي تكلفة إضافية لوقت تشغيل الرمز الثانوي.

تجنُّب الأخطاء الشائعة

توضّح الأقسام التالية المشاكل الشائعة في proguard.flags وkeep.xml ضمن رمز المنصة وكيفية حلّها.

قواعد الاحتفاظ بالمكوّنات الواسعة

# Don't do this:
-keep class * extends android.app.Activity
-keep class * extends android.app.Service
-keep class * extends android.content.BroadcastReceiver
  • سبب التأثير السلبي في الأداء: هذه القاعدة مكرّرة تمامًا مع AAPT2. ونظرًا لأنّه يستخدم حرف بدل بدون التحقّق مما إذا كان المكوّن مسجّلاً في AndroidManifest.xml النهائي الذي تم دمجه، فإنّه يجبر R8 على الاحتفاظ بكل فئة فرعية من Activity أو Service أو BroadcastReceiver تم العثور عليها في أي مكان في مسار الفئة، بما في ذلك مكوّنات المكتبة غير المستخدَمة وأنشطة تصحيح الأخطاء غير المفعَّلة.
  • الحلّ المقترَح: احذف القاعدة. يفحص AAPT2 ملف البيان المدمج وينشئ قواعد -keep دقيقة للعناصر المسجّلة.

قواعد الاحتفاظ بمعالج النقر على XML الواسع

# Don't do this:
-keepclassmembers class * {
    public void *(android.view.View);
}
  • سبب التأثير السلبي في الأداء: يؤدي الاحتفاظ بكل طريقة public void *(View) في كل فئة في الوحدة إلى منع R8 من إزالة أي طريقة أو تضمينها إذا كانت تقبل مَعلمة View واحدة.
  • الحلّ المقترَح: احذف القاعدة. يفحص AAPT2 ملفات XML الخاصة بالتصميم ويضع قواعد استبقاء مستهدَفة للطُرق التي تشير إليها سمات android:onClick.

قواعد الاحتفاظ بعناصر إرجاع القيمة وعناصر تحديد القيمة في "العرض الواسع"

# Don't do this:
-keep public class * extends android.view.View {
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
    public void set*(...);
    public *** get*();
}
  • سبب تأثيرها سلبًا في الأداء: تجبر هذه القاعدة أداة R8 على الاحتفاظ بكل دوال الجلب والتعديل في كل فئة فرعية من View في التطبيق وجميع المكتبات المرتبطة (بما في ذلك مكتبات AndroidX وMaterial)، ما يمنع إزالة الدوال غير المستخدَمة وتضمينها في رمز واجهة المستخدم.
  • الحلّ المقترَح: احذف القاعدة. يحتفظ AAPT2 حاليًا بمنشئات لفئات View المخصّصة التي يتم تضخيمها من ملفات XML للتصميم. إذا كان الرمز البرمجي يحرك سمة عرض باستخدام أسماء سلاسل انعكاسية، مثل ObjectAnimator.ofFloat(view, "translationZ", ...)، استبدِل اسم السلسلة بمرجع سمة مكتوب (View.TRANSLATION_Z أو FloatProperty أو IntProperty مخصّص) لتجنُّب الانعكاس تمامًا. إذا كان لا يمكن تجنُّب الوصول إلى سمة انعكاسية، أضِف التعليق التوضيحي @UsedByReflection إلى أداة الجلب أو الضبط المحدّدة.

إيقاف التحسين أو التشويش الشامل

# Don't do this in proguard.flags:
-dontoptimize
-dontshrink
-dontobfuscate
  • سبب التأثير السلبي في الأداء: يؤدي وضع -dontoptimize أو -dontshrink داخل ملف .flags إلى تجاهل إعدادات Android.bp الخاصة بالوحدة النمطية بدون تنبيه، كما يؤدي إلى إيقاف عمليات التحسين على مستوى الهدف بأكمله. والأسوأ من ذلك، إذا صدّرت إحدى المكتبات ملف .flags يحتوي على -dontoptimize، سيؤدي ذلك إلى إيقاف تحسين R8 لكل android_app لاحق يربط المكتبة.
  • الحلّ المقترَح: أزِل -dontoptimize و-dontshrink و-dontobfuscate من ملفات .flags. يمكنك التحكّم في سلوك التحسين بشكلٍ صريح في Android.bp باستخدام الحظر optimize (shrink وoptimize وobfuscate) في الاستهداف النهائي.

علامات التشخيص في القواعد التي تمّت الموافقة عليها

# Don't do this in proguard.flags:
-verbose
-printmapping proguard.map
-printusage usage.txt
-printconfiguration config.txt
  • سبب حدوث المشكلة في عملية الإنشاء: تؤدي علامات التشخيص إلى إغراق سجلّات الإنشاء أو محاولة كتابة ملفات الإخراج إلى مسارات محلية أثناء عمليات إنشاء Soong في بيئة معزولة.
  • الحل المقترَح: احذف هذه العلامات من ملفات .flags التي تم إرسالها. تكتب أداة Soong تلقائيًا مخرجات الربط والاستخدام الخاصة بأداة R8، مثل proguard_dictionary وproguard_usage.zip، إلى الدليل الوسيط للوحدة ضمن out/soong/.intermediates/.

قواعد الاحتفاظ بالإنتاج للرمز البرمجي للاختبار

  • سبب التأثير السلبي في الأداء: تؤدي إضافة قواعد -keep مخصّصة للوصول إلى الاختبار فقط إلى إبقاء الرمز البرمجي غير مشفَّر والاحتفاظ به في إصدارات الإنتاج. على الرغم من أنّ إضافة التعليقات التوضيحية إلى الطرق باستخدام @VisibleForTesting أو ضبط trace_references_from يؤدي إلى تجنُّب الحفاظ على قواعد -keep اليدوية، إلا أنّ هذه الرموز تظل مضمّنة في الرمز الثنائي الخاص بالإنتاج.
  • الحلّ المقترَح: لإبقاء نقاط الدخول المخصّصة للاختبار فقط خارج حِزمة APK الخاصة بالإنتاج، قسِّم التطبيق إلى مكتبة .impl واربطها بحِزمة APK للاختبار ذاتية القياس باستخدام static_libs، كما هو موضّح في الخطوة 2: نقل قواعد الإبقاء المستندة إلى الاختبار.

أحرف بدل شاملة للمراجع في ملف keep.xml

<!-- Don't do this in res/raw/keep.xml: -->
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@raw/*,@drawable/*,@string/*" />
  • سبب التأثير السلبي في الأداء: تحتفظ أحرف البدل الشاملة بكل مورد من النوع المطابق في الوحدة النمطية والموارد التابعة لها، ما يؤدي إلى إبطال تصغير الموارد (shrink_resources: true) وتضخيم حزمة APK وجدول resources.arsc الذي تم ربطه.
  • الإجراء المقترَح لحلّ المشكلة:
    • تفضيل مراجع الموارد الثابتة على keep.xml: تجنَّب استخدام طريقة Resources.getIdentifier للبحث عن مجموعة محدودة من الموارد بشكل ديناميكي، مثل سلاسل التجارب المرقمة أو الرسومات المتوافقة مع سمة معيّنة. بدلاً من ذلك، استخدِم عبارة switch في وقت الترجمة أو قم بتعيين الثوابت الثابتة R.id أو R.string أو R.drawable. تتيح المراجع الثابتة لأداتَي R8 وAAPT2 تتبُّع الموارد النشطة بدقة، وإزالة النفقات العامة لعمليات البحث عن السلاسل في وقت التشغيل، وإلغاء الحاجة إلى keep.xml تمامًا.
    • إدراج أسماء مراجع محدّدة: إذا كان البحث الديناميكي مطلوبًا، مثلاً من خلال قارئ تراخيص تابع لجهة خارجية، أدرِج معرّفات المراجع الدقيقة في tools:keep (على سبيل المثال، tools:keep="@raw/third_party_licenses").
    • حذف ملفات keep.xml المكرّرة: إذا كانت الموارد المدرَجة تتم الإشارة إليها بشكل ثابت في الرمز (R.raw.foo) أو XML (@raw/foo)، سيحتفظ بها أداة التصغير تلقائيًا، لذا يمكنك حذف res/raw/keep.xml.

التحقّق من تغييرات القواعد وتدقيقها

عند إزالة قواعد الاحتفاظ أو تضييق نطاقها في proguard.flags أو keep.xml، تأكَّد من أنّ عملية إنشاء الوحدة تتم بدون أخطاء، وأنّها تجتاز اختبارات الوحدات واختبارات الأجهزة، وتحتفظ بجميع نقاط الدخول المطلوبة.

إنشاء الوحدات واختبارها

  1. أنشئ الوحدة بشكل سليم للتأكّد من أنّ R8 وAAPT2 يكتملان بدون تحذيرات بشأن مراجع غير متوفّرة:

    m <MODULE_NAME>
    
  2. نفِّذ اختبارات الوحدة والاختبارات الآلية للوحدة باستخدام atest:

    atest <TEST_MODULE_NAME>
    
  3. بالنسبة إلى تطبيقات النظام أو الخدمات ذات الامتيازات أو الوحدات النمطية التي تعتمد على الأجهزة، شغِّل اختبارات الأجهزة واختبارات واجهة المستخدم على الأجهزة المادية المستهدَفة أو في مختبر اختبار الأجهزة الخاص بك لتنفيذ مسارات الانعكاس في وقت التشغيل وعمليات ربط الاتصال بين العمليات (IPC) وتضخيم الموارد.

فحص الاختلافات في DEX والموارد

قارِن حِزمة APK أو JAR التي تم تجميعها قبل وبعد تغييرات القواعد للتأكّد من أنّ R8 يزيل الرموز البرمجية والموارد غير المستخدَمة بدون إزالة نقاط الدخول المتوقّعة:

  • استخدِم apkanalyzer أو dexdump لفحص الفئات والطُرق والحقول المحتفظ بها في حِزم APK أو ملفات DEX الناتجة:

    apkanalyzer dex packages $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    
  • عند تعديل keep.xml أو تفعيل shrink_resources، استخدِم aapt2 dump resources للتحقّق من أنّ الإصدار يزيل الموارد غير المستخدَمة ويحتفظ بالموارد المطلوبة:

    aapt2 dump resources $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    

تحليل نصف قطر الاحتفاظ بالبيانات وتضمين القواعد باستخدام R8

يتضمّن برنامج الترجمة البرمجية R8 مفتوح المصدر أداة تحليل نطاق الاحتفاظ بالبيانات (تتوفّر أيضًا في &quot;استوديو Android&quot; وGradle باسم أداة تحليل إعدادات R8) تقيس التأثير الدقيق لكل قاعدة احتفاظ بالبيانات أثناء الترجمة البرمجية. يدمج Soong هذا المحلّل مباشرةً في إصدار نظام Android الأساسي (يتم ضبطه في build/soong/java/dex.go).

لتحليل قواعد الاحتفاظ بوحدة نظام أساسي أو في إصدار كامل، اتّبِع الخطوات التالية:

  1. نفِّذ الإصدار باستخدام متغيّر البيئة R8_DUMP_KEEP_RADIUS=true:

    R8_DUMP_KEEP_RADIUS=true m <MODULE_NAME>
    

    عند ضبط R8_DUMP_KEEP_RADIUS=true، يطلب Soong من R8 تسجيل مقاييس قواعد الاحتفاظ في ملف r8keepradius.pb وسيط لكل وحدة تم تجميعها ضمن out/soong/.intermediates/. بالنسبة إلى كل قاعدة حفظ وتعليق توضيحي keepanno، يسجّل R8 ما يلي:

    • نطاق الحفاظ الفوري: الفئات والحقول والطرق الدقيقة التي تحتفظ بها القاعدة، بالإضافة إلى القيود المحدّدة التي تفرضها على التصغير أو التحسين أو التشويش
    • الاستيعاب: قواعد الاحتفاظ أو التعليقات التوضيحية الأخرى التي تحتفظ بالعناصر نفسها إذا كانت قاعدة مخصّصة مشمولة بالكامل بقاعدة AAPT2 أو قاعدة أساسية عامة، يمكنك حذفها بأمان.
    • القواعد على مستوى الحزمة والقواعد العامة: القواعد التي تستخدم أحرف بدل واسعة النطاق على مستوى الحزمة أو تطبّق توجيهات الإعدادات العامة.
  2. حوِّل ناتج r8keepradius.pb إلى تقرير HTML تفاعلي باستخدام KeepRadiusHtmlReportGenerator (المضمّن في prebuilts/r8/r8.jar):

    # Generate an HTML report for a single module
    INTERMEDIATES=out/soong/.intermediates/packages/apps/<APP_NAME>/<APP_NAME>
    java -cp prebuilts/r8/r8.jar \
        com.android.tools.r8.keepradius.KeepRadiusHtmlReportGenerator \
        $INTERMEDIATES/android_common/r8keepradius.pb \
        keep_radius_report.html
    
    # Scan all built modules under out/soong/.intermediates and generate
    # per-module HTML reports plus an aggregate keepradius.html summary
    java -cp prebuilts/r8/r8.jar \
        com.android.tools.r8.keepradius.KeepRadiusHtmlReportGenerator \
        out/soong/.intermediates \
        out/keep_radius_reports
    

    عند توفير دليل، يتنقّل KeepRadiusHtmlReportGenerator بين جميع*keepradius*.pb الملفات، وينشئ تقرير HTML لكل وحدة، وينشئ out/keep_radius_reports/keepradius.html يلخّص عدد العناصر النشطة، وعدد العناصر المحفوظة، وقواعد الحفظ ذات النطاق الأوسع على مستوى الإصدار. لمزيد من المعلومات حول تفسير نتائج التصغير والتحسين والتشويش في التقرير الذي تم إنشاؤه، راجِع مقالة استخدام أداة تحليل إعدادات R8.

تتيح أداة Soong أيضًا استخدام متغيرَي بيئة تشخيص إضافيَين في R8 في build/soong/java/dex.go:

  • ‫R8_DUMP_INPUT=true: يكتب r8inputs.zip إلى الدليل الوسيط للوحدة النمطية الذي يحتوي على جميع ملفات JAR المدخلة وملفات JAR الخاصة بالمكتبة وإعدادات ProGuard المدمجة لإعادة إنتاج R8 المستقل.
  • R8_DUMP_PERFETTO_TRACE=true: يكتب r8trace.ptrace في الدليل الوسيط للوحدة من أجل فحص عمليات تجميع R8 في Perfetto.

التحقّق من صحة قواعد المستهلك في المكتبة

عندما تحتفظ عمليات التصدير java_library أو android_library بالقواعد للمستهلكين في المراحل اللاحقة (export_proguard_flags_files: true)، يجب ألا تتضمّن هذه القواعد علامات عامة تغيّر عملية التحسين أو توقفها للتطبيق المستهلك.

توفّر أداة R8 أداة تحليل مفتوحة المصدر لقواعد الاحتفاظ بالبيانات (ProcessKeepRules)، يتم عرضها في إصدار نظام Android الأساسي على أنّها أداة المضيف process-keep-rules (محدّدة في prebuilts/r8/Android.bp):

# Build the R8 keep rules validator host binary
m process-keep-rules

# Validate one or more ProGuard configuration files
out/host/linux-x86/bin/process-keep-rules <PROGUARD_FLAGS_PATH>

تحلّل أداة process-keep-rules كل ملف إعداد وتتعذّر مع عرض معلومات تشخيصية خاصة بالملف والسطر إذا صادفت توجيهات غير مسموح بها في قواعد مستخدمي المكتبة. تشمل التوجيهات غير المسموح بها عمليات التحسين أو التصغير أو التشويش على مستوى العالم (مثل -dontoptimize أو -dontshrink)، وعمليات إعادة تجميع الحِزم وعلامات تعديل الوصول، وعلامات التشخيص أو الربط، و-keepattributes على مستوى التطبيق.

تدقيق شجرات المصدر باستخدام pgaudit.py

للتدقيق في أدلة المصدر غير المنشأة إلى جانب أدوات تحليل R8 أثناء وقت الإنشاء، تتضمّن شجرة نظام Android الأساسي النص البرمجي pgaudit.py ضمن build/make/core/proguard/tools/. تفحص أداة التحليل الثابت هذه ملفات Android.bp وproguard.flags وkeep.xml في جميع المستودعات لتحديد القواعد التي تغطيها AAPT2 أو خطوط الأساس للمنصات العالمية، وأحرف البدل العامة، وعمليات إلغاء -dontoptimize، وقواعد الإبقاء المخصصة للاختبار فقط:

# Audit a single package directory
./build/make/core/proguard/tools/pgaudit.py packages/apps/Provision/

# Scan a subsystem tree and write a structured JSON report
./build/make/core/proguard/tools/pgaudit.py packages/ --json=audit_report.json

يمكن لفِرق المنصات ومصنّعي المعدات الأصلية الجمع بين pgaudit.py لتحديد المشاكل بسرعة في شجرة المصدر، وprocess-keep-rules للتحقّق من صحة قواعد المكتبة التي تم تصديرها، وR8_DUMP_KEEP_RADIUS=true مع KeepRadiusHtmlReportGenerator لقياس نطاق الاحتفاظ الدقيق بالصفوف والحقول والطرق لكل قاعدة في الإصدار.