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

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

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

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

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

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

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

برای مثال، یک تصویر تمام‌صفحه در دستگاه ۱۰۸۰p‏ (۱۹۲۰×۱۰۸۰) در ARGB_8888 این مقدار فضا می‌گیرد: ۱۹۲۰ × ۱۰۸۰ × ۴ بایت ≈ ۸٫۳ مگابایت.

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

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

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

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

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

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

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

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

نگاشت‌های بیتی تغییرپذیر دربرابر تغییرناپذیر

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

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

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

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

‫Google‏ Glide را به‌عنوان راه‌حل برای برنامه‌های مبتنی بر Java و Coil را برای برنامه‌های مبتنی بر Kotlin (به‌ویژه هنگام استفاده از Jetpack Compose) توصیه می‌کند.

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

نگاشت‌های بیتی سخت‌افزار

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

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

مشکلات رایج حافظه بیت‌مپ

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

رمزگشایی بیت‌مپ‌های بیش‌ازحد مقیاس‌بندی‌شده

یک عکس با وضوح کامل ۴۰۰۰ × ۳۰۰۰ پیکسل، ۴۸ مگابایت در ARGB_8888 فضا اشغال می‌کند. رمزگشایی آن تصویر کامل فقط برای ارائه آن در یک ریزعکس ۲۰۰ × ۱۵۰ پیکسلی، بیش‌از ۹۹٪ از میان‌گیر پیکسل اختصاص‌داده‌شده را هدر می‌دهد.

وقتی تصاویر را مستقیماً با ImageDecoder یا BitmapFactory کدگشایی می‌کنید، درطول مرحله کدگشایی نمونه‌کاهی کنید تا بااستفاده از ImageDecoder.setTargetSize() یا BitmapFactory.Options.inSampleSize با ابعاد نمای هدف مطابقت داشته باشد. کتابخانه‌های بار کردن تصویر مثل Glide و Coil وقتی اندازه نمای هدف محدودشده‌ای را ارائه می‌دهید، این نمونه‌کاهی را به‌طور خودکار انجام می‌دهند.

برای مثال، هنگام کدگشایی بیت‌مپ با ImageDecoder، OnHeaderDecodedListener را ارسال کنید که ابعاد خروجی را به اندازه نمای هدف شما کاهش می‌دهد:

val source = ImageDecoder.createSource(resources, R.drawable.high_res_photo)
val bitmap = ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
    if (info.size.width > targetWidth || info.size.height > targetHeight) {
        decoder.setTargetSize(targetWidth, targetHeight)
    }
}

حفظ همزمان بالا از رمزگشایی موازی

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

این حفظ هم‌زمان بالا ردپای پشته بومی را در اوج افزایش می‌دهد و می‌تواند قبل‌از اتمام دسته، باعث توقف اجباری lmkd شود. همزمانی رمزگشایی را با استخر رشته محدود، پرچم، یا توزیع‌کننده روال هم‌زمان مانند Dispatchers.IO.limitedParallelism(2) محدود کنید تا فقط چند نقشه بیتی به‌طور هم‌زمان درحال اجرا رمزگشایی شوند.

نگاشت‌های بیتی گذرا و بازیافت‌نشده در حلقه‌های پردازش قاب

در Android 8.0 و بالاتر، شیء پوشاننده Java Bitmap فقط حدود ۵۶ بایت در توده Java می‌گیرد، درحالی‌که میان‌گیر پیکسل آن در توده بومی قرار دارد و می‌تواند چندین مگابایت فضا بگیرد. می‌توانید این تقسیم را در Android Studio Memory Profiler یا AHAT تأیید کنید، جایی که هر نمونه Bitmap یک اندازه Java سطحی حدود ۵۶ بایت را در کنار اندازه بومی چند مگابایتی خود نشان می‌دهد، و در dumpsys meminfo تحت Native Allocations (Bitmap (malloced)).

خطوط لوله با فرکانس بالا مانند تجزیه‌وتحلیل قاب دوربین، OCR، یا حلقه‌های استنتاج یادگیری ماشین اغلب با فراخوانی ImageProxy.toBitmap() و Bitmap.createBitmap() برای چرخش یا برش، یک نقشه بیتی جدید در هر قاب اختصاص می‌دهند. رها کردن ارجاع‌ها به قاب‌های جایگزین‌شده بدون بازیافت آن‌ها می‌تواند حافظه بومی را افزایش دهد. پوشاننده‌های کوچک Java به‌ندرت اشغال توده Java را افزایش می‌دهند، بنابراین آن‌ها جمع‌آوری زباله را به‌اندازه کافی سریع راه‌اندازی نمی‌کنند تا از انباشته شدن صدها مگابایت میان‌گیر پیکسل بومی قبل‌از اینکه NativeAllocationRegistry آن‌ها را بازیابی کند جلوگیری کنند.

وقتی فریم‌ها را در یک حلقه فشرده پردازش می‌کنید، درصورت امکان از میانگیرهای ازپیش اختصاص‌داده‌شده مجدداً استفاده کنید، یا به‌محض اینکه پردازش هر فریم تمام شد، bitmap.recycle() را به‌طور صریح در نقشه بیتی موقت واسط فراخوانی کنید.

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

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

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

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

adb shell dumpsys meminfo -s com.android.bitmaplab

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

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

اگر Shared Bitmap را در BitmapLab اختصاص دهید، آن را در Bitmap (nonmalloced) خواهید دید:

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

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

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

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

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 تجسم عالی برای Bitmaps ارائه می‌دهد.

  1. در BitmapLab، چند بیت‌مپ اختصاص دهید.
  2. با پرچم -b رونوشت پشته بگیرید (تا داده‌های نقشه بیتی بومی را اضافه کنید):

    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 را باز کنید و به‌دنبال پیوند نقشه‌های بیتی در نوار کناری بگردید یا کلاس Bitmap را جستجو کنید.

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

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

۳. قطعه‌های بیت‌مپ در Perfetto

Perfetto می‌تواند تخصیص‌ها و تعداد بیت‌مپ را درطول زمان ردیابی کند. این شمارنده‌ها توسط چارچوب Android زمانی‌که دسته 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، روی دکمه‌های اختصاص دادن و پاک کردن مکرراً تک‌ضرب بزنید.

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

  4. ردیابی را در ui.perfetto.dev تحلیل کنید.

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

برش‌های سطح بالا (کیت توسعه نرم‌افزار Perfetto)

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

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

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

در نماگرفت زیر می‌توانید ببینید که برنامه بیت‌مپ بزرگ را برای استفاده در تراکنش Binder برای پست کردن اعلان بسته‌بندی می‌کند و بسته‌بندی مربوطه در فرایند system_server باز می‌شود.

Perfetto که جریانی را از BitmapLab به system_server ازطریق «اعلان» نشان می‌دهد

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

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

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

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

    از adb shell dmabuf_dump برای دیدن تخصیص‌های DMABuf در سراسر سیستم استفاده کنید. این ابزار تفکیک میان‌گیرها را براساس فرایند ارائه می‌دهد:

     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 در سراسر سیستم استفاده کنید.


← Java | ↑ Up | Native →