OutOfMemoryError في تطوير التطبيقات: ما هو، أسبابه وطرق الوقاية

المؤلف: IT Sectr نُشر: 2026-03-29 وقت القراءة: 9 دق

OutOfMemoryError هو استثناء fatal يحدث عندما لا تستطيع الآلة الافتراضية لجافا (JVM) أو Android Runtime (ART) تخصيص ذاكرة لكائن جديد بسبب عدم وجود مساحة كافية في Heap. وفقًا لـ Square Engineering، 70% من أخطاء OutOfMemoryError في التطبيقات المحمولة ناتجة عن تسرب الذاكرة، وليس عن تجاوز الحد الفعلي. فهم أسباب OOM هو مفتاح استقرار التطبيق.

الخلاصة

  • OutOfMemoryError — استثناء عند عدم كفاية Heap لإنشاء كائن جديد
  • Heap — منطقة الذاكرة حيث تعيش كل كائنات Java/Kotlin
  • Bitmap — المستهلك الرئيسي لـ Heap في Android، مصدر نموذجي لـ OOM
  • Heap Dump — لقطة للـ Heap لتحليل من يشغل وكم يستهلك من الذاكرة
  • معالجة OOM تتطلب إصلاح التسريبات وتحسين استهلاك الذاكرة

ما هو OutOfMemoryError

OutOfMemoryError (OOM) هو استثناء من عائلة VirtualMachineError في Java/Kotlin يشير إلى عدم القدرة على تخصيص ذاكرة لكائن جديد. على عكس الاستثناءات المفحوصة، OOM هو Error ولا يتطلب معالجة عبر catch — على الرغم من أنه يمكن التقاطه تقنيًا. بعد حدوث OOM، يكون التطبيق عادة في حالة غير مستقرة ويوصى بإنهائه.

على Android، كل تطبيق له حد Heap محدد من قبل الشركة المصنعة للجهاز. للهواتف الذكية الحديثة التي بها 6+ جيجابايت RAM، الحد هو 256–512 ميجابايت، للأجهزة الاقتصادية — 128–192 ميجابايت. عندما يتجاوز الحجم الإجمالي لجميع الكائنات الحية هذا الحد، يطرح ART استثناء OutOfMemoryError.

من المهم فهم: OOM لا يعني دائمًا أن الجهاز نفدت منه الذاكرة الفعلية. يعني أن التطبيق استنفد حد Heap الخاص به الذي حدده النظام. التطبيقات الأخرى قد يكون لديها ذاكرة خالية، لكن تطبيقك لا يمكنه استخدامها بسبب عزل العمليات في Android.

الأسباب الرئيسية لـ OutOfMemoryError

خمسة سيناريوهات تؤدي بانتظام إلى OOM في التطبيقات المحمولة. كل سيناريو مرتبط بنوع بيانات أو عملية محددة.

Bitmap بدون تحجيم

Bitmap هو المستهلك الرئيسي للذاكرة في تطبيقات Android. تحميل صورة FullHD (1920 × 1080) بالحجم الأصلي يستهلك 8.3 ميجابايت بصيغة ARGB_8888. إذا كان هناك 50 صورة من هذا القبيل في RecyclerView، فهذا 415 ميجابايت، متجاوزًا Heap أي جهاز. تحميل الصور بدون inSampleSize يضمن OOM على الأجهزة الضعيفة.

استخدم Glide أو Coil للتحجيم التلقائي. هذه المكتبات تحمّل الصور بحجم يتوافق مع View وليس مع الدقة الأصلية. للاستخدام المباشر لـ BitmapFactory.Options، طبق inSampleSize: احسبه كقوة للعدد 2 بحيث لا يتجاوز الحجم النهائي 2048 × 2048 بكسل. بالإضافة، استخدم RGB_565 بدلاً من ARGB_8888 للصور بدون شفافية — هذا يقلل استهلاك الذاكرة إلى النصف.

kotlin
fun loadScaledBitmap(path: String, reqWidth: Int): Bitmap? {
    val opts = BitmapFactory.Options().apply {
        inJustDecodeBounds = true
    }
    BitmapFactory.decodeFile(path, opts)
    opts.inSampleSize = calculateSampleSize(opts.outWidth, reqWidth)
    opts.inJustDecodeBounds = false
    return BitmapFactory.decodeFile(path, opts)
}

تسرب الذاكرة (التراكم)

تسرب واحد بضع كيلوبايت لن يسبب OOM. لكن عشرات التسريبات على كل شاشة تتراكم: كل انتقال بين الشاشات يضيف تسربًا، GC لا يستطيع تحرير الكائنات، ويمتلئ Heap. نمط نموذجي: يفتح المستخدم شاشة الملف الشخصي ويغلقها 20 مرة → ينمو Heap بمقدار 200 ميجابايت → يتعطل التطبيق مع OOM.

قم بتثبيت LeakCanary في المشروع للكشف التلقائي عن التسريبات. سيظهر كل كائن مسرب مع تتبع دقيق للمكدس. بعد إصلاح جميع التسريبات، يصبح استهلاك Heap مستقرًا: بعد إغلاق شاشة، تعود الذاكرة إلى المستوى الأساسي.

ملفات كبيرة في الذاكرة

تحميل ملفات كاملة في byte[] هو طريق مباشر إلى OOM. ملف JSON بحجم 50 ميجابايت أثناء التحليل سينشئ سلسلة بنفس الحجم بالإضافة إلى نموذج DOM. ملفات الفيديو المحملة في الذاكرة، ومخازن الصوت المؤقتة، ومجموعات البيانات الكبيرة protobuf — كلها يمكن أن تتجاوز حد Heap في عملية واحدة.

عالج البيانات الكبيرة باستخدام التدفقات: InputStream مع مخزن مؤقت 4–8 كيلوبايت، محلل JSON متدفق (Jackson أو Gson مع JsonReader)، MediaCodec للفيديو. لا تستدعي File.readBytes() أبدًا على ملفات أكبر من 10% من Heap المتاح.

إنشاء العديد من الكائنات في حلقة

الإنشاء المكثف للكائنات في حلقة بدون GC وسيط يمكن أن يؤدي إلى OOM، خاصة على الأجهزة ذات Heap الصغير. مثال: توليد 100,000 كائن في for-loop لا تسع في Heap قبل أن يتمكن GC من جمعها. هذا أكثر شيوعًا في الألعاب ومحررات الرسوم.

استخدم Object Pool للكائنات التي يتم إنشاؤها وتدميرها بشكل جماعي. للبيانات الرقمية، استخدم الأنواع البدائية (FloatArray بدلاً من List<Float>). RecyclerView مع ViewHolder Pool يحل هذه المشكلة لمكونات واجهة المستخدم.

تجزئة Heap

التجزئة هي حالة حيث توجد ذاكرة خالية كافية إجمالاً، لكن لا يوجد كتلة متجاورة لكائن جديد. ART يقوم بضغط Heap أثناء GC، لكن ليس دائمًا بنجاح. المصفوفات الكبيرة (Bitmap, byte[]) هي الأكثر حساسية للتجزئة.

ART على Android 8+ يستخدم GC الأجيال، الذي يقلل التجزئة عن طريق فصل الكائنات الشابة والقديمة. مع ذلك، تجنب تخصيص أجزاء بأحجام مختلفة في نفس المجموعة — حاول استخدام مخازن مؤقتة مسبقة التخصيص ذات حجم ثابت.

حدود Heap في Android

حد Heap في Android ليس ثابتًا — يعتمد على الشركة المصنعة وطراز الجهاز وإصدار نظام التشغيل. تحدد Google الحد الأدنى من المتطلبات من خلال وثيقة تعريف التوافق (CDD)، لكن الشركات المصنعة تحدد القيم الفعلية.

فئة الجهازHeap نموذجيlargeHeap
اقتصادي (1–2 جيجابايت RAM)128–192 ميجابايت256–384 ميجابايت
متوسط (3–4 جيجابايت RAM)256–384 ميجابايت512 ميجابايت
رائد (6+ جيجابايت RAM)384–512 ميجابايت768 ميجابايت–1 جيجابايت
أجهزة لوحية (4+ جيجابايت RAM)256–512 ميجابايت768 ميجابايت
Wear OS32–64 ميجابايتغير متاح

يمكنك طلب حد متزايد عبر android:largeHeap="true" في البيان. استخدمه بحذر: زيادة Heap لا تحل مشكلة التسريبات وقد تؤدي إلى تفاقم تجربة المستخدم إذا اضطر النظام لقتل تطبيقات أخرى لتحرير الذاكرة لتطبيقك. بالنسبة لـ Wear OS، حد Heap ضئيل — فقط 32–64 ميجابايت، largeHeap غير متاح هنا، وتوفير الذاكرة مهم بشكل مضاعف.

تشخيص OutOfMemoryError

تشخيص OOM يتطلب تحليل Heap Dump وفهم أي الكائنات تستهلك الذاكرة. Android Studio يوفر جميع الأدوات اللازمة.

الخطوة 1: التقط لحظة OOM. في Android Memory Profiler، انقر على Record memory allocations ونفذ السيناريو الذي يسبب التعطل. سيظهر Profiler قفزة في التخصيصات قبل OOM. إذا لم يكن OOM قابلًا للتكرار، قلل Heap عبر android:smallHeap في بناء التصحيح أو استخدم DDMS مع استدعاء GC يدوي.

الخطوة 2: خذ Heap Dump في ذروة التحميل (قبل OOM). افتح Dump في Android Studio: علامة التبويب Classes مرتبة حسب Retained Size. أكبر الكائنات هي Bitmap, byte[], String. لكل Bitmap، تحقق من الحجم (العرض × الارتفاع × 4 بايت) ومسار التحميل عبر Stack Trace.

الخطوة 3: حلل عدد الكائنات المكررة. إذا رأيت 200 Fragment أو Activity متطابقة — فهذا تسرب. إذا 500 Bitmap بنفس الحجم — فهي مشكلة تخزين مؤقت للصور. MAT (Memory Analyzer Tool) يوفر تحليلًا أعمق مع Dominator Tree يظهر أي الكائنات تحتجز 80% من Heap.

text
// أمر Heap Dump عبر adb
adb shell am dumpheap com.example.app /data/local/tmp/dump.hprof
adb pull /data/local/tmp/dump.hprof.

استراتيجيات الوقاية من OOM

استراتيجية شاملة للوقاية من OOM تشمل خمسة مستويات من الحماية: من القرارات المعمارية إلى المراقبة في الإنتاج.

القرارات المعمارية

ViewModel + Repository يفصل البيانات عن واجهة المستخدم ويمنع الاحتفاظ بـ View عند تدوير الشاشة. ViewModel يعمر أكثر من Activity، بياناته لا تُفقد، ويمكن إعادة إنشاء View دون تكرار البيانات في الذاكرة. استخدم StateFlow بدلاً من LiveData للإدارة الصريحة للحالات.

إدارة Bitmap والصور

Glide هي مكتبة إلزامية للعمل مع الصور. تقوم تلقائيًا بالتحجيم والتخزين المؤقت (قرص + ذاكرة) وإعادة تدوير Bitmap. قم بتكوين diskCacheStrategy و skipMemoryCache للقوائم الكبيرة. للصور المتحركة، استخدم Glide مع GIF/WebP — تستهلك ذاكرة أقل من سلسلة من Bitmaps.

المراقبة في الإنتاج

Firebase Performance Monitoring يتتبع استهلاك الذاكرة في الوقت الفعلي. قم بتعيين تنبيه على استخدام Heap يتجاوز 80% من الحد — هذه إشارة للتحقق. Crashlytics يجمع OOM كاستثناء ويظهر آخر حالة معروفة لـ Heap قبل التعطل. لـ Android 11+، استخدم ApplicationExitInfo للكشف عن إنهاءات OOM.

الاختبار على الأجهزة الضعيفة

تأكد من اختبار التطبيق على الأجهزة ذات Heap الأدنى (128–192 ميجابايت). المحاكي بشاشة صغيرة وHeap صغير يحاكي جهازًا اقتصاديًا. إذا كان التطبيق يعمل على هذا الجهاز، لن تكون هناك مشاكل OOM على الأجهزة الرائدة. استخدم Firebase Test Lab مع أجهزة حقيقية من فئات أسعار مختلفة.

kotlin
// التحقق من Heap المتاح قبل عملية ثقيلة
fun canAllocate(requiredBytes: Long): Boolean {
    val runtime = Runtime.getRuntime()
    val free = runtime.freeMemory()
    return free > requiredBytes * 2 // هامش 50%
}

الأسئلة الشائعة

هل يمكن التقاط OutOfMemoryError باستخدام try-catch؟

نعم تقنيًا، لكن هذا غير موصى به. بعد OOM، التطبيق في حالة غير مستقرة: قد تفشل التخصيصات الجديدة، وقد تكون بعض الكائنات منشأة جزئيًا. الإجراء المعقول الوحيد في catch هو التسجيل وإعادة تشغيل Activity.

لماذا لا يحدث OOM على جميع الأجهزة؟

حد Heap يختلف بين الأجهزة. عملية تتطلب 300 ميجابايت ستفشل على جهاز بحد 192 ميجابايت لكنها ستنجح على جهاز رائد بـ 512 ميجابايت. اختبر على أجهزة بأقل المواصفات لاكتشاف سيناريوهات OOM.

كيف يؤثر largeHeap على الأداء؟

largeHeap يزيد الحد لكنه لا يسرع التطبيق. توقفات GC تطول لأن جمع Heap كبير يستغرق وقتًا أطول. قد يضطر النظام لقتل تطبيقات الخلفية لتوفير الذاكرة. استخدم largeHeap فقط للتطبيقات التي تحتاج موضوعيًا للكثير من الذاكرة (الكاميرات، المحررات).

كيف يختلف OOM عن قتل النظام للعملية؟

OOM هو استثناء داخل التطبيق عند عدم كفاية Heap. قتل النظام (Low Memory Killer) هو قرار من نواة Linux لقتل عملية لتحرير الذاكرة لتطبيقات أخرى. في قتل النظام، لا يتلقى التطبيق استثناء — العملية تنتهي ببساطة.

كم تستهلك Bitmap من الذاكرة فعليًا؟

الصيغة: العرض × الارتفاع × bytesPerPixel. ARGB_8888 = 4 بايت/بكسل، RGB_565 = 2 بايت/بكسل. Bitmap FullHD (1920 × 1080) بصيغة ARGB_8888 = 8.3 ميجابايت. Bitmap بدقة 4K (3840 × 2160) = 33 ميجابايت. قم دائمًا بتحجيم الصور إلى الحجم المطلوب للعرض على الشاشة.

الملخص

  • OutOfMemoryError — استثناء fatal عند استنفاد حد Heap للتطبيق
  • Bitmap بدون تحجيم — السبب الرئيسي لـ OOM في التطبيقات المحمولة
  • تسرب الذاكرة يسبب 70% من OOM عبر تراكم الكائنات في كل انتقال
  • حد Heap يتراوح من 128 ميجابايت على الأجهزة الاقتصادية إلى 512 ميجابايت على الأجهزة الرائدة
  • Heap Dump مع تحليل Retained Size — الأداة الرئيسية لتشخيص OOM
  • Glide أو Coil إلزاميان للعمل مع الصور من أي حجم
  • الاختبار على الأجهزة ذات Heap الأدنى أمر لا بد منه لجميع المشاريع

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا