اشیاء نگاشت بیتی اغلب بزرگترین مشارکتکنندگان تکی در ردپای حافظه برنامه هستند. چه نمادهای برنامه باشد، چه تصاویر اعلان، یا محتوای رسانهای، مدیریت ناکارآمد نقشه بیتی میتواند بهسرعت به خطاهای «کمبود حافظه» (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 ارائه میدهد.
- در BitmapLab، چند بیتمپ اختصاص دهید.
با پرچم
-bرونوشت پشته بگیرید (تا دادههای نقشه بیتی بومی را اضافه کنید):adb shell am dumpheap -b png com.android.bitmaplab /data/local/tmp/bitmaps.hprof adb pull /data/local/tmp/bitmaps.hprof . ahat bitmaps.hproflocalhost:7100را باز کنید و بهدنبال پیوند نقشههای بیتی در نوار کناری بگردید یا کلاسBitmapرا جستجو کنید.AHAT درواقع نقشههای بیتی را در مرورگر ارائه میکند و شناسایی تصاویری را که حافظه را اشغال میکنند آسان میکند.

۳. قطعههای بیتمپ در Perfetto
Perfetto میتواند تخصیصها و تعداد بیتمپ را درطول زمان ردیابی کند. این شمارندهها توسط چارچوب Android زمانیکه دسته gfx atrace برای برنامه خاصی فعال باشد منتشر میشوند.
ردیابی را شروع کنید. باید دسته
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در BitmapLab، روی دکمههای اختصاص دادن و پاک کردن مکرراً تکضرب بزنید.
همچنین روی Parcel/Unparcel Bitmap تکضرب بزنید.
ردیابی را در 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 حتی میتوانید همان نقشه بیتی اعلان را دنبال کنید، زیرا بیشتر در رشتهها و فرایندها منتشر میشود، برای مثال از رشته binder در system_server (که سرور Binder INotificationManager را پیادهسازی میکند) به رشتههای عامل system_server که ممکن است همان نقشه بیتی را به com.android.systemui بازارسال کنند تا در کشو اعلانات نشان داده شود.
چالشهای برنامه سیستم
برنامههای سیستم مانند SystemUI (اعلانها) و راهانداز با چالشهای منحصربهفردی مواجه هستند:
- محتوای نامحدود: اعلانها و ابزارهها میتوانند متعدد باشند. اگر هرکدام از آنها حاوی نقشه بیتی بزرگی باشد، سیستم ممکن است بهسرعت با کمبود حافظه مواجه شود.
- تکرار: ممکن است نماد برنامه یکسانی در حافظه نهان «راهانداز»، ناحیه اعلان SystemUI، و برنامه «تنظیمات» نگهداری شود.
- همرسانی ازطریق میانگیرهای سختافزاری: برای کاهش این مشکل، مؤلفههای سیستم بهسمت سرویس متمرکز «برونسپاری تصویر» حرکت میکنند که نمونههای
HardwareBufferرا در سراسر فرایندها همرسانی میکند. انتساب 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 در سراسر سیستم استفاده کنید.