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

غالبًا ما تكون عناصر الصور النقطية أكبر المساهمين الفرديين في استهلاك الذاكرة الذي يشغله التطبيق. سواء كانت رموز تطبيقات أو صور إشعارات أو محتوى وسائط، يمكن أن يؤدي التعامل غير الفعّال مع الصور النقطية إلى حدوث أخطاء نفاد الذاكرة (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 (1920x1080) في ARGB_8888 ما يلي: 1920 × 1080 × 4 بايت ≈ 8.3 ميغابايت.

الصور النقطية لأجزاء من الذاكرة مقابل الصور النقطية المشترَكة

الصور النقطية لعناصر متعدّدة (الذاكرة الأصلية)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

فك ترميز الصور النقطية التي تم تغيير حجمها بشكل مفرط

تستهلك صورة بدقة 4000 × 3000 بكسل مساحة 48 ميغابايت في ARGB_8888. يؤدي فك ترميز الصورة الكاملة لعرضها داخل صورة مصغّرة بحجم 200 × 150 بكسل إلى إهدار أكثر من% 99 من مخزن مؤقت البكسل المخصّص.

عند فك ترميز الصور مباشرةً باستخدام 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)
    }
}

الاحتفاظ بالمشاهدين المتزامنين بشكل كبير من خلال فك الترميز المتوازي

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

يؤدي هذا الاحتفاظ العالي المتزامن إلى زيادة الحد الأقصى لمساحة الذاكرة المستخدَمة في الذاكرة الأصلية، ويمكن أن يؤدي إلى عمليات lmkd قبل انتهاء الدفعة. يمكنك ربط عدد عمليات فك الترميز المتزامنة بمجموعة سلاسل محادثات محدودة أو دلالة إشارة أو أداة إرسال روتينية مشتركة مثل Dispatchers.IO.limitedParallelism(2) حتى لا يتم فك ترميز سوى عدد قليل من الصور النقطية في الوقت نفسه.

صور نقطية مؤقتة غير مُعاد استخدامها في حلقات معالجة اللقطات

في نظام التشغيل Android 8.0 والإصدارات الأحدث، لا يشغل عنصر التغليف Bitmap Java سوى 56 بايت تقريبًا في مساحة الذاكرة المخصّصة لتطبيق Java، بينما يتم تخزين مخزن وحدات البكسل الخاص به في مساحة الذاكرة الأصلية ويمكن أن يشغل عدة ميغابايت. يمكنك التحقّق من هذا التقسيم في "محلّل الذاكرة" في "استوديو Android" أو AHAT، حيث يعرض كل مثيل من Bitmap حجم Java سطحي يبلغ حوالي 56 بايت إلى جانب حجمه الأصلي الذي يبلغ عدة ميغابايت، وفي dumpsys meminfo ضمن Native Allocations (Bitmap (malloced)).

غالبًا ما تخصص مسارات المعالجة العالية التردد، مثل تحليل لقطات الكاميرا أو التعرّف الضوئي على الحروف أو حلقات استنتاج تعلُّم الآلة، صورة نقطية جديدة في كل لقطة من خلال استدعاء ImageProxy.toBitmap() وBitmap.createBitmap() للتدوير أو الاقتصاص. يمكن أن يؤدي حذف مراجع الإطارات التي تم استبدالها بدون إعادة استخدامها إلى زيادة حجم الذاكرة الأصلية. لا تؤدي برامج تضمين Java الصغيرة إلى زيادة شغل مساحة التخزين المؤقت في Java إلا بشكل طفيف، وبالتالي لا تؤدي إلى بدء عملية جمع البيانات المُهمَلة بسرعة كافية لمنع تراكم مئات الميجابايت من المخازن المؤقتة لوحدات البكسل الأصلية قبل أن تستردها NativeAllocationRegistry.

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

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

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

1. قياس الأداء باستخدام "dumpsys meminfo"

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

adb shell dumpsys meminfo -s com.android.bitmaplab

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

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240  # <--- 10MB Bitmap data!
Bitmap (nonmalloced):        0                              0
  • الصور النقطية (التي تم تخصيصها باستخدام malloc): الصور النقطية التي تم تخصيصها في الذاكرة الأصلية للعملية وهي المكان الذي يتم فيه تخزين معظم الصور النقطية العادية في الإصدار 8.0 من نظام التشغيل Android والإصدارات الأحدث.
  • الصور النقطية (غير المخصّصة): الصور النقطية التي تستخدم ذاكرة متخصّصة، مثل الصور النقطية للأجهزة أو الصور النقطية المشترَكة (من خلال 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
  • تم الربط: إجمالي حجم جميع عمليات ربط الذاكرة ذات الصلة بالصور النقطية
  • القيم الفريدة: حجم الصور النقطية مع الأخذ في الاعتبار القيم الفريدة فقط (أي أنّ عمليتَي ربط أو أكثر لبيانات البكسل نفسها في الصورة النقطية المشترَكة الأساسية يتم احتسابها مرة واحدة فقط).

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

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

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

3- مقاطع الفيديو النقطية في 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. انقر أيضًا على تقسيم/إلغاء تقسيم صورة نقطية.

  4. حلِّل عملية التتبُّع في ui.perfetto.dev.

في قسم العمليات الخاص بـ com.android.bitmaplab، سيظهر لك ما يلي: * عدد الصور النقطية: عدّاد يعرض عدد الصور النقطية النشطة. * ذاكرة الصور النقطية: عدّاد يعرض إجمالي عدد البايتات التي تستخدمها الصور النقطية.

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

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

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

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

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

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

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

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

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

  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 | ↑ للأعلى | الإعلانات المدمجة مع المحتوى →