کد برنامه حافظه است

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

حافظه با پشتیبانی فایل و صفحه‌بندی درخواستی

Android کد اجرایی را از .apk شما (مثل فایل‌های .oat یا .so) بااستفاده از mmap بار می‌کند. این یعنی کد پشتیبان‌گیری‌شده در فایل است.

نکته مهم این است که Android از صفحه‌بندی درخواستی استفاده می‌کند. وقتی برنامه‌تان شروع می‌شود، هسته کل APK را بلافاصله در RAM بار نمی‌کند. درعوض، فقط فایل را در فضای نشانی مجازی فرایند نگاشت می‌کند. وقتی برنامه‌تان اجرا می‌شود و CPU به تابع جدیدی می‌رود، «خطای صفحه» را راه‌اندازی می‌کند. هسته رشته را موقتاً متوقف می‌کند، آن صفحه کد ۴ کیلوبایتی خاص را از فضای ذخیره‌سازی در RAM فیزیکی می‌خواند، و اجرا را ازسر می‌گیرد.

نموداری که صفحه‌بندی تقاضا را نشان می‌دهد، صفحه‌های مجازی را نشان می‌دهد که فقط هنگام دسترسی به صفحه‌های RAM فیزیکی نگاشت می‌شوند

این یعنی کدی که بسته‌بندی می‌کنید اما هرگز اجرا نمی‌کنید، از حافظه فیزیکی برای خود صفحه‌های کد استفاده نمی‌کند. بااین‌حال، کتابخانه‌های استفاده‌نشده همچنان اندازه کلی APK را افزایش می‌دهند و می‌توانند حافظه مورداستفاده فراداده داخلی سیستم (مثل نمایه‌های DEX و توصیف‌کننده‌های کلاس) را به‌طور قابل‌توجهی افزایش دهند، که حتی برای دانستن اینکه کد وجود دارد باید خوانده شود. علاوه‌براین، بسیاری از کتابخانه‌ها حاوی مقداردهی‌های اولیه ایستا هستند یا درطول راه‌اندازی برنامه توسط چارچوب‌های تزریق وابستگی لمس می‌شوند، که باعث می‌شود به‌هرحال به RAM صفحه‌بندی شوند.

بیرون راندن صفحه و کند شدن

ازآنجایی‌که حافظه با پشتیبانی فایل همیشه می‌تواند از فضای ذخیره‌سازی دوباره خوانده شود، هسته این صفحه‌ها را «تمیز» درنظر می‌گیرد. وقتی سیستم با فشار حافظه مواجه می‌شود، هسته این صفحه‌های کد تمیز را از RAM بیرون می‌کند (رها می‌کند) تا برای چیزهای دیگر فضا ایجاد کند.

اگر برنامه‌تان بعداً نیاز داشته باشد آن کد را دوباره اجرا کند، CPU دچار خطا می‌شود و هسته باید صفحه را دوباره از فضای ذخیره‌سازی بخواند. هرچه برنامه شما کد بیشتری داشته باشد، آسیب‌پذیری آن دربرابر حذف کد بیشتر است. وقتی کاربر پس‌از استفاده از برنامه‌های دیگر به برنامه حجیم شما برمی‌گردد، با کندی و لنگی تصادفی مواجه می‌شود زیرا واحد پردازش مرکزی دائماً متوقف می‌شود تا کد از فضای ذخیره‌سازی به حافظه برگردد.

هزینه خطای صفحه: اگرچه این هزینه به‌شدت به سرعت فضای ذخیره‌سازی دستگاه (UFS درمقابل eMMC) و وضعیت هسته بستگی دارد، اما یک خطای صفحه اصلی (خواندن ۴ کیلوبایت از فضای ذخیره‌سازی) می‌تواند هزینه‌ای بین ۰٫۵ تا ۵ میلی‌ثانیه داشته باشد. اگر مسیر راه‌اندازی شما با ۵۰۰ صفحه مختلف از کد بهینه‌سازی‌نشده تماس داشته باشد، به‌راحتی می‌توانید چند صد میلی‌ثانیه تأخیر خالص ورودی/خروجی به زمان راه‌اندازی برنامه‌تان اضافه کنید.

کاوش اندازه کد با Compiler Explorer

برای اینکه درک شهودی از نحوه ترجمه کد Java یا Kotlin به کد ماشین بومی (و درنتیجه بایت‌های حافظه) داشته باشید، می‌توانید از Compiler Explorer استفاده کنید.

پشتیبانی Android مستقیماً در Godbolt تعبیه شده است. این ابزار به شما امکان می‌دهد ببینید که چگونه بخش‌های مختلف زنجیره ابزار Android (‏D8،‏ R8، و dex2oat) کد منبع شما را تبدیل می‌کنند.

نحوه استفاده از Compiler Explorer با Android

  1. به godbolt.org بروید.
  2. Android Java یا Android Kotlin را از منو کرکره‌ای زبان (بالا سمت راست) انتخاب کنید.
  3. در منو کرکره‌ای کامپایلر (بالا سمت چپ قاب کد)، می‌توانید بین ابزارهای مختلف انتخاب کنید:
    • d8: بایت‌کد Dalvik (.dex) را نشان می‌دهد. این نزدیک‌ترین نمایش به کد اصلی شما است و خواندن آن آسان‌تر است.
    • r8: نشان می‌دهد که بهینه‌ساز R8 چگونه بایت‌کد شما را کوچک و بهینه‌سازی می‌کند.
    • dex2oat: کد ماشینی ARM64 نهایی را که درواقع در دستگاه اجرا می‌شود نشان می‌دهد. اینجا جایی است که می‌توانید تأثیر واقعی حافظه را ببینید (۴ بایت برای هر دستورالعمل). dex2oat می‌تواند ISAهای مختلفی را هدف قرار دهد، اما ARM64 رایج‌ترین ISA برای تلفن‌های همراه است.
  4. برجسته کردن منبع<>برونداد: نگه داشتن نشانگر روی خطی از کد، دستورالعمل‌های کد ماشینی یا کد بایت مربوطه را برجسته می‌کند و ردیابی تأثیر عبارت‌های خاص را آسان می‌کند.
  5. خط لوله بهینه‌سازی: در نمای واهم‌گذاری، می‌توانید روی افزودن جدید... کلیک کنید -> Opt Pipeline. این کار به شما امکان می‌دهد مراحل داخلی را که کامپایلر طی می‌کند ببینید. می‌توانید بررسی کنید که نمایش داخلی (IR) در هر مرحله (مثلاً بین مراحل «Inliner (قبل)» و «Inliner (بعد)») چگونه تبدیل می‌شود، قبل از اینکه به کد ماشین نهایی ARM64 تبدیل شود.

نماگرفتی از میانای کاربر Compiler Explorer که برنامه نمونه و
واسازی خروجی dex2oat و خط لوله بهینه‌سازی آن را با نمایش مرحله درون‌خطی
نشان می‌دهد

چرا این موضوع برای حافظه مهم است

هر دستورالعملی که در خروجی dex2oat با هدف ARM64 ISA می‌بینید، ۴ بایت در فایل اجرایی برنامه‌تان (.odex یا .oat) اشغال می‌کند.

کدی را وارد کنید که از ویژگی‌های زبانی مختلف استفاده می‌کند و خروجی کامپایلر را مطالعه کنید:

  • دسترسی به آرایه درمقابل تکرارکننده‌های فهرست:
    • یک حلقه آرایه ساده روی int[] ممکن است به حدود ۱۰ دستورالعمل (~۴۰ بایت) کامپایل شود.
    • یک حلقه foreach روی یک List به‌طور ضمنی از یک Iterator استفاده می‌کند. این می‌تواند منجر به ۳۰ تا ۴۰ دستورالعمل (حدود ۱۶۰ بایت) شود، به‌دلیل فراخوانی‌های روش اضافی (hasNext()، next()) و تخصیص خود شیء تکرارگر.
    • بهینه‌سازی R8: تحت شرایط مناسب (مثلاً وقتی ثابت شود که List یک ArrayList است)، بهینه‌ساز R8 می‌تواند یک حلقه foreach را به یک حلقه نمایه‌شده ساده تبدیل کند، سربار تکرارکننده را حذف کند، و هم اندازه کد و هم ریزش حافظه زمان اجرا را کاهش دهد.
  • فراخوانی‌های روش مجازی: شامل بار کردن کلاس شیء، پیدا کردن روش در vtable، و سپس انشعاب است. این کار معمولاً ۴ تا ۵ دستورالعمل (~۲۰ بایت) طول می‌کشد.
  • تماس‌های مستقیم/ایستا: اغلب به یک دستورالعمل bl (شاخه با پیوند) (۴ بایت) ترجمه می‌شوند.
  • Kotlin Lambdas: می‌تواند کل کلاس‌های ناشناس و روش‌های پل اضافی را تولید کند و صدها بایت کد و سربار فراداده را برای یک بلوک عملکردی ساده اضافه کند.

بااستفاده از Compiler Explorer، می‌توانید ببینید ویژگی‌های پیچیده زبان (مانند Kotlin lambdas، میاناهای برنامه‌سازی کاربردی جریان، یا استفاده سنگین از generics) چگونه بر اندازه نهایی کامپایل‌شده برنامه‌تان تأثیر می‌گذارند و چگونه بهینه‌سازهایی مانند R8 می‌توانند در برخی موارد با هزینه انتزاع زبان مقابله کنند. این ابزار می‌تواند به شما کمک کند تا در طراحی و پیاده‌سازی برنامه، مبادلات آگاهانه‌ای انجام دهید.

به‌طور کلی، پیچیدگی بیشتر در کد برنامه منجر به استفاده بیشتر از حافظه می‌شود. برعکس، کد ساده‌تر - یا کدی که توسط R8 ساده شده است - منجر به نمایش کوچک‌تر به‌عنوان دستورالعمل‌های CPU و بایت‌ها در فضای ذخیره‌سازی و RAM می‌شود.

سنجش تأثیر کد با meminfo و showmap

می‌توانید از ابزارهای استاندارد حافظه Android استفاده کنید تا ببینید کد برنامه‌تان چقدر حافظه مصرف می‌کند.

dumpsys meminfo

وقتی adb shell dumpsys meminfo <package> را اجرا می‌کنید، دسته کد در بخش خلاصه برنامه نمای سطح بالایی از حافظه مربوط به کد ارائه می‌دهد:

 App Summary
                       Pss(KB)
                        ------
           Java Heap:     3244
         Native Heap:     5412
                Code:    24512  # <--- Sum of .so, .dex, .oat, .art, etc.

showmap

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

adb shell showmap $(pidof <package>) | grep -E "\.oat|\.odex|\.dex|\.apk"

ورودی‌های کد کامپایل‌شده برنامه‌تان را خواهید دید:

   size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS object
------- -------- -------- -------- -------- -------- -------- -------- -------- ----------------
  12288     8192     8192     8192        0        0        0        0        0 /data/app/.../base.odex

کد مرده و R8

چون هر روش اجراشده حافظه اشغال می‌کند، داشتن برنامه‌ای «متورم» با مقداردهی‌های اولیه غیرضروری یا کتابخانه‌های استفاده‌نشده می‌تواند به‌شدت بر عملکرد راه‌اندازی و استفاده از حافظه پایه تأثیر بگذارد.

به‌همین دلیل است که ابزارهایی مانند R8 (ProGuard) بسیار مهم هستند. R8 بایت‌کد برنامه‌تان را تجزیه‌وتحلیل می‌کند و هر کلاس یا روشی را که هرگز فراخوانی نشده است برمی‌دارد («حذف کد مرده»).

تمرین عملی: هزینه تورم

برای نشان دادن تأثیر اندازه کد، آزمایشی را درنظر بگیرید که دو ساخت از یک برنامه را که حاوی ۳۰۰ کلاس تولیدشده (هرکدام با ۵۰۰ روش) است مقایسه می‌کند:

  • CodeBloat (بهینه‌سازی‌نشده): ساخت استاندارد و بهینه‌سازی‌نشده که حاوی همه کلاس‌های تولیدشده و رشته‌های یکتا است.
  • CodeBloatOptimized: همان کد منبع، اما با فعال بودن کوچک‌سازی R8 کامپایل شده است.

۱. کامپایل پیش‌از موعد (AOT)

برای به‌حداکثر رساندن تأثیر حافظه با پشتیبانی فایل، از ابزار cmd package compile برای کامپایل کردن برنامه‌ها در فایل‌های .oat پیش‌از موعد (AOT) استفاده خواهیم کرد.

adb shell cmd package compile -m speed -f com.android.codebloat
adb shell cmd package compile -m speed -f com.android.codebloat.optimized

لطفاً توجه داشته باشید که این یک مثال مصنوعی است. معمولاً برنامه‌ها از حالت گردآوری speed-profile استفاده می‌کنند (در زیر بیشتر ببینید).

۲. راه‌اندازی و مقایسه

برای دیدن راه‌اندازی واقعاً سرد که در آن سیستم باید کد را از فضای ذخیره‌سازی بخواند، پیش‌از راه‌اندازی هر برنامه، حافظه پنهان صفحه هسته را حذف می‌کنیم. این کار به دسترسی ریشه‌ای نیاز دارد.

برنامه بهینه‌سازی‌نشده را راه‌اندازی کنید:

adb shell am force-stop com.android.codebloat
# Drop page cache to ensure the start is truly cold
adb shell "echo 3 > /proc/sys/vm/drop_caches"
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5 # Wait for the background thread to load classes
adb shell dumpsys meminfo -s com.android.codebloat

اکنون همین کار را برای برنامه بهینه‌شده انجام دهید:

adb shell am force-stop com.android.codebloat.optimized
# Drop page cache to ensure the start is truly cold
adb shell "echo 3 > /proc/sys/vm/drop_caches"
adb shell am start -W -n com.android.codebloat.optimized/com.android.codebloat.MainActivity
sleep 5
adb shell dumpsys meminfo -s com.android.codebloat.optimized
نتایج

اگر به ردیف کد در بخش App Summary نگاه کنید، تفاوت بزرگی را خواهید دید:

  • بهینه‌سازی‌نشده Code: ~۳۰٬۰۰۰ کیلوبایت (۳۰ مگابایت)
  • بهینه‌سازی‌شده Code: ~۲۰۰۰ کیلوبایت (۲ مگابایت)

چون R8 تشخیص داد که ۵۰۰ روش داخل آن کلاس‌ها هرگز کار مفیدی انجام نمی‌دهند (روش doSomething() فقط method0() را فراخوانی می‌کند و نتایج نادیده گرفته می‌شوند)، تقریباً همه کدهای تولیدشده مصنوعی را از APK نهایی حذف کرد.

۳. مشاهده تأثیر در Perfetto

تأثیر کد زائد به‌وضوح درطول مرحله بار کردن اولیه برنامه قابل‌مشاهده است. به‌طور خاص، به‌دنبال برش bindApplication در رشته اصلی و برش‌های تودرتو که با madvising شروع می‌شوند بگردید، که نشان‌دهنده آماده شدن سیستم برای بار کردن فایل‌ها از APK و کد کامپایل‌شده آن (.odex) است.

در شروع سرد تعاملی، سیستم کد و داده‌های دیگر را از این فایل‌ها که برای بار کردن و اجرای برنامه لازم است mmap() و madvise() می‌کند. مقدار بعداز «size=» در برش‌های madvising نشان می‌دهد چه مقدار داده باید بار شود. این واکشی پیشاپیش کد برنامه برای تسریع راه‌اندازی برنامه انجام می‌شود.

از این مقایسه می‌توانیم ببینیم که مقدار کد برنامه که باید از فضای ذخیره‌سازی به RAM بارگیری می‌شد در برنامه حجیم‌شده بسیار بیشتر بود و درنتیجه مدت زمان طولانی‌تری طول کشید که باعث شروع کندتر برنامه شد. علاوه‌براین، ردگیری شروع برنامه حجیم‌شده برش‌هایی را برای بار کردن فایل‌های DEX ثانویه نشان می‌دهد (classes2.dex، classes3.dex) که برنامه حجیم‌شده مجبور شده است به آن‌ها «سرریز» کند چون در یک فایل DEX جا نمی‌شود.

درمقایسه (شروع سرد در Pixel 10a)
متریک بهینه‌سازی‌نشده (CodeBloat) بهینه‌سازی‌شده (CodeBloatOptimized)
base.odex اندازه madvise ~۷٫۹ مگابایت (۲٫۰ میلی‌ثانیه) ~۱۶ کیلوبایت (۰٫۰۰۳ میلی‌ثانیه)
base.apk اندازه madvise ~۲٫۴ مگابایت (۲٫۴ میلی‌ثانیه) ~۴ کیلوبایت (۰٫۰۰۱ میلی‌ثانیه)
classes2.dex اندازه madvise ~۷٫۳ مگابایت (۸٫۶ میلی‌ثانیه) موجود نیست
classes3.dex اندازه madvise ~۷٫۳ مگابایت (۸٫۰ میلی‌ثانیه) موجود نیست
کل مدت madvising ~۲۱ میلی‌ثانیه ~0.004 ms
عملکرد بار کردن برنامه بهینه‌سازی‌نشده

نماگرفتی از واسط کاربر Perfetto که فرایند com.android.codebloat را با
برش‌های madvising برای فایل‌های DEX
اصلی و فرعی نشان می‌دهد

عملکرد بار کردن برنامه بهینه‌سازی شد

نماگرفتی از میانای کاربر Perfetto که فرایند com.android.codebloat.optimized را با یک برش کوچک madvising نشان می‌دهد

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

PerfettoSQL برای بار کردن تجزیه‌وتحلیل

می‌توانید از پُرسمان‌های زیر برای استخراج این سنجه‌ها از ردگیری‌هایتان استفاده کنید.

۱. مدت راه‌اندازی برنامه

این سنجه مدت‌زمان بین راه‌اندازی «فعالیت» برنامه تا زمانی را نشان می‌دهد که «فعالیت» اولین قاب را ترسیم کرده است.

INCLUDE PERFETTO MODULE android.startup.startups;

SELECT package, dur, startup_type
FROM android_startups
WHERE package LIKE 'com.android.codebloat%';

ببینید: آشنایی با وضعیت‌های مختلف راه‌اندازی برنامه

مدت‌زمان راه‌اندازی برنامه به عوامل بسیار دیگری غیراز موارد پوشش‌داده‌شده در این راهنما حساس است!

۲. استخراج madvising اندازه و مدت زمان

این پُرسمان روی بخش madvising که در بالا دیدیم زوم‌پیش می‌کند.

INCLUDE PERFETTO MODULE slices.with_context;

SELECT
  name,
  dur/1e6 AS dur_ms
FROM thread_slice
WHERE process_name LIKE 'com.android.codebloat%'
  AND name LIKE 'madvising %';
۳. تفکیک وضعیت رشته اصلی (مدت کل براساس وضعیت)

این پُرسمان نشان می‌دهد که رشته اصلی برنامه چقدر زمان در حالت‌های مختلف صرف کرده است.

SELECT
  p.name AS process_name,
  state,
  sum(dur)/1e6 AS total_dur_ms
FROM thread_state ts
JOIN thread t USING (utid)
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.codebloat%'
  AND t.is_main_thread = 1
GROUP BY p.name, state;

می‌توانید پُرسمان را پالایش کنید تا فقط وضعیت‌های رشته اصلی را درطول مدت راه‌اندازی برنامه بررسی کند.

INCLUDE PERFETTO MODULE android.startup.startups;

SELECT
  p.name AS process_name,
  ts.state,
  -- Calculate only the duration that falls within the startup window
  SUM(
    MAX(0,
      MIN(ts.ts + ts.dur, s.ts + s.dur) - MAX(ts.ts, s.ts)
    )
  ) / 1e6 AS startup_dur_ms
FROM thread_state ts
JOIN thread t USING (utid)
JOIN process p USING (upid)
-- Join on the package name to align thread states with the correct startup
JOIN android_startups s ON s.package = p.name
WHERE p.name LIKE 'com.android.codebloat%'
  AND t.is_main_thread = 1
  -- Only select thread states that overlap with the startup interval
  AND ts.ts + ts.dur > s.ts
  AND ts.ts < s.ts + s.dur
GROUP BY 1, 2
ORDER BY startup_dur_ms DESC;

این کار می‌تواند برخی مشکلات جالب را آشکار کند، برای مثال:

  • زمان زیادی صرف «قابل‌اجرا» (R) شده است اما «درحال اجرا» نیست: این نشان می‌دهد که راه‌اندازی برنامه به‌دلیل رقابت CPU به‌تأخیر افتاده است، یعنی رشته اصلی برنامه نمی‌تواند اجرا شود زیرا رشته‌های دیگر (احتمالاً از برنامه‌های دیگر) CPU را اشغال کرده‌اند.
  • زمان زیادی در «خواب وقفه‌پذیر» (D) صرف شده است: این معمولاً نشان‌دهنده فشار حافظه یا ورودی/خروجی کند است که راه‌اندازی برنامه را متوقف می‌کند.
  • زمان زیادی در حالت «خواب» (S) سپری شده است: این یعنی رشته اصلی منتظر بوده است تا رشته‌های دیگر کارشان را انجام دهند. گاهی اوقات این نشان‌دهنده رقابت قفل در مسیر راه‌اندازی برنامه است (یعنی رشته اصلی در منبعی انحصاری که توسط رشته دیگری در برنامه اشغال شده بود مسدود شده است).
۴. حداکثر حافظه پشتیبانی‌شده با فایل (فایل RSS)

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

SELECT
  p.name AS process_name,
  max(c.value)/1024.0/1024.0 AS max_rss_file_mb
FROM counter c
JOIN process_counter_track t ON c.track_id = t.id
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.codebloat%'
  AND t.name = 'mem.rss.file'
GROUP BY p.name;

حالت‌های کامپایل ART و حافظه

«زمان اجرای Android» (ART) می‌تواند کد برنامه شما را در یکی از چندین حالت مختلف، که با نام فیلترهای کامپایلر نیز شناخته می‌شود، کامپایل کند. فیلتر کامپایلر انتخاب‌شده تأثیر مستقیمی بر ردپای حافظه برنامه‌تان دارد.

  • verify: ART فقط درستی‌سنجی بایت‌کد را انجام می‌دهد. هیچ کامپایل AOT انجام نمی‌شود. کد ازطریق مفسر اجرا می‌شود یا در زمان اجرا توسط کامپایلر JIT کامپایل می‌شود.
    • تأثیر بر حافظه: کوچک‌ترین اندازه روی دیسک. استفاده از حافظه کد بومی به JIT Cache (حافظه کثیف ناشناس) منتقل می‌شود.
  • speed: ART کامپایل کامل AOT را برای همه روش‌ها انجام می‌دهد.
    • تأثیر بر حافظه: بزرگ‌ترین اندازه .odex. استفاده از حافظه پشتیبانی‌شده با فایل (پاک) را به حداکثر می‌رساند.
  • speed-profile: ART فقط روش‌هایی را کامپایل می‌کند که در نمایه JIT به‌عنوان «داغ» علامت‌گذاری شده‌اند.
    • تأثیر بر حافظه: رویکرد متعادل. فقط مهم‌ترین کدها به‌صورت AOT کامپایل می‌شوند.

رایج‌ترین فیلتر speed-profile است که هنگام نصب برنامه‌های کاربر استفاده می‌شود. این مورد در ویژگی‌های سیستم pm.dexopt.install و pm.dexopt.bg-dexopt پیکربندی می‌شود و معمولاً در build/make/target/product/runtime_libart.mk تنظیم می‌شود.

برخی‌از برنامه‌های سیستم از کامپایل speed استفاده خواهند کرد و همچنین در زمان ساخت ایمیج سیستم کامپایل خواهند شد. verify معمولاً فقط در موارد استفاده توسعه به‌کار می‌رود.

مورد استفاده فیلتر کامپایلر معمولی
توسعه verify
تصویر سیستم speed
برنامه‌های کاربر speed-profile

تمرین عملی: حالت‌های کامپایل و حافظه

می‌توانیم از برنامه CodeBloat استفاده کنیم تا ببینیم این فیلترها چگونه بر حافظه تأثیر می‌گذارند. برای بازتولید این اندازه‌گیری‌ها:

  1. برنامه را به‌اجبار در حالت هدف دوباره کامپایل کنید.
  2. برنامه را توقف اجباری و راه‌اندازی سرد کنید.
  3. منتظر بمانید تا رشته پس‌زمینه لمس کلاس‌ها را تمام کند (logcat را تماشا کنید یا ۵ ثانیه صبر کنید).
  4. ‫adb shell dumpsys meminfo com.android.codebloat را اجرا کنید.

حالت: verify (بدون AOT)

adb shell cmd package compile -m verify -f com.android.codebloat
adb shell am force-stop com.android.codebloat
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5
adb shell dumpsys meminfo com.android.codebloat

در حالت verify، «خلاصه» برنامه این موارد را نشان می‌دهد: * Code PSS: ~8,000 KB * Dalvik Other (JIT): ~25,000 KB

چون هیچ کدی به‌صورت AOT کامپایل نمی‌شود، زمان اجرا باید روش‌های داغ را به‌صورت JIT در حافظه پنهان JIT کامپایل کند که به‌صورت حافظه ناشناس کثیف (Dalvik Other) نشان داده می‌شود.

حالت: speed (AOT کامل)

adb shell cmd package compile -m speed -f com.android.codebloat
adb shell am force-stop com.android.codebloat
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5
adb shell dumpsys meminfo com.android.codebloat

در حالت speed، نتایج به‌طور چشمگیری تغییر می‌کند: * Code PSS: ~24,000 KB * Dalvik Other (JIT): ~5,000 KB

اکنون کد برنامه از فایل .odex به‌عنوان حافظه پشتیبانی‌شده با فایل تمیز نگاشت می‌شود. این کار فشار روی حافظه نهان JIT را کاهش می‌دهد و باعث می‌شود حافظه در شرایط فشار برای تخلیه واجدشرایط شود، به‌جای اینکه به‌عنوان RAM کثیف «گیر کند».

حالت: speed-profile (AOT انتخابی)

برنامه‌های مدرن ممکن است نمایه پایه baseline.prof را بسته‌بندی کنند. ART از این برای کامپایل انتخابی فقط کدهای موردنیاز برای راه‌اندازی سریع و کارآمد ازنظر حافظه استفاده می‌کند.

در این تمرین، نمایه مبنایی ایجاد می‌کنیم تا کلاس‌های راه‌اندازی برنامه را فهرست کنیم. اما در واقعیت، کامپایلر ممکن است نمایه‌ها را از منابع خارجی مثل فروشگاه برنامه («نمایه‌های ابری») نیز دریافت کند که می‌تواند نمایه‌های JIT انبوه سپاری‌شده را برای برنامه‌ها ارائه دهد، صرف‌نظر از اینکه توسعه‌دهنده نمایه پایه تولیدشده توسط خودش را نیز بسته‌بندی کرده باشد یا نه.

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

برای دیدن تأثیر speed-profile، می‌توانید نمایه خودتان را در دستگاه تولید کنید:

  1. بازنشانی و شروع:

    adb shell am force-stop com.android.codebloat
    
  2. تعامل: برنامه را راه‌اندازی کنید و اجازه دهید توالی راه‌اندازی خود را اجرا کند.

  3. Dump Profile:

    adb shell kill -s SIGUSR1 $(pidof com.android.codebloat)
    

    (این کار برنامه را مجبور می‌کند نمایه فعلی‌اش را در دیسک بنویسد).

  4. نصب نمایه:

    adb shell cp /data/misc/profiles/cur/0/com.android.codebloat/primary.prof \
    /data/misc/profiles/ref/com.android.codebloat/primary.prof
    
  5. Compile:

    adb shell cmd package compile -m speed-profile -f com.android.codebloat
    

وقتی دوباره راه‌اندازی می‌کنید، توازنی را خواهید دید: Code PSS کمتر از speed (مثلاً حدود ۱۶٬۰۰۰ کیلوبایت) خواهد بود زیرا فقط روش‌های راه‌اندازی «hot» کامپایل شده‌اند، و بقیه موارد فقط درصورتی‌که واقعاً استفاده شوند توسط مفسر یا JIT مدیریت می‌شوند.

مشاهده:

ژرف‌کاوی در کد کامپایل‌شده

اگر می‌خواهید دقیقاً ببینید ART چه دستورالعمل‌هایی تولید می‌کند، به art/DISASSEMBLY_GUIDE.md مراجعه کنید.

این راهنما دستورالعمل‌های دقیقی درباره استفاده از موارد زیر ارائه می‌دهد:

  • oatdump: برای دیدن دستورالعمل‌های ARM64 در فایل .odex موجود.
  • ‫dex2oat: برای شبیه‌سازی کامپایل با پرچم‌های اشکال‌زدایی پرجزئیات.

تمرین: کد درون‌خطی

یکی از دلایلی که کد کامپایل‌شده می‌تواند به‌طور غیرمنتظره‌ای رشد کند، درون‌خطی کردن روش است. کامپایلر ممکن است تصمیم بگیرد بدنه یک روش کوچک و مکرراً فراخوانی‌شده را مستقیماً در فراخوانندگان آن کپی کند.

در برنامه CodeBloat ما، روش doSomething() در هر کلاس تولیدشده به‌سادگی method0() را فراخوانی می‌کند. وقتی در حالت speed کامپایل شود، کامپایلر بهینه‌سازی ART احتمالاً method0() را در doSomething() درون‌خطی می‌کند.

تمرین: این مورد را بااستفاده از oatdump در دستگاهتان به‌تأیید برسانید:

# 1. Find the path to the application's APK and compiled .odex file
adb shell pm path com.android.codebloat
# Output: package:/data/app/~~.../base.apk

adb shell "dumpsys package com.android.codebloat | grep 'location is' | head -n 1"
# Example output: [location is /data/app/~~.../oat/arm64/base.odex]

# 2. Run oatdump (substituting the correct path to base.odex)
adb shell oatdump --oat-file=/data/app/~~.../oat/arm64/base.odex \
                  --class-filter=com.android.codebloat.GeneratedClass0

در خروجی، به‌دنبال روش doSomething بگردید. اگر درون‌خطی شده باشد، دستورالعمل‌های بار کردن ثابت رشته طولانی را مستقیماً در doSomething خواهید دید، نه دستورالعمل bl که method0 را هدف قرار می‌دهد.

درحال دیداری‌سازی بهینه‌سازی (CFG)

برای دیدن دقیقاً زمانی که کامپایلر تصمیم گرفت روش را درون‌خطی کند، می‌توانید نمودار جریان کنترل (CFG) تولید کنید. این ابزار وضعیت کد را در هر مرحله از خط لوله بهینه‌سازی، با هر تبدیل روی «نمایش میانی» (IR) کامپایلر تا زمانی که کد به ISA هدف (مثلاً ARM64) کاهش یابد، نشان می‌دهد.

  1. dex2oat را با پرچم‌های dump اجرا کنید: از پرچم --verbose-methods برای محدود کردن خروجی به روش‌های خاص استفاده کنید؛ درغیراین‌صورت، فایل .cfg برای برنامه‌های بزرگ می‌تواند تا چند گیگابایت افزایش یابد.

    # Substitution of actual paths required:
    adb shell dex2oat64 --dex-file=/data/app/~~.../base.apk \
                        --oat-file=/data/local/tmp/dump.odex \
                        --compiler-filter=speed \
                        --dump-cfg=/data/local/tmp/codebloat.cfg \
                        --verbose-methods=doSomething
    
  2. کشیدن و مشاهده: فایل .cfg را به ایستگاه کاری‌تان بکشید و آن را با IR Hydra باز کنید.

  3. یافتن Inliner: در IR Hydra، مصنوعات گردآوری را بار کنید و doSomething را جستجو کنید. نمایش را قبل و بعداز عبور Inliner مقایسه کنید. با ادغام دستورالعمل‌های method0 در تماس‌گیرنده، نمودار گسترش می‌یابد.

یا از ابزار Opt Pipeline در Compiler Explorer (همان‌طور که در بخش بالا توضیح داده شد) استفاده کنید و کد مشابهی را وارد کنید تا تبدیل مشابهی را که در مرحله Inliner انجام شده است ببینید.

تمرین: فیلدهای فرار و موانع حافظه

در برنامه MemoryLab، فیلد mGarbageSink به‌عنوان volatile علامت‌گذاری شده است. این کار تضمین می‌کند که کامپایلر تخصیص‌های زباله ما را بهینه‌سازی نکند.

public volatile byte[] mGarbageSink;

در واچیدن ARM64، خواهید دید که هر ذخیره در این فیلد با یک مانع حافظه (dmb ish) یا استفاده از دستورالعمل‌های بارگیری-کسب/ذخیره-انتشار (ldar/stlr) همراه است. این امر رؤیت‌پذیری رشته را تضمین می‌کند اما چند دستورالعمل اضافی به هر دسترسی اضافه می‌کند و اندازه کد را درمقایسه با یک فیلد معمولی کمی افزایش می‌دهد.

تمرین: دسترسی‌های فیلد و موانع حافظه مرتبط را در واسازی پیدا کنید.

تمرین: بررسی‌های تعلیق ضمنی

اگر حلقه‌ای را ازهم باز کنید، مانند حلقه موجود در generateAllocationChurn، متوجه دستورالعمل عجیبی در انتهای بدنه حلقه خواهید شد:

ldr x21, [x21]

این یک بررسی تعلیق ضمنی است. ART از این برای اجازه دادن به Garbage Collector برای توقف موقت ایمن رشته‌ها استفاده می‌کند. ثبت x21 معمولاً به خودش اشاره می‌کند. وقتی GC نیاز به تعلیق رشته دارد، آن مکان حافظه را «مسموم» می‌کند. دفعه بعدی که رشته آن ldr را اجرا می‌کند، خطایی را راه‌اندازی می‌کند که زمان اجرا آن را می‌گیرد و از آن برای انتقال رشته به حالت تعلیق استفاده می‌کند.

این الگو در هر حلقه و در شروع هر روش تکرار می‌شود، و به اندازه کل کد برنامه شما کمک می‌کند.

تمرین: همه بررسی‌های تعلیق ضمنی را در واهم‌گذاری روش پیدا کنید، و سعی کنید آن‌ها را با کد منبع اصلی مرتبط کنید.


← WebView | ↑ Up | رشته‌ها →