ابزارها و عملکرد

قوانین حفظ ایمنی تایپ و بهینه‌سازی DEX

بلندگو

درباره

درباره سخنرانی

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

با آستانه‌های عملکرد جدید Google Play برای بهینه‌سازی DEX، رؤیت‌پذیری برنامه اکنون مستقیماً به جلوتر بودن از منحنی بهینه‌سازی بستگی دارد.

این سخنرانی یک گردش کار مدرن R8 را برای مقابله با تورم قوانین معرفی می‌کند. با این موارد آشنا خواهید شد:

  • از مهارت و تحلیل‌گر پیکربندی R8 برای شناسایی سربار قانون قدیمی استفاده کنید.
  • از قوانین نگهداری عام گسترده به حاشیه‌نویسی‌های دقیق، ایمن ازنظر نوع، و تولیدشده منتقل شوید.
  • با ابزارهای جدید Android CLI، از پس‌رفت بهینه‌سازی جلوگیری کنید.

خلاصه

  • Google Play آستانه‌های کیفیت برنامه را که نیازمند بیش‌از ‎٪۲۵ بهینه‌سازی کد است اعمال می‌کند. رعایت آن‌ها باعث کاهش استفاده از حافظه و درعین‌حال بهبود سرعت راه‌اندازی و نرخ خطای ANR می‌شود.
  • «تحلیل‌گر پیکربندی» R8 گزارش HTML تولید می‌کند که نشان می‌دهد کدام قوانین نگهداری کد را حفظ می‌کنند. این ابزار قوانین فرعی و استفاده‌نشده را برجسته می‌کند و پتانسیل بهینه‌سازی را قبل‌از کامپایل کردن می‌سنجد.
  • قوانین نگهداری بیش‌ازحد گسترده، که اغلب برای جلوگیری از خرابی‌های بازتابی معرفی می‌شوند یا از کتابخانه‌ها به ارث می‌رسند، بهینه‌سازی برنامه را تضعیف می‌کنند. یک قانون جانشین‌ساز در سطح بسته می‌تواند ده‌ها کلاس استفاده‌نشده را حفظ کند.
  • Jetpack Keep از lint برای شناسایی بازتاب و تولید حاشیه‌نویسی‌های دقیق و قوانین نگهداری شرطی مستقیماً از کد استفاده می‌کند. این افزونه قوانین مصرف‌کننده استاندارد سازگار با نسخه‌های قدیمی‌تر R8 را صادر می‌کند.
  • رابط خط فرمان Android بااستفاده از منطق دقیق Google Play و بدون بارگذاری، بسته‌های برنامه و AAR را بازرسی می‌کند. این ابزار قوانین مشکل‌ساز را پرچم‌گذاری می‌کند و گزارش‌های JSON را برای آزمایش رگرسیون خودکار صادر می‌کند.

منابع

Gemini Notebook

موضوع

  • بهینه‌سازی dex
  • r8
  • قوانین حفظ
  • jetpack-keep
  • android-cli
  • عملکرد برنامه

آوانویسی

مقدمه‌ای بر بهینه‌سازی DEX

  • سلام به همه!

  • نام من بریانا است، من در تیم Android DevRel هستم.

  • و من کریس کریک هستم، در تیم عملکرد Android.

  • به قوانین حفظ ایمنی تایپی و بهینه‌سازی DEX خوش آمدید.

  • امروز می‌خواهیم درباره اینکه بهینه‌سازی DEX چیست، چرا باید به آن اهمیت دهید، و همچنین برخی به‌روزرسانی‌های هیجان‌انگیز اخیر صحبت کنیم.

  • مدت‌ها است که بهینه‌سازی DEX به‌عنوان چیزی درنظر گرفته می‌شود که فقط داشتنش خوب است.

  • بنابراین، شاید آن را یک‌بار پیکربندی کنید و سپس آن را در جای خود رها کنید، یا شاید وقتی زمان اختصاصی داشتید به آن بپردازید.

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

  • برای حفظ اکوسیستم سالم Android و اطمینان از اینکه برنامه‌هایتان همچنان تجربه‌های ممتاز ارائه می‌دهند، Google Play الزامات جدید کیفیت برنامه را با آستانه‌های عملکردی اعلام کرد که در فوریه اجرایی می‌شوند.

  • این شامل آستانه‌های بهینه‌سازی کد DEX می‌شود.

  • برنامه‌ها باید به بیش‌از ۲۵٪ برای کوچک‌سازی، بهینه‌سازی، و مبهم‌سازی برسند.

  • بنابراین، این موضوع برای شما به‌عنوان توسعه‌دهنده Android چه معنایی دارد؟

  • کد DEX بهینه‌سازی‌نشده نه‌تنها بر تجربه کاربری تأثیر منفی می‌گذارد، بلکه ممکن است بر رؤیت‌پذیری و قابلیت‌های انتشار برنامه‌تان در «فروشگاه Play» نیز تأثیر بگذارد.

  • بااین‌حال، شما در جای درستی هستید و تا پایان این صحبت، ابزارهایی را خواهید داشت که نه‌تنها این الزامات را برآورده کنید، بلکه از آن‌ها فراتر بروید.

  • بهینه‌سازی کد DEX با R8، بهینه‌ساز برنامه Android، قابل‌دستیابی است.

  • و اگرچه R8 به‌طور اکید الزامی نیست اگر بهینه‌ساز برنامه دیگری بتواند برنامه شما را به بالاتر از این آستانه‌ها برساند، این مسیر توصیه‌شده‌ای است که مستقیماً در زنجیره ابزار Android ساخته شده است.

  • و اگرچه بسته برنامه DEX که به‌خوبی بهینه‌سازی شده باشد از حافظه کمتری استفاده می‌کند، مزایای اصلی در اینجا همچنان مربوط به عملکرد است: ANR کمتر، راه‌اندازی سریع‌تر، و بهبود عملکرد زمان اجرا.

  • و بهینه‌سازی برنامه شما نتایج واقعی، اثبات‌شده، و مثبتی برای برنامه و کاربران شما دارد.

  • برای مثال، Grab با اتخاذ حالت کامل R8 و اصلاح قوانین نگهداری خود توانست به ۷۸٪ بهینه‌سازی، ۷۹٪ کوچک‌سازی، و ۸۲٪ مبهم‌سازی برای برنامه‌اش دست یابد.

  • و ممکن است به این اسلاید نگاه کنید و فکر کنید، «بسیارخوب، این اعداد بسیار بالا هستند، اما این در واقع به چه معناست؟»

  • درنتیجه این بهبودها، در ترکیب با Baseline Profiles، خطاهای ANR را ۳۲٪ کاهش دادند و راه‌اندازی را ۳۵٪ بهبود بخشیدند.

  • وقتی برنامه‌های برتر برای اولین‌بار R8 را اتخاذ می‌کنند، به‌طور میانگین شاهد کاهش ۳۸ درصدی در اندازه DEX و کاهش ۱۶ درصدی در خطاهای ANR مشهود برای کاربر هستند.

  • و اگر ازقبل از R8 استفاده می‌کنید، خبرهای خوبی برایتان داریم.

  • برنامه‌هایی که R8 را ازقبل فعال کرده‌اند و سپس اقداماتی برای بهبود بیشتر سطوح بهینه‌سازی، کوچک‌سازی، و مبهم‌سازی به بیش‌از ۵۰٪ انجام می‌دهند، به‌طور میانگین شاهد کاهش ۲۵٪ بیشتر در اندازه DEX، کاهش ۴٪ در زمان راه‌اندازی، و کاهش ۱۷٪ بیشتر در خطاهای ANR مشهود برای کاربر هستند که علاوه‌بر ۱۶٪ نشان‌داده‌شده در اسلاید قبلی است.

  • بنابراین، امروز می‌خواهیم شما را راهنمایی کنیم که چگونه می‌توانید به اعدادی مانند این دست یابید.

  • ابزارهایی را پوشش خواهیم داد که می‌توانند به شما کمک کنند کد DEX خود را بهینه‌سازی کنید و از قوانین نگهداری بیش‌ازحد گسترده و با پیکربندی نادرست به قوانین خاص و محدود بروید.

  • ابتدا می‌خواهیم اشاره کنیم که R8 سال‌ها است که وجود دارد.

  • و می‌دانیم که اگرچه این ویژگی بهینه‌سازی بسیار مؤثری را ممکن کرده است، اما پیکربندی آن همچنان به دقت زیادی نیاز دارد.

  • فراخوانی‌های بازتابی به این معنی است که ورودی‌ها غیرقابل‌پیش‌بینی هستند و می‌توانند منجر به ازکارافتادن زمان اجرا شوند.

  • و این موضوع در گذشته هم تیم‌های برنامه و هم تیم‌های کتابخانه را وسوسه کرده است که قوانین نگهداری بیش‌ازحد گسترده‌ای را فقط برای اطمینان از ایمنی اضافه کنند.

  • می‌دانیم که ۴۱٪ از برنامه‌های برتر کمتر از سطح بهینه‌سازی ۲۵٪ تعریف‌شده توسط Google Play هستند، اما این را هم می‌دانیم که ۱۳٪ از برنامه‌های برتر درحال‌حاضر به سه برابر این آستانه دست یافته‌اند.

  • و این برنامه‌ها از طیف گسترده‌ای از صنایع و مسیرهای کاربر هستند که می‌توانند به این عدد دست یابند.

  • و تا زمانی که ابزارهای مناسب و دید مناسب برای مدیریت قوانین حفظ خود و قوانینی که از کتابخانه‌ها به ارث برده‌اید داشته باشید، می‌توانید به امتیازهای بهینه‌سازی بالا برای برنامه‌تان دست پیدا کنید.

  • و قبل‌از اینکه به ابزارها و بهبودهای جدیدی بپردازیم که در این زمینه کمک می‌کنند، می‌خواهیم چند بهبود دیگر R8 را که در سال گذشته انجام شده و این بهینه‌سازی را کمی آسان‌تر می‌کند برجسته کنیم.

  • اول اینکه الگوهای جدید برنامه شامل دامنه بسته است که برای اطمینان از حداقل سطح بهینه‌سازی از همان ابتدا مفید است.

  • تحلیل‌گر پیکربندی R8، که به‌زودی درباره آن صحبت خواهیم کرد، اکنون در نسخه‌های قدیمی‌تر AGP پشتیبانی می‌شود.

  • و همچنین به‌طور غیررسمی برای Bazel پشتیبانی می‌شود.

  • و در AGP 9.2، سرعت اولیه هم‌روال دو برابر شد.

  • و درنتیجه بهبودهای R8 که در AGP 9.2 انجام شد، Tinder پس‌از ارتقا از نسخه ۹.۰.۱ شاهد کاهش ۱۳ درصدی در خطاهای ANR بود.

  • ‫AGP 9.3 همچنین یک R8 API بازطراحی‌شده با نام‌گذاری واضح‌تر، پیش‌فرض‌های ساده‌تر برای قوانین پیش‌فرض و کاهش منابع، و پشتیبانی از دایرکتوری منبع 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 برای شناسایی سربار قانون قدیمی و تعیین مسیر بهبود شروع می‌کنیم.

  • سپس، کریس نحوه انتقال از قوانین نگهداری عام و عام‌کارت به حاشیه‌نویسی‌های دقیق، ایمن ازنظر نوع، و خودکار تولیدشده جدید را پوشش خواهد داد.

  • و درنهایت، نحوه استفاده از ابزارهای جدید اعتبارسنجی CLI را برای اطمینان از بهینه‌سازی تهاجمی بدون ایجاد خرابی بررسی خواهیم کرد.

نمای کلی تحلیل‌گر پیکربندی R8

  • بنابراین، با شروع از «تحلیل‌گر پیکربندی» R8، این مکان ایده‌آل برای شروع بررسی بهینه‌سازی DEX است.

  • اگر از این ابزار استفاده نکرده‌اید، این ابزار به این شکل است.

  • این گزارش HTML اطلاعات مربوط به پیکربندی R8 شما را به‌صورت تصویری نشان می‌دهد.

  • این گزارش برای برنامه من است که شامل کتابخانه‌ای به‌نام Analytics Bloat است.

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

  • «تحلیل‌گر پیکربندی» موارد زیر را اندازه‌گیری می‌کند: کوچک‌سازی، که عبارت است از درصد کلاس‌ها، روش‌ها، و فیلدهایی که R8 به‌دلیل قوانین حفظ از کوچک‌سازی آن‌ها منع نشده است.

  • و این پس‌از اینکه R8 اولین گذر کوچک‌سازی را انجام داد محاسبه می‌شود.

  • همچنین بهینه‌سازی را اندازه‌گیری می‌کند که عبارت است از درصد کدی که برای بهینه‌سازی‌های کل برنامه مانند ادغام کلاس و درون‌خطی کردن روش باز است.

  • و ابهام، که درصدی از اعضا است که می‌توانند مبهم شوند یا به شناسه‌های کوتاه‌تر تغییر نام دهند.

  • همچنین می‌خواهم اشاره کنم که چون «تحلیل‌گر پیکربندی» R8 این امتیازها را قبل‌از بهینه‌سازی‌های نهایی و کامپایل DEX محاسبه می‌کند، مقادیری که در این گزارش برای بهینه‌سازی، کوچک‌سازی، و مبهم‌سازی می‌بینید دقیقاً با مقادیری که در «کنسول Play» می‌بینید مطابقت نخواهد داشت.

  • دلیل آن این است که Play خروجی نهایی DEX را اندازه‌گیری می‌کند.

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

  • اما امتیازهایی که در R8 Config Analyzer می‌بینید همچنان برای اعتبارسنجی پتانسیل بهینه‌سازی DEX قبل‌از بارگذاری در «فروشگاه Play» بسیار مفید هستند.

  • این گزارش همچنین شامل جدول «تحلیل قانون حفظ» است که نمای اصلی‌ای است که هنگام ورود به گزارش می‌بینید.

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

  • همچنین دارای قوانین فرعی است که در اینجا روی آن بزرگ‌نمایی شده است.

  • و این‌ها قوانینی هستند که با قانون گسترده‌تری هم‌پوشانی دارند.

  • و این موارد برجسته شده‌اند تا بتوانید مورد گسترده را بردارید و فقط مورد محدودتر را نگه دارید.

  • همچنین قوانین استفاده‌نشده را به شما نشان می‌دهد، یعنی قوانینی که می‌توانند بدون به‌خطر انداختن پایداری برنامه‌تان حذف شوند.

  • بنابراین، Config Analyzer برای رفع چند مشکل طراحی شده است که با انباشت قانون قدیمی شروع می‌شود.

  • قبلاً، بدون نمایان بودن هر قانون، حذف قوانین قدیمی تا حدودی خطرناک بود.

  • بنابراین، تیم‌ها به‌نوعی آن‌ها را رها می‌کردند یا فقط نویسه‌های عام گسترده‌تری اضافه می‌کردند.

  • دوم اینکه به شما کمک می‌کند نقاط کور قانون کتابخانه را کشف کنید.

  • بنابراین، وقتی کیت توسعه نرم‌افزاری را اضافه می‌کنید، ازنظر تاریخی دشوار بوده است که بگوییم آیا قانونی که با آن کتابخانه همراه شده است بیش‌ازحد گسترده است یا حتی مثلاً ۱۰ نسخه بعدتر به آن نیاز است یا نه.

  • و کمی بعدتر درباره کتابخانه‌ها بیشتر توضیح خواهیم داد.

  • و سوم اینکه به شما کمک می‌کند تأثیر بهینه‌سازی را ارزیابی کنید.

  • قبلاً راه آسانی برای تشخیص اینکه آیا یک قانون دو کلاس را نگه می‌دارد یا ۲۰۰۰ کلاس وجود نداشت.

  • بیایید نگاهی به نحوه عملکرد R8 در زمینه برنامه من و آن کتابخانه‌ای که قبلاً به آن نگاه کردیم بیندازیم.

  • بنابراین، در اینجا یک ماژول برنامه و سه کتابخانه دارم.

  • و به کتابخانه سه در سمت راست توجه کنید.

  • این کتابخانه Analytics Bloat است که شامل قانون حفظ در سطح بسته است.

  • ابتدا، R8 نقطه ورودی ریشه را از همه قوانین حفظ ادغام‌شده شناسایی می‌کند که در اینجا با رنگ زرد برجسته شده‌اند.

  • و به‌طور کلی، این‌ها باید نقاط ورودی اصلی برنامه باشند، مانند کلاس‌ها در مانیفست.

  • و به‌دلیل قانون حفظ عام کتابخانه Analytics Bloat، هر کلاس در کتابخانه به‌عنوان نقطه ورودی درنظر گرفته می‌شود.

  • سپس، R8 قابلیت دسترسی را در سراسر نمودار تماس از آن ریشه‌ها ردیابی می‌کند.

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

  • سپس R8 «تکان دادن درخت» را انجام می‌دهد و کد مرده بدون ارجاع را حذف می‌کند.

  • بنابراین، پس‌از این حذف کدهای بلااستفاده، یا اولین مرحله کوچک‌سازی، این چیزی است که باقی می‌ماند.

  • و برنامه ما امتیاز کوچک‌سازی ۴۲٫۵٪ دارد که بالاتر از آستانه ۲۵٪ از Google Play است، اما هنوز به جایی که می‌خواهیم نرسیده است.

  • و این عمدتاً به‌دلیل قانون حفظ سراسری بسته از کتابخانه تجزیه‌وتحلیل است.

  • و این باعث می‌شود که فقط ۱۷ کلاس از ۴۰ کلاس باقی‌مانده بتوانند بیشتر کوچک شوند.

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

  • با درنظر گرفتن این موضوع، بیایید سریعاً روی کتابخانه‌ها تمرکز کنیم.

  • در سال گذشته، تیم ما سرمایه‌گذاری زیادی روی درک این موضوع کرده است که قوانین نگهداری مصرف‌کننده کتابخانه چگونه بر بهینه‌سازی بوم‌سازگان تأثیر می‌گذارد.

  • بسیاری‌از کتابخانه‌ها با قوانین بسیار گسترده‌ای به‌نام پایداری عرضه می‌شوند.

  • یا قوانین کلی را در اسناد خود برای توسعه‌دهندگان برنامه مشخص می‌کنند تا خودشان به برنامه‌شان اضافه کنند.

  • اما ارسال با قوانین گسترده برای پایداری ضروری نیست و بر عملکرد تأثیر منفی می‌گذارد، که هزینه‌های آن عمدتاً برعهده شما به‌عنوان توسعه‌دهنده برنامه است.

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

  • همچنین می‌خواهم اشاره کنم که حتی کیت‌های توسعه نرم‌افزار خودمان در Google نیز از این موضوع مصون نبودند و بیش‌از دوازده کتابخانه Google در سال گذشته قوانین نگهداری بیش‌ازحد گسترده را اصلاح کرده‌اند.

  • بنابراین اگر می‌خواهید از تحلیل‌گر پیکربندی استفاده کنید، این ابزار مستقیماً در AGP 9.3 ادغام شده است، اما اگر نسخه R8 خود را ارتقا دهید، برای نسخه‌های قدیمی‌تر نیز دردسترس است.

  • بنابراین اجرای این فرمان آن گزارش HTML را که نشان دادیم تولید می‌کند، قوانین نگهداری شما، تأثیرات بر کوچک‌سازی، بهینه‌سازی، مبهم‌سازی برای قوانین فردی و برنامه شما را به‌طور کلی نشان می‌دهد.

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

  • مزیت این تکلیف مستقل این است که تولید آن نسبت‌به ساخت کامل زمان کمتری می‌برد.

مطالعه موردی: بهینه‌سازی WHOOP

  • تیم WHOOP، که ممکن است در جایی در اینجا باشد، تحلیل‌گر پیکربندی را اجرا کرد و به کاهش ۷۴٪، ابهام‌سازی ۷۴٪، و بهینه‌سازی ۰٪ رسید.

  • و همان‌طور که می‌توانید تصور کنید، تیم واقعاً از دیدن این مقدار بهینه‌سازی شگفت‌زده شد.

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

  • این کار منجر به ۵۵٪ بهینه‌سازی و ۴٪ کاهش در اندازه APK شد.

  • با ترکیب این موارد با «نمایه‌های پایه»، زمان شروع سرد را تا ۳۸٪ بهبود بخشیدند و قاب‌های ثابت بیش‌ازحد را تا ۲۸٪ کاهش دادند.

  • صفحه سمت چپ بدون Baseline Profiles است و صفحه سمت راست با آن است، و همان‌طور که می‌بینید، راه‌اندازی برنامه با افزودن Baseline Profiles سریع‌تر است.

  • بگذارید یک بار دیگر پخش شود.

  • باحال.

  • این پیوند شما را به راهنمای تحلیل‌گر پیکربندی می‌برد و بازبینی دستی گزارش عالی است، اما همان‌طور که می‌توانید تصور کنید، بسته به وضعیت پیکربندی R8 برنامه‌تان، ممکن است زمان زیادی طول بکشد.

  • بنابراین برای سرعت بخشیدن به کارها، مهارت R8 Analyzer را داریم.

    $ android skills list
    $ android skills add r8-analyzer
  • می‌توانید از نماینده بخواهید پیکربندی R8 شما را تجزیه‌وتحلیل کند، و این مهارت نه‌تنها گزارش تحلیل‌گر پیکربندی R8 را تولید می‌کند، بلکه شما را به مؤثرترین قوانین نیز هدایت می‌کند.

  • این یک نمونه خروجی است و فقط برای اشاره به چند جزئیات، به امتیاز بهینه‌سازی ۴۲٪ اشاره می‌کند، به این معنی که ۵۲٪-- ۵۸٪، بلکه، از پایگاه کد نمی‌تواند توسط R8 بهینه‌سازی شود.

  • و اگر به‌طور خاص به بخش قانون نگهداری نگاه کنیم، قانون نگهداری در سطح بسته برای کتابخانه حجیم analytics مانع از بهینه‌سازی ۲۲٪ از پایگاه کد می‌شود، و این مهارت همچنین توصیه می‌کند که این قانون نگهداری را اصلاح کنیم زیرا، همان‌طور که می‌دانیم، بسیار گسترده است.

  • بنابراین اکنون می‌توانید بااستفاده از تحلیل‌گر پیکربندی R8 و مهارت، علت تورم برنامه و مسدود شدن بهینه‌سازی DEX را تشخیص دهید.

چالش‌های بازتاب در بهینه‌سازی DEX

  • اما وقتی یک قانون نگهداری بیش‌ازحد گسترده پیدا کردید، چگونه آن را به‌طور ایمن جایگزین می‌کنید بدون اینکه به‌طور تصادفی باعث خرابی غیرمنتظره شوید؟

  • کار را به کریس می‌سپارم تا به شما نشان دهد چگونه از قوانین حفظ قدیمی به حاشیه‌نویسی‌های حفظ خودکار تولیدشده ایمن تایپی جدید منتقل شوید.

  • متشکرم، برینا.

  • بسیارخوب، بیایید درباره اینکه چطور به اینجا رسیدیم صحبت کنیم، موافقید؟

  • با این کتابخانه تجزیه‌وتحلیل متورم، با همه این قوانین حفظ که-- با این قانون حفظ که اساساً همه‌چیز را حفظ می‌کند، مشکل اصلی که با آن مواجه هستیم، بازتاب است.

  • بنابراین، بیایید نگاهی به کمی از کد بازتابی بیندازیم، فقط به عنوان یک یادآوری کوچک در اینجا.

    fun createWithReflection(className: String): MyObject {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as MyObject
    }
  • این یک تابع نمونه‌سازی شیء است که فقط یک شیء خاص را باد می‌کند و این کار را به‌صورت بازتابی انجام می‌دهد.

  • اکنون، جنبه منفی این موضوع، در زمینه بهینه‌سازی DEX، این است که همه این فراخوانی‌های ایستا را تا زمان اجرا به‌تأخیر می‌اندازد.

  • فقط با نگاه کردن به کد، بسته به اینکه این نام کلاس از کجا می‌آید، بهینه‌ساز R8 ما نمی‌تواند ببیند چه اتفاقی در اینجا می‌افتد.

  • این‌ها موقعیت‌هایی هستند که به‌طور پیش‌فرض بهینه‌سازی Dex را مختل می‌کنند، بنابراین معمولاً باید قانون Keep اضافه کنید.

  • حالا، در حالت ایده‌آل شما از این اجتناب می‌کنید، درست است؟

  • مثلاً در حالت ایده‌آل، ما به‌طور کامل از بازتاب اجتناب می‌کنیم، به‌ویژه اکنون در سال ۲۰۲۶ که ابزارهای تولید کد شگفت‌انگیزی برای سریال‌سازی و تزریق وابستگی داریم که می‌توانند این دسته از مشکلات را به‌طور کامل حذف کنند.

  • اما این همه ماجرا نیست، درست است؟

  • ما هنوز در برخی موارد به بازتاب نیاز داریم.

  • بنابراین، بیایید بیایید به این بپردازیم که این دقیقاً چگونه در این کتابخانه متورم اتفاق افتاد.

  • بنابراین، در اینجا یک مثال خاص وجود دارد، و این یک مثال رایج است، یک وابستگی اختیاری است.

    // 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.** { * };
  • ‫Some Someday, I'll I'll come around and fix it, right?‎

  • بنابراین، بیایید درباره کاری که قرار است انجام دهیم، کاری که حداقل قبل‌از امروز قرار بود انجام دهیم، و اینکه چگونه می‌توانید یک قانون «حفظ» ایده‌آل در اینجا بنویسید، صحبت کنیم.

    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 {
        *
    }
  • And in fact, the important thing here is that I'm not actually returning an EventFilter, I'm actually returning a subclass of EventFilter, so I need to keep classes that extend EventFilter in order to be able to inflate this correctly.

    ### 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 انجام می‌دهد.

  • اما، می‌دانید، می‌توانم از شما بخواهم دستتان را بالا ببرید، اما می‌دانم تقریباً هیچ‌کس این کار را نمی‌کند، درست است؟

  • مثلاً، واقعاً آن را بنویسید و آن را مشروط کنید.

  • خب، چه می‌شد اگر می‌توانستیم این را، می‌دانید، کمی بهتر کنیم؟

  • بیایید Let's Let's first look at see, like, okay.

  • شاید راه دیگری برای انجام این کار وجود داشته باشد.

  • بنابراین، ما درباره این صحبت کردیم که مثلاً اگر من یک قانون 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>();
    }
  • این دارو دقیقاً همان کلاس‌ها و زیرکلاس‌ها را هدف قرار می‌دهد و مشروط به این نقطه بازتاب است.

  • And I don't have to maintain a-- I don't have to care about annotating all of the subclasses.

  • لازم نیست فایل قانون حفظ جداگانه‌ای را در جای دیگری نگه دارم.

  • این روش بسیار آسان‌تری برای مدیریت قوانین نگهداری است.

  • بیایید دوباره به آن مثال وابستگی اختیاری نگاه کنیم.

    @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 به AGP 8.7 نیاز دارند.

  • این باید تقریباً همه باشد.

  • اما درباره اینکه کجا می‌توانید از آن استفاده کنید نیز بسیار انعطاف‌پذیر است.

  • می‌توانید از این ویژگی در برنامه‌ها استفاده کنید.

  • می‌توانید از این در کتابخانه‌های Android،‏ AAR، کتابخانه‌های KMP، و JAR استفاده کنید - در این حالت کمی سیم‌کشی دستی لازم است.

  • اما ویژگی کلیدی در اینجا این است که استفاده از آن به‌عنوان توسعه‌دهنده کتابخانه ایمن است.

  • حتی اگر کسی از نسخه قدیمی R8 یا حتی ابزار شخص ثالثی استفاده می‌کند که کتابخانه شما را مصرف می‌کند، ما همچنان قوانین مصرف‌کننده استاندارد واقعی را صادر می‌کنیم تا هرکسی بتواند همچنان از شما استفاده کند و نگران مصرف نوعی قالب ناسازگار جدید نباشد.

درحال تحلیل کردن برنامه‌ها با Android CLI

  • اکنون که درباره کارهایی که Jetpack Keep می‌تواند برای کمک به شما در بهینه‌سازی و کاهش نگرانی درباره محافظت از فراخوانی‌های کد بازتابی انجام دهد صحبت کردم، بیایید درباره ویژگی‌های جدیدی صحبت کنیم که اخیراً در Android CLI برای بررسی مجدد در آخرین مرحله انتشار راه‌اندازی کرده‌ایم.

  • بنابراین، برینا قبلاً اشاره کرد که ابزارهایی وجود دارد که «یافته‌های Play» اطلاعاتی درباره اطلاعات اندازه‌بندی شما، اندازه DEX شما، و درصدهای بهینه‌سازی شما ارائه می‌دهد.

  • خب، اکنون شروع کرده‌ایم که آن را ازطریق Android CLI نیز دردسترس قرار دهیم.

  • این ابزار از همان منطقی استفاده می‌کند که «فروشگاه Play» استفاده می‌کند، بنابراین بدون نیاز به بارگذاری دسته در «فروشگاه Play»، می‌توانید فقط در CLI از آن بخواهید دسته‌تان را تجزیه‌وتحلیل کند.

    $ android analyze <mybundle.aab>
    $ android analyze <mybundle.apk>
  • و همان اطلاعات دقیق با تجزیه‌وتحلیل محلی مستقیماً و فوراً به شما گزارش می‌شود.

  • در این مورد خاص می‌بینید که این برنامه به‌دلیل داشتن نرخ بهینه‌سازی بسیار پایین هشدار دریافت می‌کند و همچنین به آن گفته می‌شود: «این اندازه DEX شما است و این‌ها درصدهای بهینه‌سازی شما هستند»، دقیقاً همان‌طور که «فروشگاه 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 CLI، ابزار جدیدی که به شما کمک می‌کند بررسی و اطمینان حاصل کنید، دوباره با مرحله آخر مقایسه کنید، و مطمئن شوید که شما، نماینده‌تان، یا مهندس تازه‌کار تیمتان چیزی را برای مصرف‌کنندگان خراب نکرده‌اید.

  • بنابراین، اگر می‌خواهید بیشتر بدانید، اسنادی برای R8، تحلیل‌گر پیکربندی، و Jetpack Keep وجود دارد.

  • و اسنادی درباره Android CLI وجود دارد که درباره فرمان تجزیه‌وتحلیل و نحوه استفاده از آن صحبت می‌کند.

  • بسیار سپاس‌گزارم.

کد

تکه‌کدها

قطعه کد Kotlin در دقیقه ۰۵:۱۵ در «مقدمه‌ای بر بهینه‌سازی DEX»
android {
    buildTypes {
        release {
            optimization {
                // no need to specify default
                // rules and resource shrinking
                enabled = true
            }
        }
    }
    ...
}
قطعه Kotlin Script در ۰۵:۱۵ در «مقدمه‌ای بر بهینه‌سازی 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 در ۱۳:۱۹ در «مطالعه موردی: بهینه‌سازی WHOOP»
$ android skills list
$ android skills add r8-analyzer
قطعه Kotlin در دقیقه ۱۴:۴۱ در «چالش‌های بازتاب در بهینه‌سازی DEX»
fun createWithReflection(className: String): MyObject {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as MyObject
}
قطعه Kotlin در ۱۵:۴۰ در «چالش‌های بازتاب در بهینه‌سازی 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 در ۱۶:۵۲ در «چالش‌های بازتاب در بهینه‌سازی DEX»
# TODO: clean this up
-keep com.example.analytics.bloat.** { * };
قطعه Kotlin در ۱۶:۵۹ در «چالش‌های بازتاب در بهینه‌سازی DEX»
fun createEventFilter(className: String): EventFilter {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as? EventFilter
}
قطعه کد Shell در دقیقه ۱۷:۰۷ در «چالش‌های بازتاب در بهینه‌سازی DEX»
### Keep the EventFilter
-keep class com.example.analytics.bloat.EventFilter {
    *
}
قطعه کد Shell در دقیقه ۱۷:۱۵ در «چالش‌های بازتاب در بهینه‌سازی DEX»
### Keep the actual EventFilter
-keep class com.example.analytics.bloat.EventFilterKt {
    *
}
قطعه کد Shell در دقیقه ۱۷:۱۹ در «چالش‌های بازتاب در بهینه‌سازی DEX»
### Keep the EventFilter subclasses
-keep class ** extends com.example.analytics.bloat.EventFilterKt {
    *
}
قطعه کد Shell در دقیقه ۱۷:۴۰ در «چالش‌های بازتاب در بهینه‌سازی DEX»
### Keep the EventFilter subclasses, if they have this constructor
-keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
   void <init>();
}
قطعه کد Shell در دقیقه ۱۷:۵۰ در «چالش‌های بازتاب در بهینه‌سازی 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 در دقیقه ۱۸:۰۳ در «چالش‌های بازتاب در بهینه‌سازی 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 در ۱۸:۵۷ در «چالش‌های بازتاب در بهینه‌سازی DEX»
@androidx.annotation.Keep
class EventFilter { … }

@androidx.annotation.Keep
class SizeBasedEventFilter { … }

@androidx.annotation.Keep
class DropLogsEventFilter { … }
قطعه Kotlin در ۱۹:۲۲ در «چالش‌های بازتاب در بهینه‌سازی DEX»
@Keep
class EventFilter { … }
قطعه Kotlin در ۱۹:۲۹ در «چالش‌های بازتاب در بهینه‌سازی DEX»
@Keep
class EventFilter { … }

@Keep
class SizeBasedEventFilter { … }

@Keep
class DropLogsEventFilter { … }
قطعه کد Kotlin Script در دقیقه ۲۰:۳۸ در «مقدمه‌ای بر Jetpack Keep»
plugins {
   id("com.android.library")
   id("androidx.annotation.keep")
}

dependencies {
    implementation("androidx.annotation.keep:keep")
    ...
}
قطعه کد Shell در دقیقه ۲۰:۵۴ در «مقدمه‌ای بر Jetpack Keep»
./gradlew analyticsbloat:lintFix
قطعه Kotlin در دقیقه ۲۰:۵۵ در «مقدمه‌ای بر Jetpack Keep»
@UsesReflectionToConstruct(
   classConstant = EventFilter::class,
   parameterTypes = []
)
fun createEventFilter(className: String): EventFilter {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as? EventFilter
}
قطعه کد Shell در دقیقه ۲۱:۲۵ در «مقدمه‌ای بر 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 در ۲۱:۵۲ در «مقدمه‌ای بر 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 در دقیقه ۲۲:۰۵ در «مقدمه‌ای بر 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 در ۲۲:۱۶ در «مقدمه‌ای بر 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 در دقیقه ۲۲:۲۷ در «مقدمه‌ای بر 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>;
}
قطعه کد پوسته در دقیقه ۲۴:۲۲ در «تجزیه‌وتحلیل برنامه‌ها با Android CLI»
$ android analyze <mybundle.aab>
$ android analyze <mybundle.apk>
قطعه JSON در دقیقه ۲۴:۵۲ در «تجزیه‌وتحلیل برنامه‌ها با 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.** { *; }",
              },
              ...
            ]
          },
          ...
}