الصور النقطية والذاكرة

غالبًا ما تكون كائنات الصور النقطية أكبر المساهمين في استهلاك الذاكرة للتطبيق. سواء كانت رموز التطبيقات أو صور الإشعارات أو محتوى الوسائط، يمكن أن يؤدي التعامل غير الفعّال مع الصور النقطية بسرعة إلى حدوث أخطاء في الذاكرة (OOM) وضغط على الذاكرة على مستوى النظام.

إعدادات الصور النقطية وبيانات البكسل

يعتمد مقدار الذاكرة التي تستهلكها الصورة النقطية بشكل أساسي على أبعادها (العرض × الارتفاع) وإعداداتها (Bitmap.Config).

تحدّد الإعدادات عدد البايتات المستخدَمة لتمثيل كل بكسل:

الإعداد بايت لكل بكسل الوصف
ALPHA_8 1 قناة ألفا (الشفافية) فقط مفيدة للأقنعة
RGB_565 2 أحمر (5 بت)، أخضر (6 بت)، أزرق (5 بت) لا تتضمّن قناة ألفا مناسبة للصور المعتمة التي لا تتطلّب دقة عالية في الألوان
ARGB_8888 4 ألفا وأحمر وأخضر وأزرق (8 بت لكل منها) الإعداد التلقائي والأكثر شيوعًا
RGBA_F16 8 نقطة عائمة بنصف الدقة تُستخدَم للمحتوى ذي النطاق اللوني الواسع والمحتوى العالي الدقة (HDR)
HARDWARE لا ينطبق يتم تخزينها في ذاكرة الرسومات (gralloc/DMABuf) يُرجى الاطّلاع على الصور النقطية للأجهزة.

صيغة الذاكرة: Memory (Bytes) = Width × Height × Bytes Per Pixel

على سبيل المثال، تستهلك صورة بملء الشاشة على جهاز بدقة 1080p ‏ (1920×1080) في ARGB_8888: ‏1920 × 1080 × 4 بايت ≈ 8.3 ميغابايت.

الصور النقطية في الذاكرة المخصّصة مقابل الصور النقطية المشترَكة

الصور النقطية في الذاكرة المخصّصة (الذاكرة المخصّصة الأصلية)

في إصدارات Android الحديثة (8.0 والإصدارات الأحدث)، يتم تخزين بيانات بكسل الصور النقطية في الذاكرة المخصّصة الأصلية، بينما لا يتم تخزين سوى كائن صغير للغلاف في الذاكرة المخصّصة في Java.

عندما يحتاج التطبيق إلى عرض صورة، يتم عادةً فك ترميزها من ملف صورة مضغوط إلى صورة نقطية وتخزينها في الذاكرة المخصّصة.

الصور النقطية المشترَكة (ashmem/memfd)

عند نقل صورة نقطية بين العمليات (مثلاً، عبر Binder إلى SystemUI لعرض إشعار)، يتجنّب Android نسخ بيانات البكسل باستخدام الذاكرة المشترَكة (ashmem أو memfd).

يمكن نسخ مثيل Bitmap إلى الذاكرة المشترَكة بشكلٍ صريح من خلال استدعاء Bitmap.asShared()، أو بشكلٍ ضمني إذا تم وضع Bitmap داخل Parcel (عادةً عن طريق إضافة الصورة النقطية إلى Parcelable مثل Bundle) وإرسالها عبر Binder IPC.

عند إرسال صورة نقطية مشترَكة عبر Binder IPC، لا يتم نسخ بيانات البكسل نفسها، بل يتم تكرار واصف ملف يشير إلى منطقة ذاكرة مشترَكة إلى العملية المستلِمة. يمكن مشاركة منطقة الذاكرة الأساسية بين عمليات متعددة، ولا يتم تحريرها إلى أن يتم إغلاق جميع واصفات الملفات التي تشير إليها.

الصور النقطية القابلة للتغيير مقابل الصور النقطية غير القابلة للتغيير

  • الصور النقطية القابلة للتغيير: يمكن تعديلها بعد إنشائها (مثلاً، عبر Canvas). وتتطلّب دائمًا تخصيص ذاكرة خاصة بها. إذا تم نسخ صورة نقطية قابلة للتغيير، يجب إجراء نسخة كاملة (نسخة ثانية من جميع بيانات البكسل).
  • الصور النقطية غير القابلة للتغيير: لا يمكن تغييرها. يسمح ذلك بإجراء تحسينات، مثل مشاركة مخزن الذاكرة الأساسي نفسه بين مثيلات Bitmap مختلفة. عادةً ما تكون الصور النقطية التي يتم تحميلها من موارد APK (BitmapFactory) غير قابلة للتغيير.

التعامل الفعّال مع الصور النقطية

تجميع الصور النقطية وإعادة استخدامها

يؤدي تخصيص الصور النقطية وإلغاء تخصيصها بشكل متكرر إلى تغيير مستمر في تخصيص الذاكرة، ما يجبر أداة جمع البيانات غير الضرورية (GC) على العمل باستمرار. تستخدم مكتبات تحميل الصور الشائعة مجموعة صور نقطية.

تنصح Google باستخدام Glide كحل للتطبيقات المستندة إلى Java، وCoil للتطبيقات المستندة إلى Kotlin (خاصةً عند استخدام Jetpack Compose).

عندما تصبح الصورة النقطية غير مطلوبة، بدلاً من السماح لأداة جمع البيانات غير الضرورية (GC) بإزالتها، يستدعي التطبيق bitmap.recycle() أو يعيدها إلى مجموعة. في المرة التالية التي تكون فيها هناك حاجة إلى صورة نقطية بالأبعاد والإعدادات نفسها، توفّر المجموعة المخزن الحالي، ما يمنع تخصيص ذاكرة جديدة.

الصور النقطية للأجهزة

يسمح لك Bitmap.Config.HARDWARE بتخزين بيانات البكسل مباشرةً في ذاكرة الرسومات (DMABuf).

  • المزايا:
    • توفير الذاكرة: لا تستخدم الذاكرة المخصّصة للتطبيق أو الذاكرة المخصّصة الأصلية، بل تستخدم ذاكرة وحدة معالجة الرسومات. غالبًا ما يجب نسخ الصور النقطية المعروضة في واجهة مستخدم التطبيق إلى ذاكرة وحدة معالجة الرسومات على أي حال، لذا يوفّر ذلك عملية النسخ وتكلفة الذاكرة الإضافية.
    • الأداء: يكون الرسم سريعًا للغاية لأنّ البيانات موجودة على وحدة معالجة الرسومات.
  • العيوب:
    • غير قابلة للتغيير: لا يمكن تعديل الصور النقطية للأجهزة.
    • بطء عملية القراءة: يكون الوصول إلى وحدات البكسل من وحدة المعالجة المركزية (CPU) (مثلاً، getPixel()) مكلفًا جدًا.
    • الإسناد: يصعب تتبُّعها في الأدوات العادية، مثل AHAT (انظر أدناه).

تمرين عملي: استكشاف الصور النقطية

سنستخدم تطبيق BitmapLab التجريبي لاستكشاف هذه المفاهيم.

‫1. القياس باستخدام dumpsys meminfo

شغِّل تطبيق BitmapLab وانقر على ALLOCATE 10MB ARGB_8888. بعد ذلك، شغِّل الأمر التالي:

adb shell dumpsys meminfo -s com.android.bitmaplab

في إصدارات Android الحديثة، ابحث عن قسم Native Allocations. توفر هذه الإصدارات إسنادًا أفضل بكثير للصور النقطية مقارنةً بقسم App Summary العام:

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240  # <--- 10MB Bitmap data!
Bitmap (nonmalloced):        0                              0
  • Bitmap (malloced): الصور النقطية التي تم تخصيصها في الذاكرة المخصّصة الأصلية للعملية هذا هو المكان الذي يتم فيه تخزين معظم الصور النقطية العادية في Android 8.0 والإصدارات الأحدث.
  • Bitmap (nonmalloced): الصور النقطية التي تستخدم ذاكرة متخصّصة، مثل Hardware Bitmaps أو Shared Bitmaps (عبر ashmem أو memfd)

إذا خصّصت صورة نقطية مشترَكة في BitmapLab، ستظهر في Bitmap (nonmalloced):

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

تتبُّع الصور النقطية المشترَكة

في بعض إصدارات Android وإعدادات النواة، يوفّر 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
  • Mapped: إجمالي حجم جميع عمليات ربط الذاكرة ذات الصلة بالصور النقطية
  • Unique: حجم الصور النقطية مع الأخذ في الاعتبار الصور الفريدة فقط (أي، لا يتم احتساب عمليتَي ربط أو أكثر لبيانات البكسل نفسها في الصورة النقطية المشترَكة الأساسية إلا مرة واحدة)

‫2. الصور النقطية في AHAT

توفّر أداة AHAT تصورًا ممتازًا للصور النقطية.

  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 وابحث عن الرابط Bitmaps في الشريط الجانبي أو ابحث عن الفئة Bitmap.

  4. ستعرض أداة AHAT الصور النقطية في المتصفّح، ما يسهّل تحديد الصور التي تستهلك الذاكرة.

AHAT تعرض الصور النقطية المعروضة

‫3. عمليات تتبُّع الصور النقطية في Perfetto

يمكن أن يتتبّع Perfetto عمليات تخصيص الصور النقطية وأعدادها بمرور الوقت. تُصدر هذه العدادات من إطار عمل Android عند تفعيل فئة atrace gfx لتطبيق معيّن.

  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، سيظهر لك ما يلي: * Bitmap Count: عدّاد يعرض عدد الصور النقطية النشطة * ذاكرة الصور النقطية: عدّاد يعرض إجمالي البايتات المستخدَمة في الصور النقطية

الشرائح العالية المستوى (حزمة تطوير البرامج Perfetto SDK)

يستخدم BitmapLab أيضًا حزمة تطوير البرامج Perfetto SDK لإصدار شرائح عالية المستوى لعمليات الصور النقطية. ابحث عن BitmapLab_ في عملية التتبُّع للعثور على ما يلي: * BitmapLab_parcelUnparcel: شرائح تغطي منطق التغليف وإلغاء التغليف * BitmapLab_postNotification: شرائح تغطي مسار نشر الإشعارات

تتبُّع مسارات الإشعارات

عند النقر على Post Notification، ينشئ التطبيق إشعارًا يحتوي على الصورة النقطية الحالية ويرسله إلى النظام. يُصدر رمز إطار العمل المسؤول عن ذلك شرائح Perfetto تتضمّن أحداث مسار تربط بين التغليف (كتابة الصورة النقطية في Parcel لإرسالها عبر Binder IPC) وإلغاء التغليف (قراءة الصورة النقطية من Parcel على الطرف المستلِم).

في لقطة الشاشة أدناه، يمكنك الاطّلاع على التطبيق الذي يغلّف الصورة النقطية الكبيرة لاستخدامها في معاملة Binder لنشر الإشعار، وعملية إلغاء التغليف المقابلة في عملية system_server.

تعرض أداة Perfetto مسارًا من BitmapLab إلى system_server عبر الإشعار

باستخدام Perfetto، يمكنك حتى تتبُّع الصورة النقطية نفسها للإشعار أثناء انتشارها بشكلٍ أكبر بين سلاسل المحادثات والعمليات، مثلاً من سلسلة محادثات Binder في system_server (التي تنفّذ خادم INotificationManager Binder) إلى سلاسل محادثات العامل في system_server التي قد تعيد توجيه الصورة النقطية نفسها إلى com.android.systemui لعرضها في لوحة الإشعارات.

تحديات تطبيقات النظام

تواجه تطبيقات النظام، مثل SystemUI (الإشعارات) وLauncher ، تحديات فريدة:

  1. محتوى غير محدود: يمكن أن تكون الإشعارات والأدوات عديدة. إذا كان كل منها يحتوي على صورة نقطية كبيرة، يمكن أن تنفد ذاكرة النظام بسرعة.
  2. التكرار: قد يتم الاحتفاظ برمز التطبيق نفسه في ذاكرة التخزين المؤقت لـ Launcher، ومنطقة الإشعارات في 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 | ↑ أعلى الصفحة | Native →