الأدوات والأداء

قواعد الاحتفاظ الآمن بالنوع وتحسين DEX

سماعات

لمحة

لمحة عن المحاضرة

غالبًا ما يتم طرح تطبيقات Android بدون تحسين كافٍ بسبب قواعد الاحتفاظ المحافظة للغاية، والتي غالبًا ما تكون من مكتبات تابعة لجهات خارجية، ما يؤدي إلى تضخّم ملفات APK وبطء عمليات التشغيل على البارد وانخفاض أداء وقت التشغيل.

مع الحدود الدنيا الجديدة للأداء في Google Play لتحسين DEX، يعتمد ظهور التطبيق الآن بشكل مباشر على البقاء في صدارة منحنى التحسين.

تتناول هذه الجلسة سير عمل R8 حديثًا لمعالجة مشكلة تضخّم القواعد. ستتعلّم كيفية:

  • استخدِم أداة تحليل إعدادات R8 ومهارة تحليل R8 لتحديد أعباء القواعد القديمة.
  • الانتقال من قواعد الاحتفاظ العامة التي تستخدم أحرف البدل إلى تعليقات توضيحية دقيقة يتم إنشاؤها وتمنع أخطاء الكتابة
  • يمكنك منع تراجع مستوى التحسين باستخدام أدوات Android CLI الجديدة.

ملخّص

  • يفرض Google Play حدودًا قصوى لجودة التطبيقات تتطلّب تحسين الرموز البرمجية بنسبة تزيد عن %25. ويؤدي استيفاؤها إلى تقليل استخدام الذاكرة وتحسين سرعة بدء التشغيل ونِسب أخطاء ANR.
  • تُنشئ ميزة "محلّل إعدادات R8" تقرير HTML يعرض قواعد الاحتفاظ بالبيانات التي تحتفظ بالرمز البرمجي. وتُبرز هذه الميزة القواعد المضمّنة وغير المستخدَمة وتقيس إمكانية التحسين قبل التجميع.
  • قواعد الاحتفاظ الواسعة النطاق، التي غالبًا ما يتم تقديمها لمنع تعطُّل التطبيق بسبب الانعكاس أو التي يتم اكتسابها من المكتبات، تؤدي إلى تقويض تحسين التطبيق. يمكن لقاعدة أحرف بدل واحدة على مستوى الحزمة الاحتفاظ بالعشرات من الفئات غير المستخدَمة.
  • تستخدم Jetpack Keep أداة lint لرصد الانعكاس وإنشاء تعليقات توضيحية دقيقة وقواعد احتفاظ شرطية مباشرةً من الرمز البرمجي. ويصدِّر قواعد المستهلك العادية المتوافقة مع إصدارات R8 الأقدم.
  • تفحص واجهة سطر الأوامر في Android حِزم التطبيقات وملفات AAR باستخدام منطق Google Play الدقيق بدون تحميل. ويضع علامة على القواعد التي تتضمّن مشاكل ويصدّر تقارير JSON لإجراء اختبارات الانحدار المبرمَجة.

الموارد

Gemini Notebook

المواضيع

  • dex-optimization
  • r8
  • keep-rules
  • jetpack-keep
  • android-cli
  • أداء التطبيق

النص

تنزيل النص

مقدمة عن تحسين DEX

  • مرحبًا بالجميع!

  • اسمي "بريانا"، وأنا عضو في فريق Android DevRel.

  • وأنا "كريس كريك"، وأعمل في فريق أداء Android.

  • مرحبًا بك في قواعد الاحتفاظ الآمنة من حيث النوع وتحسين DEX.

  • سنتحدّث اليوم عن مفهوم تحسين DEX، وأهميته، وبعض التحديثات الأخيرة الرائعة.

  • لفترة طويلة، كان يُنظر إلى تحسين تجربة الموظفين الرقمية على أنّه أمر جيد، ولكنه ليس ضروريًا.

  • لذا، قد تضبطه مرة واحدة ثم تتركه في مكانه، أو قد تضبطه عندما يكون لديك بعض الوقت المخصّص لذلك.

  • ومع ذلك، في ظل القيود الكبيرة التي يواجهها قطاع الأجهزة الجوّالة في توفير الأجهزة والتي تؤثر في توفّر ذاكرة الأجهزة، من الضروري تحسين ملفات DEX بشكل جيد لكي يستهلك تطبيقك قدرًا أقل من الذاكرة، ويبدأ بشكل أسرع، ويقلل من أخطاء ANR، ويحسّن أداء العرض ووقت التشغيل بشكل عام.

  • للحفاظ على سلامة منظومة Android المتكاملة وضمان استمرار تطبيقاتك في تقديم تجارب متميّزة، أعلن Google Play عن متطلبات جديدة لجودة التطبيقات تتضمّن حدودًا قصوى للأداء سيتم تطبيقها في فبراير.

  • ويشمل ذلك الحدود الدنيا لتحسين رمز DEX.

  • يجب أن تحقّق التطبيقات نسبة أعلى من %25 في ما يتعلّق بالتقليص والتحسين وإخفاء مفاتيح فك التشفير.

  • إذًا، ما تأثير ذلك فيك كمطوّر برامج Android؟

  • لا يؤثّر رمز DEX غير المحسَّن سلبًا في تجربة المستخدم فحسب، بل قد يؤثّر أيضًا في إمكانية ظهور تطبيقك ونشره على "متجر Google Play".

  • ومع ذلك، أنت في المكان المناسب، وبنهاية هذه المحادثة، سيكون لديك الأدوات ليس فقط لتلبية هذه المتطلبات، بل لتجاوزها.

  • يمكن تحسين رمز DEX باستخدام R8، وهو مُحسِّن تطبيقات Android.

  • وعلى الرغم من أنّ استخدام أداة R8 ليس إلزاميًا إذا كان بإمكان أداة تحسين أخرى رفع أداء تطبيقك فوق هذه الحدود، فإنّها المسار المقترَح والمضمَّن مباشرةً في سلسلة أدوات Android.

  • وعلى الرغم من أنّ حزمة التطبيق ورموز DEX المحسَّنة بشكل جيد تستخدم ذاكرة أقل، فإنّ المزايا الأساسية هنا لا تزال مرتبطة بالأداء، مثل تقليل أخطاء ANR وتسريع بدء التشغيل وتحسين أداء وقت التشغيل.

  • ويؤدي تحسين تطبيقك إلى نتائج إيجابية حقيقية ومثبتة لتطبيقك ومستخدميه.

  • على سبيل المثال، من خلال استخدام الوضع الكامل لأداة R8 وتحسين قواعد الاحتفاظ بالبيانات، تمكّن تطبيق Grab من تحسين الأداء بنسبة %78 وتقليص حجم التطبيق بنسبة %79 وإخفاء الرموز البرمجية بنسبة %82.

  • قد تنظرون إلى هذه الشريحة وتتساءلون: "حسنًا، هذه الأرقام مرتفعة جدًا، ولكن ما الذي تعنيه فعليًا؟"

  • ونتيجة لهذه التحسينات، إلى جانب "الملفات الشخصية للمرجع"، تم تقليل أحداث ANR بنسبة %32 وتحسين وقت بدء التشغيل بنسبة %35.

  • عندما تبدأ التطبيقات الرائدة في استخدام R8، فإنّها تشهد انخفاضًا في حجم DEX بنسبة %38 في المتوسط، وانخفاضًا بنسبة %16 في أخطاء ANR التي يلاحظها المستخدمون.

  • وإذا كنت تستخدم R8 حاليًا، فلدينا أخبار رائعة لك.

  • بالنسبة إلى التطبيقات التي فعّلت أداة R8، ثم اتّخذت خطوات لتحسين مستويات التحسين والتقليص والتمويه إلى أكثر من %50، فإنّها تشهد في المتوسط انخفاضًا إضافيًا بنسبة %25 في حجم ملفات DEX، وانخفاضًا بنسبة %4 في وقت بدء التشغيل، وانخفاضًا إضافيًا بنسبة %17 في أخطاء ANR التي لاحظها المستخدمون، بالإضافة إلى نسبة %16 التي عرضناها في الشريحة السابقة.

  • لذلك، نريد اليوم أن نطلعكم على كيفية تحقيق أرقام مماثلة.

  • سنتناول الأدوات التي يمكن أن تساعدك في تحسين رمز DEX والانتقال من قواعد الاحتفاظ الواسعة جدًا والتي تم ضبطها بشكل خاطئ إلى قواعد محدّدة وضيقة.

  • أولاً، نودّ أيضًا الإشارة إلى أنّ أداة R8 متوفّرة منذ سنوات عديدة.

  • وندرك أنّه على الرغم من أنّ هذه الميزة قد أتاحت تحسينًا عالي التأثير، إلا أنّها لا تزال تتطلّب الكثير من العناية لضبطها.

  • تعني الاستدعاءات الانعكاسية أنّ المدخلات لا يمكن توقّعها، ويمكن أن تؤدي إلى تعطُّل التطبيق أثناء التشغيل.

  • وقد دفع ذلك كلاً من فِرق التطبيقات وفِرق المكتبات في الماضي إلى إضافة قواعد احتفاظ واسعة النطاق لمجرد الحفاظ على سلامة الأمور.

  • نعلم أنّ %41 من أهم التطبيقات لا تبلغ مستويات التحسين التي حدّدها Google Play والبالغة %25، ولكننا نعلم أيضًا أنّ %13 من أهم التطبيقات تحقّق حاليًا ثلاثة أضعاف هذا الحدّ.

  • وهذا الرقم تحقّقه تطبيقات من مجموعة واسعة من المجالات وتجارب المستخدمين.

  • ويمكن تحقيق نتائج تحسين عالية لتطبيقك، طالما أنّ لديك الأدوات المناسبة ومستوى الرؤية المناسب لإدارة قواعد الاحتفاظ الخاصة بك وتلك المكتسَبة من المكتبات.

  • وقبل أن نتعمّق في الأدوات والتحسينات الجديدة التي ستساعد في ذلك، أردنا أيضًا تسليط الضوء على بعض التحسينات الأخرى في أداة R8 من العام الماضي والتي تجعل عملية التحسين هذه أسهل قليلاً.

  • أولاً، تتضمّن نماذج التطبيقات الجديدة نطاق حزمة، وهو مفيد لضمان الحدّ الأدنى من مستوى التحسين منذ البداية.

  • أصبحت ميزة "محلّل إعدادات R8"، التي سنتحدث عنها بعد قليل، متوافقة الآن مع الإصدارات القديمة من "مكوّن Android الإضافي لنظام Gradle".

  • وهي متاحة أيضًا بشكل غير رسمي في Bazel.

  • وفي الإصدار 9.2 من AGP، أصبحت وظائف coroutine الأساسية أسرع بمرتين.

  • ونتيجةً لتحسينات R8 التي تم إجراؤها في الإصدار 9.2 من المكوّن الإضافي لنظام Gradle المتوافق مع Android، شهد تطبيق Tinder انخفاضًا بنسبة %13 في أخطاء ANR بعد الترقية من الإصدار 9.0.1.

  • يوفّر الإصدار 9.3 من AGP أيضًا واجهة برمجة تطبيقات R8 بتصميم جديد مع تسمية أوضح وإعدادات تلقائية أبسط للقواعد التلقائية وتقليص الموارد، بالإضافة إلى إتاحة دليل ملفات المصدر في AGP لقواعد الاحتفاظ بالملفات.

    android {
        buildTypes {
            release {
                optimization {
                    // no need to specify default
                    // rules and resource shrinking
                    enabled = true
                }
            }
        }
        ...
    }
    android {
        buildTypes {
            release {
                // Enables code optimizations.
                isMinifyEnabled = true
    
                // Enables resource shrinking.
                isShrinkResources = true
    
                proguardFiles(
                    // Default rules.
                    getDefaultProguardFile(
                        "proguard-android-optimize.txt"
                    ),
                    ... // list out each file with keep rules
                )
                ...
            }
        }
        ...
    }
  • التعديلات التي سنتناولها اليوم هي أيضًا تحسينات على تجربة المطوّرين.

  • سنبدأ بكيفية الاستفادة من "أداة تحليل إعدادات R8" ومهارة R8 لتحديد أعباء القواعد القديمة وتحديد مسار التحسين.

  • بعد ذلك، سيتناول "كريس" كيفية الانتقال من قواعد الاحتفاظ العامة التي تستخدم أحرف البدل إلى التعليقات التوضيحية الجديدة الدقيقة والآمنة من حيث النوع والتي يتم إنشاؤها تلقائيًا.

  • وأخيرًا، سنلقي نظرة على كيفية استخدام أدوات التحقّق الجديدة من واجهة سطر الأوامر لضمان التحسين المكثّف بدون حدوث أعطال.

نظرة عامة على أداة تحليل إعدادات R8

  • لذا، يُعدّ R8 Config Analyzer المكان المثالي لبدء التحقيق في تحسين DEX.

  • إذا لم يسبق لك استخدام الأداة، إليك شكلها.

  • وهو تقرير HTML يعرض معلومات حول إعدادات R8.

  • هذا التقرير مخصّص لتطبيقي الذي يتضمّن مكتبة باسم Analytics Bloat.

  • وكما تتوقعون، هذا ليس تطبيقًا حقيقيًا أو مكتبة حقيقية، ولكن تم تمييزه باللون الأزرق في الأعلى.

  • تقيس أداة "محلّل الإعدادات" ما يلي: التقليص، وهو النسبة المئوية للفئات والطرق والحقول التي لا تمنع قواعد الاحتفاظ بالبيانات أداة R8 من تقليصها.

  • ويتم احتساب ذلك بعد أن يُجري R8 أول عملية تقليص.

  • ويقيس أيضًا التحسين، وهو النسبة المئوية للرمز البرمجي المفتوح لتحسينات البرنامج بالكامل، مثل تضمين الطرق ودمج الفئات.

  • والتشويش، وهو النسبة المئوية للأعضاء الذين يمكن تشويشهم أو إعادة تسميتهم إلى معرّفات أقصر.

  • أودّ أيضًا أن أشير إلى أنّه نظرًا لأنّ أداة R8 Config Analyzer تحسب هذه النتائج قبل عمليات التحسين النهائية وتجميع ملفات DEX، فإنّ القيم التي تظهر في هذا التقرير للتحسين والتقليص وإخفاء مفاتيح فك التشفير لن تتطابق مع القيم التي تظهر في Play Console تمامًا.

  • ويعود السبب في ذلك إلى أنّ Google Play يقيس الناتج النهائي لملفات DEX.

  • والسبب في هذا الاختلاف هو التأكّد من أنّه عند تطوير التطبيق، يمكنك الوصول إلى تقرير ملاحظات وآراء محلي سريع.

  • ومع ذلك، فإنّ النتائج التي تظهر في أداة تحليل إعدادات R8 لا تزال مفيدة جدًا للتحقّق من إمكانية تحسين ملفات DEX قبل تحميلها إلى "متجر Google Play".

  • يتضمّن التقرير أيضًا جدول "تحليل قواعد الاحتفاظ بالبيانات"، وهو طريقة العرض الرئيسية التي تظهر لك عند الدخول إلى التقرير.

  • ومن هناك، يمكنك النقر على أي قاعدة والاطّلاع على العدد الدقيق للفئات والطرق والحقول التي تحتفظ بها قاعدة معيّنة، حتى تتمكّن من معرفة القواعد التي تحتفظ بأكثر مما ينبغي.

  • وتتضمّن أيضًا قواعد فرعية، وهي ما يتم التركيز عليه هنا.

  • وهذه هي القواعد التي تتداخل مع قاعدة أوسع.

  • ويتم تمييز هذه الكلمات الرئيسية حتى تتمكّن من إزالة الكلمة الرئيسية العامة والاحتفاظ بالكلمة الرئيسية الأكثر تحديدًا.

  • وتعرض لك أيضًا القواعد غير المستخدَمة، وهي القواعد التي يمكن حذفها بدون أي مخاطر على استقرار تطبيقك.

  • لذلك، تم تصميم "أداة تحليل الإعدادات" لمعالجة بعض المشاكل بدءًا من تراكم القواعد القديمة.

  • في السابق، كان حذف القواعد القديمة ينطوي على بعض المخاطر بسبب عدم توفّر إمكانية الاطّلاع على كل قاعدة على حدة.

  • لذلك، كانت الفِرق تتركها وشأنها أو تضيف أحرف بدل أوسع نطاقًا.

  • ثانيًا، يساعدك في اكتشاف الثغرات في قواعد المكتبة.

  • لذلك، عند إضافة حزمة SDK، كان من الصعب في السابق معرفة ما إذا كانت القاعدة المضمّنة في تلك المكتبة واسعة النطاق بشكل مفرط أو ما إذا كانت هناك حاجة إليها، مثلاً، بعد 10 إصدارات أو نحو ذلك.

  • وسنتناول أيضًا المزيد عن المكتبات بعد قليل.

  • ثالثًا، تساعدك في تقييم تأثير التحسين.

  • في السابق، لم تكن هناك طريقة سهلة لمعرفة ما إذا كانت القاعدة تحتفظ بفئتَين أو 2,000 فئة.

  • لنلقِ نظرة على طريقة عمل R8 في سياق تطبيقي والمكتبة التي اطّلعنا عليها سابقًا.

  • لدينا هنا وحدة تطبيق وثلاث مكتبات.

  • وانتبه جيدًا إلى المكتبة الثالثة على اليمين.

  • هذه هي مكتبة Analytics Bloat التي تتضمّن قاعدة احتفاظ على مستوى الحزمة.

  • أولاً، يحدّد R8 نقطة الدخول الجذرية من جميع قواعد الاحتفاظ المدمجة، والتي تم تمييزها هنا باللون الأصفر.

  • وبشكل عام، يجب أن تكون هذه هي نقاط الدخول الرئيسية للتطبيق، مثل الفئات في ملف البيان.

  • وبسبب قاعدة الاحتفاظ بالرموز النائبة في مكتبة Analytics Bloat، يتم التعامل مع كل فئة في المكتبة كنقطة دخول.

  • بعد ذلك، يتتبّع R8 إمكانية الوصول إلى جميع أجزاء الرسم البياني للاستدعاءات من تلك الجذور.

  • يتم تمييز أي فئة يمكن الوصول إليها من الجذر باللون الأخضر للاحتفاظ بها، بينما يتم تصنيف الفئات التي لم تتم الإشارة إليها على أنّها رمز برمجي غير مستخدَم ويتم تمييزها باللون الأحمر هنا.

  • بعد ذلك، ينفّذ R8 عملية إزالة الرموز البرمجية غير المستخدَمة ويحذف الرموز البرمجية غير المُشار إليها.

  • لذا، بعد عملية "هزّ الشجرة" هذه، أو عملية التقليص الأولى، هذا ما يتبقّى.

  • وحصل تطبيقنا على نتيجة تقلّص بلغت %42.5، وهي أعلى من الحدّ الأدنى الذي يبلغ %25 على Google Play، ولكنّها لا تزال غير مناسبة لما نريده.

  • ويرجع ذلك إلى حد كبير إلى قاعدة الاحتفاظ على مستوى الحزمة من مكتبة الإحصاءات.

  • وهذا يجعل 17 فقط من الفئات الـ 40 المتبقية يمكن تقليصها بشكل أكبر.

  • النقطة التي أريد توضيحها هنا هي أنّ قاعدة احتفاظ واحدة سيئة يمكن أن تسبّب مشاكل كبيرة في تحسين تطبيقك.

  • في ضوء ما سبق، لنركّز سريعًا على المكتبات.

  • خلال العام الماضي، استثمر فريقنا الكثير من الوقت في فهم كيفية تأثير قواعد الاحتفاظ بالمستهلكين في المكتبة على تحسين المنظومة المتكاملة.

  • ستتضمّن العديد من المكتبات قواعد واسعة جدًا من أجل تحقيق الاستقرار.

  • أو سيحدّدون أيضًا قواعد عامة في مستنداتهم ليضيفها مطوّرو التطبيقات إلى تطبيقاتهم بأنفسهم.

  • ولكنّ استخدام قواعد عامة ليس ضروريًا لتحقيق الاستقرار، ويؤثر سلبًا في الأداء، حيث يتحمّل مطوّرو التطبيقات التكاليف بشكل أساسي.

  • لقد رأينا حالات يتم فيها الاحتفاظ بالمكتبة بأكملها من خلال قاعدة احتفاظ على مستوى الحزمة مثل مكتبة تضخيم الإحصاءات، وفي حالة هذه المكتبة، كان لذلك تأثيرات بعيدة المدى على إمكانية تحسين تطبيقي.

  • أريد أيضًا أن أذكر أنّ حِزم تطوير البرامج (SDK) الخاصة بنا في Google لم تكن محصّنة ضد هذه المشكلة، وقد أصلحت أكثر من اثنتي عشرة مكتبة من مكتبات Google قواعد الاحتفاظ الواسعة النطاق في العام الماضي.

  • لذا، إذا أردت استخدام أداة تحليل الإعدادات، فهي مدمجة مباشرةً في الإصدار 9.3 من AGP، ولكنها متاحة أيضًا للإصدارات الأقدم إذا قمت بترقية إصدار R8.

  • لذا، سيؤدي تشغيل هذا الأمر إلى إنشاء تقرير HTML الذي عرضناه، والذي يعرض قواعد الاحتفاظ بالبيانات، والتأثيرات في تقليص حجم التطبيق، والتحسين، والتمويه للقواعد الفردية وتطبيقك ككل.

  • وإذا كنت تستخدم بنية إصدار R8 كاملة، يتم إنشاء التقرير تلقائيًا ويمكن العثور عليه في مسار الملف هذا.

  • تتمثل فائدة هذه المهمة المستقلة في أنها تستغرق وقتًا أقل في الإنشاء من الإصدار الكامل.

دراسة حالة: تحسين WHOOP

  • شغّل فريق WHOOP، الذي قد يكون حاضرًا هنا، أداة تحليل الإعدادات وحصل على نسبة تقليص %74، ونسبة تمويه %74، ونسبة تحسين %0.

  • وكما تتخيّلون، تفاجأ الفريق كثيرًا عندما رأى قيمة التحسين هذه.

  • وقد ساعدتهم الرؤية التي توفّرها أداة تحليل الإعدادات في اكتشاف أنّ إصدارهم كان يستخدم ملف ProGuard قديمًا أدى إلى إيقاف التحسين، ومن خلال التبديل إلى الإصدار المحدَّث، تمكّنوا من تفعيل التحسين لتطبيقهم.

  • وقد أدى ذلك إلى تحسين بنسبة %55 وتقليل حجم حزمة APK بنسبة %4.

  • وباستخدامها مع "الملفات الأساسية"، تم تحسين أوقات التشغيل على البارد بنسبة %38، وتقليل اللقطات الثابتة الزائدة بنسبة %28.

  • تعرض الشاشة على اليمين التطبيق بدون "الملفات الشخصية للمرجع"، بينما تعرض الشاشة على اليسار التطبيق مع "الملفات الشخصية للمرجع". وكما ترى، فإنّ بدء تشغيل التطبيق يكون أسرع عند إضافة "الملفات الشخصية للمرجع".

  • لنسمع ذلك مرة أخرى.

  • رائع.

  • سينقلك هذا الرابط إلى دليل أداة تحليل الإعدادات، ومراجعة التقرير يدويًا أمر رائع، ولكن كما تتخيّل، قد يستغرق الأمر الكثير من الوقت، وذلك حسب حالة إعدادات R8 في تطبيقك.

  • لذا، لتسريع العملية، لدينا مهارة R8 Analyzer.

    $ android skills list
    $ android skills add r8-analyzer
  • يمكنك أن تطلب من الوكيل تحليل إعدادات R8، ولن تقتصر المهارة على إنشاء تقرير تحليل إعدادات R8، بل ستوجّهك أيضًا إلى القواعد الأكثر تأثيرًا.

  • إليك مثال على النتيجة، وللإشارة إلى بعض التفاصيل، يوضّح المثال أنّ نتيجة التحسين هي %42، ما يعني أنّ %58 من قاعدة الرموز البرمجية لا يمكن تحسينها باستخدام R8.

  • وإذا نظرنا إلى قسم قاعدة الاحتفاظ على وجه التحديد، فإنّ قاعدة الاحتفاظ على مستوى الحزمة لمكتبة تضخّم حجم التطبيق في Analytics تمنع تحسين %22 من قاعدة الرموز البرمجية، وتنصحنا المهارة أيضًا بتحسين قاعدة الاحتفاظ هذه لأنّها واسعة جدًا كما نعلم.

  • يمكنك الآن تشخيص سبب تضخّم التطبيق وحظر تحسين DEX باستخدام أداة تحليل إعدادات R8 والمهارة.

تحديات الانعكاس في تحسين DEX

  • ولكن بعد العثور على قاعدة احتفاظ واسعة النطاق، كيف يمكنك استبدالها بأمان بدون التسبّب في حدوث عطل غير متوقّع عن طريق الخطأ؟

  • سأترككم الآن مع "كريس" الذي سيوضّح لكم كيفية الانتقال من قواعد الاحتفاظ القديمة إلى التعليقات التوضيحية الجديدة التي يتم إنشاؤها تلقائيًا لمنع أخطاء الكتابة.

  • شكرًا، بريانا.

  • حسنًا، لنتحدّث عن كيفية وصولنا إلى هنا.

  • مع مكتبة الإحصاءات المتضخمة هذه، ومع كل قواعد الاحتفاظ هذه التي تحتفظ بكل شيء تقريبًا، فإن المشكلة الأساسية التي نواجهها هي الانعكاس.

  • لِنلقِ نظرة على بعض الرموز البرمجية التأملية كتذكير بسيط.

    fun createWithReflection(className: String): MyObject {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as MyObject
    }
  • هذه دالة إنشاء كائن تقوم فقط بتضخيم كائن معين، وتقوم بذلك بشكل انعكاسي.

  • الآن، الجانب السلبي لذلك، في سياق تحسين DEX، هو أنّه يؤخّر كل هذه الاستدعاءات الثابتة حتى وقت التشغيل.

  • بمجرد النظر إلى الرمز، اعتمادًا على مصدر اسم الفئة هذا، لن يتمكن مُحسِّن R8 من معرفة ما يحدث هنا.

  • هذه هي الحالات التي تؤدي إلى إيقاف تحسين Dex تلقائيًا، لذا عليك عادةً إضافة قاعدة احتفاظ.

  • الآن، من الأفضل أن تتجنب هذا، أليس كذلك؟

  • من الأفضل تجنُّب الانعكاس تمامًا، خاصةً الآن في عام 2026، حيث تتوفّر لدينا أدوات رائعة لإنشاء الرموز البرمجية من أجل التسلسل وحقن التبعية، والتي يمكنها القضاء تمامًا على هذه الفئة من المشاكل.

  • لكن هذا ليس كل شيء، أليس كذلك؟

  • لا يزال لدينا انعكاس ضروري إلى حد ما في بعض الحالات.

  • لنتعرّف على كيفية حدوث ذلك في هذه المكتبة المليئة بالكتب.

  • إليك مثال محدّد وشائع، وهو التبعية الاختيارية.

    // Problem 1 - optional dependency
    fun tryInitCrashReporter(context: Context): EventTransport? {
       return runCatching {
           Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
               .getDeclaredConstructor(Context::class.java)
               .newInstance(context) as? EventTransport
       }.getOrNull()
    }
    
    // Problem 2 - dynamically instantiate subclass
    fun createEventFilter(className: String): EventFilter {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as? EventFilter
    }
    
    // Problem 3 - serialization
    fun serializeEvent(event: UserActionEvent): JSONObject {
       val json = JSONObject()
       for (field in event.javaClass.declaredFields) {
           field.isAccessible = true
           json.put(field.name, field.get(event))
       }
       return json
    }
  • في هذه الحالة، نحن نضخّم نوعًا معيّنًا لا يمكن الوصول إليه في مسار فئة المكتبة، لذا علينا إجراء ذلك باستخدام سلسلة.

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

  • هناك حالة أخرى لدينا هنا وهي إنشاء فئات فرعية ديناميكيًا.

  • لذلك، خاصةً إذا كان هناك شيء يخرج من ملف بيان أو شيء يتم إرساله من شيء من جهة الخادم، فإننا نبلغ التطبيق المحلي، "مرحبًا، من فضلك- تضخيم هذا الشيء، قم بإنشاء هذا الشيء متأخرًا."

  • بعد ذلك، هذه الفئة الثالثة التي نراها هنا هي التسلسل المحرج.

  • أعني، لدي تسلسل JSON يحدث من خلال التكرار عبر جميع الأعضاء.

  • هذا أمر مقزز للغاية، ولكن، كما تعلمون، هذا النوع من الأشياء يظل عالقًا في بعض الأحيان.

  • لا يتم إصلاحها دائمًا في الوقت المناسب.

  • ولكن لنتحدث عن نتيجة ذلك، أليس كذلك؟

  • ومع تراكم هذه الاستدعاءات الانعكاسية، ستحصل على المزيد من الحافز للقيام بالشيء السهل، وتحسين تجربة المطور على تجربة المستخدم.

  • أعني أنّ هذا شعور سيئ، لذا سأضيف مهمة، وسيكون كل شيء على ما يرام.

    # TODO: clean this up
    -keep com.example.analytics.bloat.** { * };
  • في يوم من الأيام، سأعود وأصلح الأمر، أليس كذلك؟

  • لنتحدّث الآن عن الإجراءات التي كان من المفترض اتّباعها قبل اليوم، وكيفية كتابة قاعدة احتفاظ مثالية هنا.

    fun createEventFilter(className: String): EventFilter {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as? EventFilter
    }
  • حسنًا، استعِدّ.

  • لذا، الجزء الأول هنا هو أننا نريد الاحتفاظ بالفئة المحددة التي نرجعها من هذه الدالة الانعكاسية.

    ### Keep the EventFilter
    -keep class com.example.analytics.bloat.EventFilter {
        *
    }
  • في الواقع، لا، انتظر.

  • هل تعرف ماذا؟

  • هذا غير صحيح.

  • هذه فئة Kotlin، أحتاج إلى التأكد من أن هذا مكتوب بشكل قوي، دعني أحصل على KT هناك.

    ### Keep the actual EventFilter
    -keep class com.example.analytics.bloat.EventFilterKt {
        *
    }
  • في الواقع، الأمر المهم هنا هو أنّني لا أُرجع EventFilter، بل أُرجع فئة فرعية من EventFilter، لذا أحتاج إلى الاحتفاظ بالفئات التي تمدّد EventFilter لأتمكّن من تضخيم ذلك بشكل صحيح.

    ### Keep the EventFilter subclasses
    -keep class ** extends com.example.analytics.bloat.EventFilterKt {
        *
    }
  • وإذا أردت تنفيذ ذلك بكفاءة أكبر، كان من الممكن أن ينجح ذلك، ولكن لجعله أكثر كفاءة، كمطوّر مكتبات جيد، ما الذي يجب أن أفعله أكثر من ذلك؟

  • حسنًا، يجب أن أقول -keepclasseswithmembers، وأحتفظ فقط بتلك الفئات التي تحتوي على دالة الإنشاء <init> المحددة التي أستخدمها لتضخيم الكائن.

    ### Keep the EventFilter subclasses, if they have this constructor
    -keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
       void <init>();
    }
  • وإذا أردت أن أكون لطيفًا حقًا، سأضيف allowaccessmodification للسماح بإجراء المزيد من التحسينات على جميع واجهات برمجة التطبيقات التي تستهدفها قاعدة Keep هذه لأنّني لا أريد منع إجراء المزيد من التحسينات من R8، بل أريد أن أحاول أن أكون مواطنًا صالحًا.

    ### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
    -keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
       void <init>();
    }
  • الخطوة الأخيرة التي لا ينفّذها أحد تقريبًا لأنّها تتطلّب مجهودًا إضافيًا، ولكنّها رائعة للمكتبة، هي جعلها شرطية.

    ### -if makes this conditional, critical for libraries
    -if class com.example.analytics.bloat.EventToolsKt {
        class com.example.analytics.bloat.EventFilterKt createEventFilter(java.lang.String);
    }
    ### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
    -keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
        void <init>();
    }
  • من الناحية النظرية، لست بحاجة إلى الاحتفاظ بكل هذه الفئات الفرعية ما لم تكن تستدعي هذه الدالة، وهذا ما تفعله عبارة if في أعلى قاعدة Keep.

  • ولكن، كما تعلمون، يمكنني أن أطلب منكم رفع أيديكم، ولكنني أعلم أنّه لا أحد تقريبًا يفعل ذلك، أليس كذلك؟

  • مثل كتابتها فعليًا، وجعلها شرطية.

  • ماذا لو تمكّنا من تحسين هذه التجربة؟

  • لنلقِ نظرة أولاً على، مثلاً، حسنًا.

  • حسنًا، ربما هناك طريقة مختلفة للقيام بذلك.

  • لذلك، ناقشنا ما سيحدث إذا كتبتُ قاعدة Keep ضيقة ومكتوبة يدويًا.

  • من الممكن إجراء ذلك من الناحية الفنية، ولكن من الصعب كتابته وقراءته، ومن الصعب جدًا الحفاظ عليه لأنّ هناك الكثير من الأجزاء الصغيرة المكتوبة بشكل متسلسل والتي يسهل كسرها عند حدوث شيء ما تحتها.

  • أسفل شيء ما يتغير.

  • ولدينا بدائل.

  • لدينا تعليقات توضيحية مثل ‎ @Keep.

  • الآن، كان التعليق التوضيحي @Keep في حزمة التعليقات التوضيحية AndroidX لفترة طويلة، ولكن في الواقع لا يُنصح باستخدامه هذه الأيام.

    @androidx.annotation.Keep
    class EventFilter { … }
    
    @androidx.annotation.Keep
    class SizeBasedEventFilter { … }
    
    @androidx.annotation.Keep
    class DropLogsEventFilter { … }
  • وهو ليس حلاً رائعًا لمطوّري المكتبات، وفي الواقع، هو محظور داخليًا، لأنّه على الرغم من سهولة صيانته، إلا أنّه غير مرن للغاية، ولا يتضمّن بعض التحسينات التي كنت أتحدّث عنها من قبل.

  • لا يمكنه ذلك.

  • لا يسمح هذا الموقع بتحليل المكالمات.

  • الآن، إذا نظرنا إلى تلك الحالة المحددة التي كانت لدينا من قبل، "حسنًا، يمكنني فقط وضع @Keep على EventFilter، أنا بخير، أليس كذلك؟"

    @Keep
    class EventFilter { … }
  • حسنًا، الإجابة هي لا.

  • لا تستهدف التعليمة التوضيحية ‎@Keep الفئات الفرعية، بل تستهدف تلك الفئة فقط.

    @Keep
    class EventFilter { … }
    
    @Keep
    class SizeBasedEventFilter { … }
    
    @Keep
    class DropLogsEventFilter { … }
  • لذا، أحتاج في الواقع إلى استهداف جميع الفئات الفرعية، وأكتب الآن المزيد من تعليقات @Keep التوضيحية أكثر مما كنت سأكتبه باستخدام قاعدة Keep واحدة يصعب كتابتها.

  • إذًا، كلاهما يواجه مشاكل، أليس كذلك؟

  • من السهل نسيانها، وتكتشفها متأخرًا، ولا تكون مشروطة بالمتصل بشكل افتراضي.

  • ونريد توفير طريقة أفضل، خاصةً للمكتبات.

  • نريد أن تكون هناك طريقة أسهل للحصول على الأشياء بشكل صحيح من البداية.

  • ومن المشاكل الكبيرة التي نراها هنا أنّ هذه العناصر بعيدة عن الانعكاس الذي نحاول الحماية منه.

  • في النهاية، الحل الذي نريده هو شيء يعيش مباشرة ضد، كما تعلم، رمز الانعكاس المشاغب هنا والذي يؤدي في الواقع إلى تضخيم الأشياء المستهدفة.

مقدّمة حول Jetpack Keep

  • لنتحدّث الآن عن Jetpack Keep، وهي المكتبة والمكوّن الإضافي الجديدان من AndroidX المصمّمان للمساعدة في حلّ هذه المشكلة تحديدًا.

  • لذا، لاستخدام-- الهدف الكامل من Jetpack Keep هو البدء أولاً بإنشاء قواعد الاحتفاظ من التعليقات التوضيحية، ولكن حتى أبعد من ذلك، إنشاء التعليقات التوضيحية مباشرةً من الانعكاس الموجود بالفعل في قاعدة الرموز البرمجية.

  • لذا، فإن الإعداد بسيط للغاية.

    plugins {
       id("com.android.library")
       id("androidx.annotation.keep")
    }
    
    dependencies {
        implementation("androidx.annotation.keep:keep")
        ...
    }
  • وهو مكوّن إضافي لنظام Gradle، ومكتبة تعليقات توضيحية.

  • ولكن لنلقِ نظرة على كيفية تشغيل هذا فعليًا في وقت الإنشاء.

  • لذا، إذا كان لدي هذا المثال السابق حيث أقوم بتضخيم فئة فرعية من نوع معروف، فماذا علي أن أفعل؟

  • أنا أدير lintFix.

    ./gradlew analyticsbloat:lintFix
  • يحلّل Lint هنا كل عمليات الانعكاس التي تحدث داخل هذه الطريقة، ويقول: "مرحبًا، أنت بحاجة إلى هذا التعليق التوضيحي".

    @UsesReflectionToConstruct(
       classConstant = EventFilter::class,
       parameterTypes = []
    )
    fun createEventFilter(className: String): EventFilter {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as? EventFilter
    }
  • يعبّر هذا التعليق التوضيحي بدقة عما يفعله الانعكاس داخل هذا الاستدعاء.

  • ويعني ذلك، "أنا أقوم بتضخيم فئة من هذا النوع".

  • في هذه الحالة تحديدًا، يُعدّ EventFilter صفًا معروفًا.

  • لا أستخدم سلسلة للإشارة إليها لأنّني لست بحاجة إلى ذلك.

  • هذا إجراء أمان إضافي، وأنا أرمّز parameterTypes حتى يوضّح بالضبط الدالة الإنشائية التي يتم الاحتفاظ بها.

  • ولكن ما الذي يفعله هذا؟

  • مثلاً، كيف يمكن الاستفادة من هذه المعلومات في إصدار R8 النهائي؟

  • حسنًا، إنّها تنشئ قاعدة الاحتفاظ، وإذا رأيتم من قبل، هذه هي قاعدة الاحتفاظ المثالية نفسها التي كانت لدينا من قبل.

    # Generated Keep Rule
    
    -if class com.example.analytics.bloat.EventTools {
        class com.example.analytics.bloat.EventFilter createEventFilter(java.lang.String);
    }
    -keepclasseswithmembers,allowaccessmodification class ** extends class com.example.analytics.bloat.EventFilter {
        void <init>();
    }
  • وهي تستهدف الفئات نفسها والفئات الفرعية، وهي مشروطة بنقطة الانعكاس هذه.

  • ولست مضطرًا إلى الحفاظ على - لست مضطرًا إلى الاهتمام بالتعليق التوضيحي لجميع الفئات الفرعية.

  • لست بحاجة إلى الاحتفاظ بملف قواعد احتفاظ منفصل في أي مكان آخر.

  • وهذه طريقة أسهل بكثير لإدارة قواعد الاحتفاظ بالبيانات.

  • لنلقِ نظرة على مثال التبعية الاختيارية.

    @UsesReflectionToConstruct(
       className = "com.example.analytics.bloat.crash.CrashReporterTransport",
       parameterTypes = [Context::class]
    )
    fun tryInitCrashReporter(context: Context): EventTransport? {
       return runCatching {
           Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
               .getDeclaredConstructor(Context::class.java)
               .newInstance(context) as? EventTransport
       }.getOrNull()
    }
  • الآن، لم تكن الفئة في مسار الفئة، ولكن لا يزال بإمكاني الرجوع إليها.

  • أفعل ذلك باستخدام سلسلة، ولكن كل شيء آخر، أنواع المعلمات، يمكن الرجوع إليها من خلال مراجع الفئة، لذلك أفعل.

  • ومرة أخرى، ننشئ قاعدة الاحتفاظ المثالية لهذه الحالة.

    # Generated Keep Rule
    
    -if class com.example.analytics.bloat.CrashConnection {
        class com.example.analytics.bloat.EventTransport tryInitCrashReporter(android.content.Context);
    }
    -keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.crash.CrashReporterTransport {
        void <init>(android.content.Context);
    }
  • وهو شرطي ومحدّد ومستهدَف، ويتيح إجراء تحسينات حيثما أمكن على هذه الفئات المستهدَفة.

  • إليك انعكاسًا على الحقول، وفي هذه الحالة تحديدًا، كنت أكرر جميع الحقول داخل كائن، ولكن هنا، مرة أخرى، نتعامل مع ذلك بشكل جيد من خلال قول استهداف جميع الحقول باستخدام عامل التشغيل النجمي هذا.

    // TODO: migrate to Kotlinx Serialization or Moshi codegen.
    @UsesReflectionToAccessField(
       classConstant = UserActionEvent::class,
       fieldName = "*"
    )
    fun serializeEvent(event: UserActionEvent): JSONObject {
       val json = JSONObject()
       for (field in event.javaClass.declaredFields) {
           field.isAccessible = true
           json.put(field.name, field.get(event))
       }
       return json
    }
  • يتيح لي ذلك استهداف جميع الحقول، وإنشاء قاعدة الاحتفاظ الدقيقة التي تتوقعها لهذا الانعكاس المؤسف للغاية الذي لا ينبغي أن يكون في تطبيقي، ولكن سأعود وأقوم بتنظيف ذلك، أعدك تمامًا.

    # Generated Keep Rule
    
    -if class com.example.analytics.bloat.serialization.EventSerializer {
        class org.json.JSONObject serializeEvent(com.example.analytics.bloat.model.UserActionEvent);
    }
    -keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.model.UserActionEvent {
        <fields>;
    }
  • إذًا، ما هي وظيفة Jetpack Keep؟

  • ويقلّل من صيانة قواعد الاحتفاظ بالبيانات بشكل كبير.

  • بالنسبة إلى كل ما يمكنك إنشاؤه تلقائيًا، فإنّه يزيله من مجال اهتمامك ومجال المخاطر للحفاظ على قواعد الاحتفاظ بالبيانات.

  • ويمنحك أيضًا حتمية أداة lint.

  • لا يقتصر الأمر على إصلاح المشاكل أثناء تكرار العملية ومحاولة نقل البيانات، بل يرصد أيضًا أي تغييرات جديدة في الانعكاس وأي تغييرات انعكاسية في أي وقت يتغير فيه الرمز، وهذا الأمان بالغ الأهمية.

  • هل يمكنني استخدام هذه الميزة؟

  • إذًا، ما هي التبعيات هنا؟

  • يتطلّب المكوّن الإضافي وأداة lint الإصدار 8.7 من AGP.

  • هذا من المفترض أن يكون الجميع تقريبًا.

  • ولكنه أيضًا مرن جدًا بشأن الأماكن التي يمكنك استخدامه فيها.

  • ويمكنك استخدام هذه الميزة داخل التطبيقات.

  • يمكنك استخدام هذه الميزة داخل مكتبات Android وملفات AAR ومكتبات KMP وملفات JAR، مع إجراء بعض عمليات الربط اليدوي في هذه الحالة.

  • لكن الميزة الرئيسية هنا هي أن هذا آمن للاستخدام كمطور مكتبة.

  • حتى إذا كان أحد الأشخاص يستخدم إصدارًا قديمًا من R8 أو حتى أداة تابعة لجهة خارجية تستخدم مكتبتك، فسنظل نصدّر قواعد المستهلك القياسية الفعلية حتى يتمكن أي شخص من استخدامك ولا داعي للقلق بشأن استهلاك نوع من التنسيق الجديد غير المتوافق.

تحليل التطبيقات باستخدام Android CLI

  • بعد أن تحدّثتُ عن مزايا Jetpack Keep التي تساعدك في تحسين تطبيقك وتقليل قلقك بشأن حماية طلبات الرموز البرمجية الانعكاسية، لنتحدّث عن الميزات الجديدة التي أطلقناها مؤخرًا في Android CLI للتحقّق من مرحلة الإصدار الأخير.

  • ذكرت بريانا سابقًا أنّ هناك أدوات في Play Findings ستخبرك بمعلومات عن حجم تطبيقك وحجم DEX ونِسب التحسين.

  • والآن، بدأنا في إتاحة ذلك أيضًا من خلال Android CLI.

  • وتستخدم هذه الأداة المنطق نفسه الذي يستخدمه "متجر Google Play"، لذا يمكنك تحليل الحزمة من خلال واجهة سطر الأوامر (CLI) بدون الحاجة إلى تحميلها إلى "متجر Google Play".

    $ android analyze <mybundle.aab>
    $ android analyze <mybundle.apk>
  • وستحصل على المعلومات نفسها التي يتم إبلاغك بها مباشرةً وفورًا مع التحليل المحلي.

  • كما ترون في هذه الحالة تحديدًا، يتلقّى هذا التطبيق تحذيرًا بشأن انخفاض معدّل التحسين، ويتم إبلاغه أيضًا بأنّ "هذا هو حجم DEX، وهذه هي نِسب التحسين"، تمامًا كما يخبرك "متجر Google Play".

  • ولكن من المفيد أيضًا هنا أن يكون لدينا مخرجات JSON بحيث إذا أردت كتابة نص برمجي لأتمتة اختبار الانحدار ضد أي من هذه القيم التي تنخفض بشكل كبير، لأن هذه هي الأرقام التي يفرضها Play، وتريد تتبعها، يمكنك القيام بذلك بسهولة شديدة باستخدام قيمة JSON هذه - تقارير JSON هذه.

    {
      "optimization": {
        "bundleAnalysisResult": {
          "agpVersion": "9.3.2",
          "r8Json": {
            "version": "9.3.16",
            "options": {
              "isRepackageClassesEnabled": true,
              "isProguardCompatibilityModeEnabled": false
            },
            "stats": {
              "shrinkingRatio": 0.2306,
              "optimizationRatio": 0.2351,
              "obfuscationRatio": 0.2322
            },
            ...
          }
        "mappingObfuscationRatio": 0.21631891,
        "dexHeuristicObfuscationRatio": 0.25118626,
        "uncompressedDexSizeBytes": "16198152"
      }
  • هنا نرى الأرقام نفسها بالضبط موجودة أيضًا.

  • هذه ليست الميزة الوحيدة في "android analyze".

  • يمكن أيضًا تشغيل android analyze على المكتبات.

    $ android analyze <library.aar>
    $ android analyze <library.jar>
  • وكما ذكرت "بريانا"، هناك الكثير من المشاكل في المكتبات التي يتم شحنها مع قواعد الاحتفاظ غير المناسبة بداخلها.

  • لذا، لنستخدم هذا للاطّلاع على بعض هذه المشاكل.

  • لذلك، عندما أشغّل هذه الأداة على ملف AAR معيّن، يمكنني أن أرى أنّ هذه المكتبة تتضمّن عددًا من المشاكل التي تم تمييزها في أعلى الصفحة.

  • تحتوي هذه المكتبة على نسبة احتفاظ عالية، ما يعني أنّها تحتفظ بالكثير من البيانات باستخدام قواعد احتفاظ غير محدودة.

  • يحتوي على قواعد احتفاظ واسعة تمنع تحسينه، وهناك بعض الأرقام أدناه تخبرك بالضبط بمدى سوء ذلك، ويحتوي أيضًا على قواعد احتفاظ، ربما تكون الأكثر رعبًا من بين كل هذه القواعد، لا تستهدف أي شيء، وهو ما يعني على الأرجح أنها تستهدف شخصًا آخر بطريقة غير محدودة.

  • مؤشرات سيئة خطيرة جدًا في الرموز البرمجية

  • لذا، يمكنك التحقّق من كل ذلك في ملف AAR، خاصةً إذا كنت ناشر مكتبة، فهذه طريقة رائعة للتأكّد من عدم شحن قواعد احتفاظ سيئة، وعدم السماح بتسريبها عندما يعمل شخص قد لا يكون خبيرًا في كتابة قواعد الاحتفاظ على المكتبة.

  • هنا، يمكنكم ملاحظة أنّنا نُخرج أيضًا نسبة الاحتفاظ.

  • هذه طريقة جيدة للتفكير في الجودة الإجمالية لمكتبتك.

  • إذا قمت بتحسين مكتبة فردية مستقلة تمامًا، بدون مراجع، باستخدام R8، من المفترض أن أرى أن ذلك يساوي صفر بايت، أليس كذلك؟

  • يجب أن يكون فارغًا بشكل أساسي لأنه لا يوجد شيء محتفظ به بداخله.

  • سيكون ذلك مثاليًا.

  • في هذه الحالة، لا يكون الأمر كذلك.

  • يمكنك الاطّلاع على حجم R8 وD8 مباشرةً.

  • في الواقع، نُبرز أيضًا بعضًا من أسوأ قواعد الاحتفاظ بالبيانات لكي تكون أكثر وضوحًا لك.

  • يستخدم هذا أيضًا تنسيق JSON، لذا سواء كنت مطوّر تطبيقات يحاول التحكّم في تحديثات المكتبة أو مطوّر مكتبات يحاول منع حدوث تراجعات كبيرة في جودة المكتبة، يمكنك إجراء التحليل نفسه ومنع حدوث هذه المشاكل باستخدام نص برمجي بسيط يغلّف Android CLI.

    {
      "optimization": {
        "libraryAnalysisResult": {
          "dex": {
            "d8CompileResult": {
              "dexSizeBytes": 4523352
            },
            "r8CompileResult": {
              "dexSizeBytes": 3266596,
              "configuration": {
                "usedPackageWideKeepRules": [
                  {
                    "source": "-keep class com.example.analytics.bloat.** { *; }",
                    "keptItemCount": 125579,
                  },
                  ...
                ],
                "unusedPackageWideKeepRules": [
                  {
                    "source": "-keep class some.other.library.** { *; }",
                  },
                  ...
                ]
              },
              ...
    }
  • لذلك، إذا عثرت على مشكلة في إحدى المكتبات، يُرجى أولاً محاولة تحديثها.

  • كما ذكرت "بريانا" سابقًا، لقد أصلحنا أكثر من عشر مكتبات من Google لإصلاح بعض قواعد الاحتفاظ بالبيانات غير المناسبة، وهناك مكتبات أخرى تم إصلاحها، ولكن لا يتم تحديثها من خلال التطبيقات التي تستخدمها.

  • إنّها خطوة أولى رائعة.

  • ثانيًا، يمكنك البحث عن مشكلة والتواصل مع مطوّر المكتبة، وربما إرفاق تقرير محلّل يخبره، "مرحبًا، أنت تحتفظ بالكثير من البيانات.

  • يُرجى مساعدتي في تحسين تطبيقي".

النقاط الرئيسية المستخلصة والمراجع

  • للتلخيص، لقد تحدثنا عن أداة R8 Config Analyzer التي تساعدك في الاطّلاع على كل ما يحدث في إصدار R8، وكل ما يوضّح ما يمكن تحسينه وما لا يمكن تحسينه.

  • لقد تحدّثنا عن Jetpack Keep، وهي المكتبة الجديدة التي تساعدك في كتابة قواعد الاحتفاظ تلقائيًا بدون أي جهد.

  • وواجهة سطر الأوامر في Android، وهي الأداة الجديدة التي تساعدك في التحقّق والتأكّد من الخطوة الأخيرة، والتأكّد من أنّك أنت أو وكيلك أو المهندس المبتدئ في فريقك لم يفسد أي شيء للمستهلكين.

  • لذا، إذا أردت معرفة المزيد من المعلومات، يمكنك الاطّلاع على مستندات R8 وأداة تحليل الإعدادات وJetpack Keep.

  • وهناك مستندات حول Android CLI تتناول الأمر analyze وكيفية استخدامه.

  • شكرًا جزيلاً.

الرمز

مقتطفات الرمز البرمجي

مقتطف Kotlin Script في الدقيقة 05:15 في فيديو "مقدمة حول تحسين ملفات DEX"
android {
    buildTypes {
        release {
            optimization {
                // no need to specify default
                // rules and resource shrinking
                enabled = true
            }
        }
    }
    ...
}
مقتطف من Kotlin Script عند الطابع الزمني 05:15 في فيديو "مقدّمة حول تحسين ملفات DEX"
android {
    buildTypes {
        release {
            // Enables code optimizations.
            isMinifyEnabled = true

            // Enables resource shrinking.
            isShrinkResources = true

            proguardFiles(
                // Default rules.
                getDefaultProguardFile(
                    "proguard-android-optimize.txt"
                ),
                ... // list out each file with keep rules
            )
            ...
        }
    }
    ...
}
مقتطف Shell عند الدقيقة 13:19 في فيديو "دراسة حالة: تحسين WHOOP"
$ android skills list
$ android skills add r8-analyzer
مقتطف Kotlin في الدقيقة 14:41 في "تحديات الانعكاس في تحسين DEX"
fun createWithReflection(className: String): MyObject {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as MyObject
}
مقتطف Kotlin في الدقيقة 15:40 في فيديو "تحديات الانعكاس في تحسين DEX"
// Problem 1 - optional dependency
fun tryInitCrashReporter(context: Context): EventTransport? {
   return runCatching {
       Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
           .getDeclaredConstructor(Context::class.java)
           .newInstance(context) as? EventTransport
   }.getOrNull()
}

// Problem 2 - dynamically instantiate subclass
fun createEventFilter(className: String): EventFilter {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as? EventFilter
}

// Problem 3 - serialization
fun serializeEvent(event: UserActionEvent): JSONObject {
   val json = JSONObject()
   for (field in event.javaClass.declaredFields) {
       field.isAccessible = true
       json.put(field.name, field.get(event))
   }
   return json
}
مقتطف Shell في الدقيقة 16:52 في "تحديات الانعكاس في تحسين DEX"
# TODO: clean this up
-keep com.example.analytics.bloat.** { * };
مقتطف Kotlin في الدقيقة 16:59 في فيديو "تحديات الانعكاس في تحسين DEX"
fun createEventFilter(className: String): EventFilter {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as? EventFilter
}
مقتطف Shell في الدقيقة 17:07 في "تحديات الانعكاس في تحسين DEX"
### Keep the EventFilter
-keep class com.example.analytics.bloat.EventFilter {
    *
}
مقتطف Shell في الدقيقة 17:15 في "تحديات الانعكاس في تحسين DEX"
### Keep the actual EventFilter
-keep class com.example.analytics.bloat.EventFilterKt {
    *
}
مقتطف Shell في الدقيقة 17:19 في "تحديات الانعكاس في تحسين DEX"
### Keep the EventFilter subclasses
-keep class ** extends com.example.analytics.bloat.EventFilterKt {
    *
}
مقتطف Shell عند الدقيقة 17:40 في فيديو "تحديات الانعكاس في تحسين DEX"
### Keep the EventFilter subclasses, if they have this constructor
-keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
   void <init>();
}
مقتطف Shell في الدقيقة 17:50 في فيديو "تحديات الانعكاس في تحسين DEX"
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
   void <init>();
}
مقتطف Shell في الدقيقة 18:03 في فيديو "تحديات الانعكاس في تحسين DEX"
### -if makes this conditional, critical for libraries
-if class com.example.analytics.bloat.EventToolsKt {
    class com.example.analytics.bloat.EventFilterKt createEventFilter(java.lang.String);
}
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
    void <init>();
}
مقتطف Kotlin في الدقيقة 18:57 في "تحديات الانعكاس في تحسين DEX"
@androidx.annotation.Keep
class EventFilter { … }

@androidx.annotation.Keep
class SizeBasedEventFilter { … }

@androidx.annotation.Keep
class DropLogsEventFilter { … }
مقتطف Kotlin في الدقيقة 19:22 في فيديو "تحديات الانعكاس في تحسين DEX"
@Keep
class EventFilter { … }
مقتطف Kotlin في الدقيقة 19:29 في فيديو "تحديات الانعكاس في تحسين DEX"
@Keep
class EventFilter { … }

@Keep
class SizeBasedEventFilter { … }

@Keep
class DropLogsEventFilter { … }
مقتطف من نص Kotlin البرمجي عند الطابع الزمني 20:38 في فيديو "مقدمة حول Jetpack Keep"
plugins {
   id("com.android.library")
   id("androidx.annotation.keep")
}

dependencies {
    implementation("androidx.annotation.keep:keep")
    ...
}
مقتطف Shell في الدقيقة 20:54 في فيديو "مقدمة حول Jetpack Keep"
./gradlew analyticsbloat:lintFix
مقتطف Kotlin في الدقيقة 20:55 في فيديو "مقدّمة حول Jetpack Keep"
@UsesReflectionToConstruct(
   classConstant = EventFilter::class,
   parameterTypes = []
)
fun createEventFilter(className: String): EventFilter {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as? EventFilter
}
مقتطف Shell في الدقيقة 21:25 في فيديو "مقدمة حول Jetpack Keep"
# Generated Keep Rule

-if class com.example.analytics.bloat.EventTools {
    class com.example.analytics.bloat.EventFilter createEventFilter(java.lang.String);
}
-keepclasseswithmembers,allowaccessmodification class ** extends class com.example.analytics.bloat.EventFilter {
    void <init>();
}
مقتطف Kotlin في الدقيقة 21:52 في فيديو "مقدمة حول Jetpack Keep"
@UsesReflectionToConstruct(
   className = "com.example.analytics.bloat.crash.CrashReporterTransport",
   parameterTypes = [Context::class]
)
fun tryInitCrashReporter(context: Context): EventTransport? {
   return runCatching {
       Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
           .getDeclaredConstructor(Context::class.java)
           .newInstance(context) as? EventTransport
   }.getOrNull()
}
مقتطف Shell عند الدقيقة 22:05 في فيديو "مقدّمة حول Jetpack Keep"
# Generated Keep Rule

-if class com.example.analytics.bloat.CrashConnection {
    class com.example.analytics.bloat.EventTransport tryInitCrashReporter(android.content.Context);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.crash.CrashReporterTransport {
    void <init>(android.content.Context);
}
مقتطف Kotlin في الدقيقة 22:16 في فيديو "مقدّمة حول Jetpack Keep"
// TODO: migrate to Kotlinx Serialization or Moshi codegen.
@UsesReflectionToAccessField(
   classConstant = UserActionEvent::class,
   fieldName = "*"
)
fun serializeEvent(event: UserActionEvent): JSONObject {
   val json = JSONObject()
   for (field in event.javaClass.declaredFields) {
       field.isAccessible = true
       json.put(field.name, field.get(event))
   }
   return json
}
مقتطف Shell في الدقيقة 22:27 في فيديو "مقدّمة حول Jetpack Keep"
# Generated Keep Rule

-if class com.example.analytics.bloat.serialization.EventSerializer {
    class org.json.JSONObject serializeEvent(com.example.analytics.bloat.model.UserActionEvent);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.model.UserActionEvent {
    <fields>;
}
مقتطف Shell في الدقيقة 24:22 في فيديو "تحليل التطبيقات باستخدام Android CLI"
$ android analyze <mybundle.aab>
$ android analyze <mybundle.apk>
مقتطف JSON في الدقيقة 24:52 في فيديو "تحليل التطبيقات باستخدام Android CLI"
{
  "optimization": {
    "bundleAnalysisResult": {
      "agpVersion": "9.3.2",
      "r8Json": {
        "version": "9.3.16",
        "options": {
          "isRepackageClassesEnabled": true,
          "isProguardCompatibilityModeEnabled": false
        },
        "stats": {
          "shrinkingRatio": 0.2306,
          "optimizationRatio": 0.2351,
          "obfuscationRatio": 0.2322
        },
        ...
      }
    "mappingObfuscationRatio": 0.21631891,
    "dexHeuristicObfuscationRatio": 0.25118626,
    "uncompressedDexSizeBytes": "16198152"
  }
مقتطف Shell في الدقيقة 25:17 في فيديو "تحليل التطبيقات باستخدام Android CLI"
$ android analyze <library.aar>
$ android analyze <library.jar>
مقتطف JSON في الدقيقة 26:46 في فيديو "تحليل التطبيقات باستخدام Android CLI"
{
  "optimization": {
    "libraryAnalysisResult": {
      "dex": {
        "d8CompileResult": {
          "dexSizeBytes": 4523352
        },
        "r8CompileResult": {
          "dexSizeBytes": 3266596,
          "configuration": {
            "usedPackageWideKeepRules": [
              {
                "source": "-keep class com.example.analytics.bloat.** { *; }",
                "keptItemCount": 125579,
              },
              ...
            ],
            "unusedPackageWideKeepRules": [
              {
                "source": "-keep class some.other.library.** { *; }",
              },
              ...
            ]
          },
          ...
}