بیت‌مپ‌ها و حافظه

اشیاء بیت‌مپ اغلب بزرگترین سهم را در اشغال فضای حافظه یک برنامه دارند. چه آیکون‌های برنامه، تصاویر اعلان یا محتوای رسانه‌ای باشند، مدیریت ناکارآمد بیت‌مپ می‌تواند به سرعت منجر به خطاهای کمبود حافظه (OOM) و فشار بر حافظه در کل سیستم شود.

پیکربندی‌های بیت‌مپ و داده‌های پیکسلی

میزان حافظه‌ای که یک بیت‌مپ مصرف می‌کند، در درجه اول توسط ابعاد آن (عرض × ارتفاع) و پیکربندی آن ( Bitmap.Config ) تعیین می‌شود.

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

پیکربندی بایت در هر پیکسل توضیحات
ALPHA_8 ۱ فقط کانال آلفا (شفافیت). برای ماسک‌ها مفید است.
RGB_565 ۲ قرمز (۵ بیت)، سبز (۶ بیت)، آبی (۵ بیت). بدون آلفا. مناسب برای تصاویر مات که در آنها دقت رنگ بالا ضروری نیست.
ARGB_8888 ۴ آلفا، قرمز، سبز، آبی (هر کدام ۸ بیت). پیش‌فرض و رایج‌ترین.
RGBA_F16 ۸ ممیز شناور با دقت نیمه دقیق. برای محتوای با طیف رنگی وسیع و HDR استفاده می‌شود.
HARDWARE ناموجود در حافظه گرافیکی (gralloc/DMABuf) ذخیره می‌شود. به بیت‌مپ‌های سخت‌افزاری مراجعه کنید.

فرمول حافظه: Memory (Bytes) = Width × Height × Bytes Per Pixel

برای مثال، یک تصویر تمام صفحه در یک دستگاه 1080p (1920x1080) در ARGB_8888 حجمی معادل 1920 × 1080 × 4 بایت ≈ 8.3 مگابایت اشغال می‌کند.

بیت‌مپ‌های هیپ در مقابل بیت‌مپ‌های اشتراکی

بیت‌مپ‌های هیپ (هیپ بومی)

در اندروید مدرن (۸.۰+)، داده‌های پیکسلی بیت‌مپ در Native Heap ذخیره می‌شوند، در حالی که فقط یک شیء wrapper کوچک در Java heap قرار دارد.

وقتی یک برنامه نیاز به نمایش یک تصویر دارد، معمولاً آن تصویر از یک فایل تصویری فشرده به یک Bitmap رمزگشایی شده و در heap ذخیره می‌شود.

بیت‌مپ‌های مشترک (ashmem/memfd)

وقتی یک بیت‌مپ بین فرآیندها منتقل می‌شود (مثلاً از طریق Binder به SystemUI برای یک اعلان)، اندروید با استفاده از حافظه مشترک ( ashmem یا memfd ) از کپی کردن داده‌های پیکسلی جلوگیری می‌کند.

یک نمونه Bitmap می‌تواند به صورت صریح با فراخوانی Bitmap.asShared() یا به صورت ضمنی اگر Bitmap درون یک Parcel قرار داده شود (معمولاً با اضافه کردن Bitmap به یک Parcelable مانند Bundle ) در حافظه مشترک کپی شود و از طریق Binder IPC ارسال شود.

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

بیت‌مپ‌های تغییرپذیر در مقابل بیت‌مپ‌های تغییرناپذیر

  • بیت‌مپ‌های تغییرپذیر : می‌توانند پس از ایجاد (مثلاً از طریق یک Canvas ) اصلاح شوند. آن‌ها همیشه به تخصیص حافظه خصوصی خود نیاز دارند. اگر یک بیت‌مپ تغییرپذیر کپی شود، باید یک کپی عمیق (کپی دوم از تمام داده‌های پیکسل) ایجاد شود.
  • بیت‌مپ‌های تغییرناپذیر : قابل تغییر نیستند. این امر امکان بهینه‌سازی‌هایی مانند اشتراک‌گذاری بافر حافظه‌ی زیربنایی یکسان بین نمونه‌های مختلف Bitmap فراهم می‌کند. بیت‌مپ‌های بارگذاری‌شده از منابع APK ( BitmapFactory ) معمولاً تغییرناپذیر هستند.

مدیریت کارآمد بیت‌مپ

ادغام و استفاده مجدد از بیت‌مپ

تخصیص و آزادسازی بیت‌مپ‌ها اغلب باعث اختلال در تخصیص می‌شود که GC را مجبور به اجرای مداوم می‌کند. کتابخانه‌های رایج بارگذاری تصویر از Bitmap Pool استفاده می‌کنند.

گوگل Glide را به عنوان راهکار برای برنامه‌های مبتنی بر جاوا و Coil را برای برنامه‌های مبتنی بر کاتلین (به خصوص هنگام استفاده از Jetpack Compose) توصیه می‌کند.

وقتی دیگر به یک بیت‌مپ نیازی نباشد، به جای اینکه اجازه دهد GC شود، برنامه bitmap.recycle() را فراخوانی می‌کند یا آن را به یک pool برمی‌گرداند. دفعه بعد که به یک بیت‌مپ با همان ابعاد و پیکربندی نیاز باشد، pool بافر موجود را فراهم می‌کند و از تخصیص جدید جلوگیری می‌کند.

بیت‌مپ‌های سخت‌افزاری

Bitmap.Config.HARDWARE به شما امکان می‌دهد داده‌های پیکسلی را مستقیماً در حافظه گرافیکی (DMABuf) ذخیره کنید.

  • مزایا :
    • صرفه‌جویی در حافظه : از حافظه برنامه یا حافظه اصلی استفاده نمی‌کند؛ از حافظه GPU استفاده می‌کند. اغلب بیت‌مپ‌های نمایش داده شده در رابط کاربری یک برنامه باید در حافظه GPU کپی شوند، بنابراین این کار باعث صرفه‌جویی در عملیات کپی و هزینه حافظه اضافی می‌شود.
    • عملکرد : ترسیم بسیار سریع زیرا داده‌ها از قبل روی پردازنده گرافیکی (GPU) هستند.
  • معایب :
    • تغییرناپذیر : بیت‌مپ‌های سخت‌افزاری قابل تغییر نیستند.
    • خواندن مجدد کند است : دسترسی به پیکسل‌ها از CPU (مثلاً getPixel() ) بسیار پرهزینه است.
    • انتساب : ردیابی آن در ابزارهای استاندارد مانند AHAT دشوارتر است (به زیر مراجعه کنید).

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

ما از برنامه نمونه BitmapLab برای بررسی این مفاهیم استفاده خواهیم کرد.

۱. اندازه‌گیری با dumpsys meminfo

BitmapLab را اجرا کنید و روی ALLOCATE 10MB ARGB_8888 ضربه بزنید. سپس دستور زیر را اجرا کنید:

adb shell dumpsys meminfo -s com.android.bitmaplab

در نسخه‌های مدرن اندروید، به بخش Native Allocations (اختصاصات بومی) مراجعه کنید. این بخش‌ها نسبت به حالت عمومی App Summary (خلاصه برنامه ): تخصیص‌های بومی، تخصیص‌های بسیار بهتری برای بیت‌مپ‌ها ارائه می‌دهند.

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240  # <--- 10MB Bitmap data!
Bitmap (nonmalloced):        0                              0
  • بیت‌مپ (malloced) : بیت‌مپ‌هایی که در هیپ (heap) بومی فرآیند تخصیص داده شده‌اند. این جایی است که اکثر بیت‌مپ‌های استاندارد در اندروید ۸.۰+ قرار دارند.
  • بیت‌مپ (غیرمتمرکز) : بیت‌مپ‌هایی که از حافظه‌های تخصصی مانند بیت‌مپ‌های سخت‌افزاری یا بیت‌مپ‌های اشتراکی (از طریق ashmem یا memfd ) استفاده می‌کنند.

اگر یک Shared Bitmap را در BitmapLab اختصاص دهید، خواهید دید که در Bitmap (nonmalloced) منعکس می‌شود:

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240
Bitmap (nonmalloced):        1                          10240  # <--- Shared Bitmap!

ردیابی بیت‌مپ‌های مشترک

در برخی از نسخه‌های اندروید و پیکربندی‌های هسته، dumpsys meminfo همچنین ردیابی با وضوح بالا را برای بیت‌مپ‌هایی که از طریق توصیف‌گرهای فایل در فضای آدرس فرآیند نگاشت شده‌اند، فراهم می‌کند.

به طور پیش‌فرض، بیت‌مپ‌های مشترک از یک نام عمومی ("bitmap") استفاده می‌کنند. برای فعال کردن انتساب دقیق و ردیابی بیت‌مپ منحصر به فرد (شناسایی بیت‌مپ‌های مشترک در فرآیندهای مختلف)، باید ویژگی سیستمی زیر را فعال کنید:

adb shell setprop debug.hwui.bitmap_ashmem_long_name true

وقتی این فعال باشد، نواحی ashmem در /proc/<pid>/smaps نام‌های توصیفی‌تری خواهند داشت. meminfo از این مزیت استفاده می‌کند و نتایج به این شکل خواهد بود:

 Shared Bitmaps
                         Count                       Size(KB)
                        ------                         ------
              Mapped:        1                          10240
              Unique:        1                          10240
  • نگاشت‌شده : اندازه کل تمام نگاشت‌های حافظه مربوط به بیت‌مپ.
  • منحصر به فرد : اندازه بیت‌مپ‌ها فقط با در نظر گرفتن منحصر به فرد بودن آنها (یعنی دو یا چند نگاشت از داده‌های پیکسلی بیت‌مپ مشترکِ زیربنایی، فقط یک بار حساب می‌شوند).

۲. بیت‌مپ‌ها در AHAT

AHAT تجسم بسیار خوبی برای Bitmapها ارائه می‌دهد.

  1. در BitmapLab ، چند بیت‌مپ اختصاص دهید.
  2. با استفاده از آپشن -b یک فایل heap dump بگیرید (تا داده‌های بیت‌مپ محلی را نیز شامل شود):

    adb shell am dumpheap -b png com.android.bitmaplab /data/local/tmp/bitmaps.hprof
    adb pull /data/local/tmp/bitmaps.hprof .
    ahat bitmaps.hprof
    
  3. localhost:7100 را باز کنید و در نوار کناری به دنبال لینک Bitmaps بگردید یا کلاس Bitmap را جستجو کنید.

  4. AHAT در واقع بیت‌مپ‌ها را در مرورگر رندر می‌کند و تشخیص اینکه کدام تصاویر حافظه را اشغال می‌کنند را آسان می‌کند.

AHAT بیت‌مپ‌های رندر شده را نمایش می‌دهد

۳. مسیرهای بیت‌مپ در Perfetto

Perfetto می‌تواند تخصیص‌ها و شمارش‌های بیت‌مپ را در طول زمان ردیابی کند. این شمارنده‌ها توسط چارچوب اندروید زمانی منتشر می‌شوند که دسته gfx atrace برای یک برنامه خاص فعال شده باشد.

  1. ردیابی را شروع کنید. شما باید دسته gfx را وارد کنید و با استفاده از پرچم -a ، بسته برنامه خاص را هدف قرار دهید:

    external/perfetto/tools/record_android_trace -o bitmaps.perfetto-trace \
        -t 15s -b 64mb view gfx dalvik am res memory -a com.android.bitmaplab
    
  2. در BitmapLab ، دکمه‌های Allocate و Clear را مکرراً فشار دهید.

  3. همچنین روی Parcel/Unparcel Bitmap ضربه بزنید.

  4. ردیابی را در ui.perfetto.dev تجزیه و تحلیل کنید.

در بخش پردازش مربوط به com.android.bitmaplab ، موارد زیر را مشاهده خواهید کرد: * تعداد بیت‌مپ : شمارنده‌ای که تعداد بیت‌مپ‌های فعال را نشان می‌دهد. * حافظه بیت‌مپ : شمارنده‌ای که کل بایت‌های استفاده شده توسط بیت‌مپ‌ها را نشان می‌دهد.

برش‌های سطح بالا (Perfetto SDK)

BitmapLab همچنین از Perfetto SDK برای انتشار برش‌های سطح بالا برای عملیات بیت‌مپ استفاده می‌کند. در trace عبارت BitmapLab_ را جستجو کنید تا موارد زیر را بیابید: * BitmapLab_parcelUnparcel : برش‌هایی که منطق پارسل‌بندی و باز کردن پارسل را پوشش می‌دهند. * BitmapLab_postNotification : برش‌هایی که جریان ارسال اعلان را پوشش می‌دهند.

ردیابی جریان‌های اعلان

وقتی روی اعلان ارسال (Post Notification) ضربه می‌زنید، برنامه یک اعلان حاوی بیت‌مپ فعلی ایجاد می‌کند و آن را به سیستم ارسال می‌کند. کد فریم‌ورک مسئول این کار، برش‌های Perfetto را با رویدادهای جریان متصل به parceling (نوشتن بیت‌مپ در یک Parcel برای ارسال از طریق Binder IPC) و unparceling (خواندن بیت‌مپ از یک Parcel در سمت گیرنده) منتشر می‌کند.

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

نمایش جریان از BitmapLab به system_server از طریق اعلان (Notification) به صورت Perfetto

با استفاده از Perfetto حتی می‌توانید همان بیت‌مپ اعلان را همزمان با انتشار آن در نخ‌ها و فرآیندها دنبال کنید، برای مثال از یک نخ اتصال‌دهنده در system_server (که سرور INotificationManager Binder را پیاده‌سازی می‌کند) به نخ‌های کارگر system_server که ممکن است همان بیت‌مپ را به com.android.systemui ارسال کنند تا در سایه اعلان‌ها نمایش داده شود.

چالش‌های برنامه‌های سیستمی

برنامه‌های سیستمی مانند SystemUI (اعلان‌ها) و Launcher با چالش‌های منحصر به فردی روبرو هستند:

  1. محتوای نامحدود : اعلان‌ها و ویجت‌ها می‌توانند متعدد باشند. اگر هر کدام از آنها یک بیت‌مپ بزرگ داشته باشند، سیستم می‌تواند به سرعت با کمبود حافظه مواجه شود.
  2. تکرار : ممکن است آیکون برنامه‌ی یکسانی در حافظه‌ی پنهان (cache) لانچر، ناحیه‌ی اعلان‌های رابط کاربری سیستم (SystemUI) و برنامه‌ی تنظیمات (Settings) وجود داشته باشد.
  3. اشتراک‌گذاری از طریق بافرهای سخت‌افزاری : برای کاهش این مشکل، اجزای سیستم به سمت یک سرویس متمرکز "تخلیه تصویر" حرکت می‌کنند که نمونه‌های HardwareBuffer را در بین فرآیندها به اشتراک می‌گذارد.
  4. انتساب DMABuf : بیت‌مپ‌های سخت‌افزاری در فضای هیپ صرفه‌جویی می‌کنند اما از حافظه DMABuf استفاده می‌کنند که انتساب آن به یک فرآیند خاص در ابزارهای استاندارد حافظه دشوارتر است.

    برای مشاهده‌ی تخصیص‌های DMABuf در سطح سیستم، adb shell dmabuf_dump استفاده کنید. این ابزار، تفکیک بافرها را به ازای هر فرآیند ارائه می‌دهد:

     droid.bitmaplab:19562
                      Name              Rss              Pss         nr_procs            Inode               Exporter
                 <unknown>          3840 kB          1280 kB                3             3397              virtio_gpu
                    system            12 kB             4 kB                3             3398                  system
                 <unknown>          3840 kB          1920 kB                2             3399              virtio_gpu
                    system            12 kB             6 kB                2             3400                  system
             PROCESS TOTAL         11556 kB          5136 kB
    
    • RSS : اندازه کل بافر در صورتی که در فرآیند نگاشت شده باشد.
    • Pss : اندازه متناسب (RSS تقسیم بر تعداد فرآیندهایی که بافر را به اشتراک می‌گذارند). این بهترین معیار برای محاسبه است.
    • nr_procs : تعداد فرآیندهایی که در حال حاضر ارجاعی به این بافر دارند.
    • صادر کننده : درایوری که بافر را تخصیص داده است (مثلاً virtio_gpu در Cuttlefish یا یک هیپ Ion/DMA-BUF مخصوص فروشنده در سخت‌افزار).

    همچنین می‌توانید از adb shell dmabuf_dump -b برای مشاهده خلاصه‌ای از تمام بافرها و کل میزان استفاده از DMA-BUF در کل سیستم استفاده کنید.


← جاوا | ↑ بالا | بومی →