إدارة ذاكرة تطبيقك

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

ذاكرة الوصول العشوائي (RAM) هي مورد قيّم لأي بيئة لتطوير البرامج، وهي أكثر أهمية لنظام التشغيل على الأجهزة الجوّالة حيث تكون الذاكرة الفعلية محدودة في كثير من الأحيان. على الرغم من أنّ كلًا من وقت تشغيل Android (ART) والجهاز الافتراضي Dalvik يجريان عملية جمع البيانات المُهمَلة بشكل روتيني، لا يعني ذلك أنّه يمكنك تجاهل وقت ومكان تخصيص تطبيقك للذاكرة وتحريرها. ومع ذلك، عليك تجنُّب حدوث تسرُّبات في الذاكرة، والتي تحدث عادةً بسبب الاحتفاظ بمراجع الكائنات في متغيرات الأعضاء الثابتة، وإصدار أي كائنات Reference في الوقت المناسب كما هو محدّد من خلال عمليات معاودة الاتصال بدورة الحياة.

تقليل حجم الرموز والموارد في تطبيقك

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

تقليل الحجم الإجمالي للتطبيق من خلال تفعيل R8

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

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

فهم قواعد الاحتفاظ

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

تمنع قواعد الاحتفاظ المكتوبة بشكل سيئ أداة R8 من تحسين أجزاء كبيرة من قاعدة الرموز البرمجية. تجنَّب قواعد الاحتفاظ الواسعة النطاق واتّبِع أفضل الممارسات التالية:

  • القواعد العامة التي يجب تجنُّبها:
    • -dontoptimize: يؤدي إلى إيقاف التحسين للتطبيق بأكمله، ما يؤدي إلى إنشاء ملفات تنفيذية أكبر وأبطأ.
    • -dontshrink: يمنع إزالة الرموز والموارد غير المستخدَمة.
    • -dontobfuscate: يمنع تصغير الأسماء، ما يؤدي إلى عدم الاستفادة من توفير الذاكرة (خاصةً في التطبيقات الكبيرة).
  • تجنُّب أحرف البدل على مستوى الحزمة: تؤدي القواعد العامة، مثل -keep class com.example.package.** { *; }، إلى إجبار R8 على الاحتفاظ بكل فئة وحقل وطريقة في تلك الحزمة. يؤدي ذلك إلى إيقاف قدرة R8 تمامًا على إزالة الرمز أو تحسينه أو تصغيره في تلك الحزمة.

  • استخدام ملف الإعداد التلقائي في R8: استخدِم proguard-android-optimize.txt دائمًا.

لمزيد من المعلومات حول كتابة قواعد الاحتفاظ، يُرجى الاطّلاع على نظرة عامة على قواعد الاحتفاظ. للاطّلاع على الأنماط المحدّدة التي يجب استخدامها وتجنُّبها، راجِع أفضل الممارسات المتعلّقة بقواعد الاحتفاظ.

تقدّم أداة "محلّل إعدادات R8" إحصاءات حول إعدادات R8 وكيفية تأثير كل قاعدة إبقاء في تطبيقك. لمزيد من المعلومات حول كيفية تحديد القواعد التي تحظر التحسين، راجِع محلّل إعدادات R8.

توخَّ الحذر بشأن استخدام المكتبات الخارجية

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

وحتى بعض المكتبات المحسّنة للأجهزة الجوّالة يمكن أن تتسبّب في حدوث مشاكل بسبب اختلاف عمليات التنفيذ. على سبيل المثال، قد تستخدم إحدى المكتبات بروتوكولات lite protobuf، بينما تستخدم مكتبة أخرى بروتوكولات micro protobuf، ما يؤدي إلى تنفيذ بروتوكولات protobuf مختلفة في تطبيقك. ويمكن أن يحدث ذلك مع عمليات التنفيذ المختلفة للتسجيل والتحليلات وأُطر تحميل الصور والتخزين المؤقت والعديد من الأشياء الأخرى التي لا تتوقّعها.

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

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

استخدام Hilt أو Dagger 2 لتوفير التبعيات

يمكن أن تُبسّط أُطر عمل إدخال التبعية الرمز الذي تكتبه وتوفّر بيئة قابلة للتكيّف ومفيدة للاختبار والتغييرات الأخرى في الإعدادات.

إذا كنت تنوي استخدام إطار عمل لتوفير التبعية في تطبيقك، ننصحك باستخدام Hilt أو Dagger. ‫Hilt هي مكتبة لتوفير التبعيات في Android، وتعمل على أساس Dagger. لا تستخدم Dagger الانعكاس لمسح رمز تطبيقك ضوئيًا. يمكنك استخدام التنفيذ الثابت لوقت الترجمة البرمجية في Dagger في تطبيقات Android بدون تكلفة غير ضرورية لوقت التشغيل أو استخدام الذاكرة.

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

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

تحديد الغرض من تحميل الصور

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

على سبيل المثال، تستخدم معظم الصور النقطية إعداد ARGB_8888، ما يعني أنّ كل بكسل يتطلّب 4 بايت من الذاكرة، أي بايت واحد لكل من الأحمر والأخضر والأزرق وقناة ألفا (الشفافية). إذا كان لديك صورة JPEG بحجم 100 كيلوبايت وعرضتها في عرض بحجم 1000×1000 بكسل، ستتطلّب الصورة النقطية 4 بايت لكل بكسل من تلك المليون بكسل، ما يؤدي إلى إضافة ما يصل إلى 4 ميغابايت من الذاكرة.

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

مراقبة الذاكرة المتاحة واستخدام الذاكرة

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

تتكامل أداة تحليل استخدام الذاكرة أيضًا مع مكتبة LeakCanary لرصد تسرُّبات الذاكرة. باستخدام LeakCanary، يمكنك نقل تحليل تسرُّب الذاكرة من الجهاز التجريبي إلى جهاز التطوير، ما قد يؤدي إلى تسريع عملية التطوير بشكل كبير. لمزيد من المعلومات، يُرجى الاطّلاع على ملاحظات إصدار "استوديو Android".

هناك أدوات أخرى يمكنك استخدامها لتشخيص مشاكل الذاكرة استنادًا إلى بيانات من المستخدمين الذين يشغّلون تطبيقك على الإصدار العلني:

تحرير الذاكرة استجابةً للأحداث

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

يجب أن يركّز تنفيذ onTrimMemory() حصريًا على الحدثَين TRIM_MEMORY_UI_HIDDEN وTRIM_MEMORY_BACKGROUND. (اعتبارًا من Android 14، لم يعُد النظام يرسل إشعارات بشأن الثوابت الأخرى القديمة. تم إيقاف هذه الثوابت نهائيًا في Android 15.)

  • TRIM_MEMORY_UI_HIDDEN: تشير هذه الإشارة إلى أنّ واجهة مستخدم تطبيقك لم تعُد ظاهرة للمستخدم. يتيح هذا الانتقال فرصة لتحرير مساحات كبيرة من الذاكرة المرتبطة بشكل صارم بواجهة المستخدم، مثل صور نقطية أو مخازن مؤقتة لتشغيل الفيديو أو موارد صور متحركة معقّدة.

  • TRIM_MEMORY_BACKGROUND: تشير هذه الإشارة إلى أنّ عمليتك تعمل في الخلفية، وهي الآن مرشّحة للإيقاف من أجل تلبية احتياجات الذاكرة العامة للنظام. لإطالة مدة بقاء العملية في حالة التخزين المؤقت وتقليل عدد عمليات بدء تشغيل التطبيق على البارد، عليك إيقاف أي موارد يمكن إعادة إنشائها بسهولة عندما يستأنف المستخدم جلسته.

تعرض عيّنة تعليمات برمجية هذه كيفية تنفيذ عملية الاسترجاع onTrimMemory() للاستجابة لأحداث مختلفة متعلقة بالذاكرة:

Kotlin

import android.content.ComponentCallbacks2
// Other import statements.

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

    // Other activity code.

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    override fun onTrimMemory(level: Int) {

        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }

        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

Java

import android.content.ComponentCallbacks2;
// Other import statements.

public class MainActivity extends AppCompatActivity
    implements ComponentCallbacks2 {

    // Other activity code.

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    public void onTrimMemory(int level) {

        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }

        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

التحقّق من حجم الذاكرة التي تحتاج إليها

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

لتجنُّب نفاد الذاكرة، يمكنك طلب معلومات من النظام لتحديد مقدار مساحة الذاكرة المؤقتة المتوفّرة على الجهاز الحالي. يمكنك طلب هذا الرقم من النظام من خلال استدعاء getMemoryInfo(). تعرض هذه السمة عنصر ActivityManager.MemoryInfo يقدّم معلومات حول حالة الذاكرة الحالية للجهاز، بما في ذلك الذاكرة المتاحة وإجمالي الذاكرة وحدّ الذاكرة الأدنى، أي مستوى الذاكرة الذي يبدأ عنده النظام في إيقاف العمليات. يعرض العنصر ActivityManager.MemoryInfo أيضًا lowMemory، وهو قيمة منطقية بسيطة تخبرك ما إذا كانت مساحة الذاكرة في الجهاز منخفضة.

يوضّح مقتطف الرمز البرمجي التالي كيفية استخدام طريقة getMemoryInfo() في تطبيقك.

Kotlin

fun doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    if (!getAvailableMemory().lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private fun getAvailableMemory(): ActivityManager.MemoryInfo {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    return ActivityManager.MemoryInfo().also { memoryInfo ->
        activityManager.getMemoryInfo(memoryInfo)
    }
}

Java

public void doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();

    if (!memoryInfo.lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
    ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
    ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
    activityManager.getMemoryInfo(memoryInfo);
    return memoryInfo;
}

رصد عمليات إيقاف التطبيقات بسبب انخفاض الذاكرة

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

تُعد عمليات إيقاف العمليات التي تعمل في المقدّمة من أهم المؤشرات في "مؤشرات Android الحيوية" لأنّها تُستخدَم كوكيل عالي الدقة لإدارة الذاكرة بشكل غير سليم. مع أنّ أي معدّل LMK أعلى من% 1 يشير إلى حاجة ملحّة لاتخاذ إجراء فوري، إلا أنّ المعدّل المنخفض ليس بالضرورة مؤشرًا على سلامة التطبيق. قد يشير انخفاض معدّل أعطال "إغلاق التطبيقات بسبب نقص الذاكرة" التي لاحظها المستخدمون إلى أنّ عملية LMK daemon توقف العمليات بشكل متكرّر أثناء تشغيلها في الخلفية، ما يؤدي إلى تدهور أداء "التشغيل السريع" وسلاسة تنفيذ المهام المتعددة. لذلك، ننصحك باتّباع أفضل الممارسات المتعلّقة بالذاكرة بغض النظر عن نتيجة LMK الحالية، وذلك لضمان الاستقرار على المدى الطويل وحالة الجهاز الجيدة.

استخدام ProfilingManager لتتبُّع مشاكل الذاكرة

يوفر نظام Android الأساسي ProfilingManager، وهي واجهة برمجة تطبيقات متقدمة لإمكانية تتبّع البيانات تتيح لك جمع بيانات المستخدمين في الإصدار العلني استنادًا إلى المشغّلات التي تحدّدها. يمكن أن يساعدك ذلك في تحديد مشاكل الذاكرة التي يصعب إعادة إنتاجها.

هناك مشغّلان جديدان تم تقديمهما مع Android 17 وهما مفيدان بشكل خاص في رصد مشاكل الذاكرة:

  • يشير TRIGGER_TYPE_OOM إلى أنّ التطبيق قد أرسل OutOfMemoryError. يتم تشغيله في المرة التالية التي يبدأ فيها التطبيق بعد تعطُّله، وذلك عندما يسجّل التطبيق مشغّلات إنشاء الملفات الشخصية.
  • TRIGGER_TYPE_ANOMALY يتم تفعيل هذا الإذن عندما يرصد النظام سلوكًا غير طبيعي من التطبيق، ويمكن أن يحدث ذلك، من بين أمور أخرى، بسبب الاستخدام المفرط للذاكرة. يتم تشغيلها بعد أن يستهلك التطبيق مساحة كبيرة من الذاكرة، وقبل أن يتخذ النظام أي إجراء لإيقاف العملية المخالفة. على سبيل المثال، إذا تجاوز التطبيق حدود الذاكرة التي تم تقديمها في Android 17، سيتم تشغيل TRIGGER_TYPE_ANOMALY قبل أن يوقف النظام التطبيق.

لمزيد من المعلومات حول استخدام ProfilingManager لتسجيل عمليات التشغيل واسترجاعها آليًا، يُرجى الاطّلاع على مستندات إنشاء الملفات الشخصية المستندة إلى عمليات التشغيل. تتيح أداة Firebase Crashlytics أيضًا استخدام كل من مشغّلي TRIGGER_TYPE_OOM وTRIGGER_TYPE_ANOMALY، كما تربط بينهما وبين البيانات الوصفية الأخرى الخاصة ببيانات التشخيص.

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

تتبُّع حالة العملية والذاكرة

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

لوحة بيانات Python للتتبُّع المستمر

استخدِم نص Python البرمجي Android Memory Monitor أثناء تشغيل تطبيقك للحصول على عرض مراقبة مستمر لعمليات تطبيقك ومساحة الذاكرة التي يشغلها. هذه أداة سطر أوامر تستخدم أوامر adb لعرض لوحات بيانات على الجهاز الطرفي وعلى الويب لمساعدتك في فهم حالة عمليات تطبيقك واستخدام الذاكرة فيها. لتشغيل لوحات البيانات، شغِّل النص البرمجي مع اسم حزمة تطبيقك، على سبيل المثال:

python3 android_mem_monitor.py your.package.name

للحصول على تعليمات تفصيلية حول ميزات لوحة البيانات واستخدامها، يُرجى الرجوع إلى ملف README أو التعليقات المضمّنة في النص البرمجي.

في وقت التشغيل مع ActivityManager

لاسترداد معلومات حول العمليات الجارية في وقت التشغيل، يمكنك طلب البحث من خلال ActivityManager.getRunningAppProcesses(). تعرض هذه الطريقة قائمة بالكائنات التي تحتوي على معلومات أساسية عن كل عملية من عمليات تطبيقك. يشير الحقل importance إلى الأهمية النسبية التي يوليها النظام للعملية، ويتم ربطه بالقيم الثابتة التالية:

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

لتصحيح الأخطاء باستخدام adb

عند تصحيح أخطاء سلوك الذاكرة ودورات حياة العمليات، يمكنك أيضًا استخدام أوامر adb للمساعدة في التحقيق في نتائج OOM الخاصة بتطبيقك ومساحة الذاكرة المستخدَمة في الوقت الفعلي. لمراقبة عمليات تطبيقك وحالات الذاكرة، يمكنك استخدام الأوامر التالية:

  • البحث عن أرقام تعريف العمليات النشطة (PIDs): ابحث أولاً عن جميع العمليات التي يشغّلها تطبيقك حاليًا باستخدام الأمر ps الذي تم فلترته حسب اسم الحزمة.
    shell adb shell ps -A | grep your.package.name
  • التحقّق من "نتيجة OOM" الحالية: استخدِم رقم تعريف العملية (PID) لقراءة نتيجة OOM من نظام ملفات العملية. يشير الرقم من 0 إلى 199 عادةً إلى تطبيق يعمل في المقدّمة، بينما يشير الرقم من 200 إلى 249 إلى خدمة يمكن إدراكها، ويشير الرقم من 250 إلى 899 إلى خدمة تُشغَّل في الخلفية، أما الأرقام 900 أو أكثر فتشير إلى أنّ العملية مخزّنة مؤقتًا ومن المحتمل أن يتم إيقافها.
    shell adb shell cat /proc/pid/oom_score_adj
  • مراقبة "نقاط OOM" باستمرار: لمراقبة التغييرات في "نقاط OOM" لتطبيق ما باستمرار (وهي ستكون أدنى "نقاط OOM" بين جميع عملياته)، يمكنك استخدام الأمر watch-uids.
    shell adb shell am watch-uids
  • قياس مساحة الذاكرة المستخدَمة: للحصول على قراءة سريعة لمساحة الذاكرة الأساسية المستخدَمة في عملية ما، مثل استخدام ذاكرة RSS وذاكرة التبديل المجهولة، يمكنك قراءة حالتها مباشرةً من نظام ملفات عمليات Linux.
    shell adb shell cat /proc/pid/status | grep -E "VmRSS|RssAnon|VmSwap"
    للحصول على تفاصيل أكثر شمولاً حول عمليات تخصيص الذاكرة، استخدِم الأمر dumpsys meminfo:
    shell adb shell dumpsys meminfo pid
  • التحقّق من المكوّنات النشطة: إذا كانت العملية تحافظ على أولوية عالية بشكل غير متوقّع (مثل البقاء عند درجة OOM تبلغ 200 بدلاً من الانخفاض إلى حالة التخزين المؤقت)، يمكنك فحص ما يتم تنفيذه بالضبط في العملية. يكشف الأمر dumpsys activity processes عن الأنشطة والخدمات وموفّري المحتوى والمستقبِلات المحدّدة التي تحافظ على استمرار العملية.
    shell adb shell dumpsys activity processes your.package.name

استخدام بنى رموز برمجية أكثر فعالية من حيث استخدام الذاكرة

تستهلك بعض ميزات Android وفئات Java وبُنى الرموز البرمجية مقدارًا أكبر من الذاكرة مقارنةً بغيرها. يمكنك تقليل مقدار الذاكرة التي يستخدمها تطبيقك من خلال اختيار بدائل أكثر كفاءة في الرمز البرمجي.

استخدام الخدمات باعتدال

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

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

بشكل عام، تجنَّب استخدام الخدمات الدائمة بسبب المتطلبات المستمرة التي تفرضها على الذاكرة المتاحة. بدلاً من ذلك، ننصحك باستخدام بديل، مثل WorkManager. لمزيد من المعلومات حول كيفية استخدام WorkManager لجدولة العمليات التي تعمل في الخلفية، راجِع العمل المستمر.

استخدام حاويات البيانات المحسّنة

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

يتضمّن إطار عمل Android العديد من حاويات البيانات المحسَّنة، بما في ذلك SparseArray وSparseBooleanArray وLongSparseArray. على سبيل المثال، تكون فئات SparseArray أكثر فعالية لأنّها تتجنّب حاجة النظام إلى التحويل التلقائي للمفتاح وأحيانًا للقيمة، ما يؤدي إلى إنشاء عنصر أو عنصرين آخرين لكل إدخال.

يمكنك دائمًا التبديل إلى المصفوفات الأولية للحصول على بنية بيانات بسيطة إذا لزم الأمر.

يجب التعامل بحذر مع تجريدات الرموز

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

استخدام بروتوكولات lite protobufs للبيانات المتسلسلة

بروتوكول المخازن المؤقتة (protobufs) هو آلية قابلة للتوسيع ومصمَّمة من قِبل Google، وهي مستقلة عن اللغة والنظام الأساسي، وتهدف إلى تسلسل البيانات المنظَّمة، وهي تشبه XML، ولكنها أصغر حجمًا وأسرع وأبسط. إذا كنت تستخدم بروتوكولات protobuf لبياناتك، استخدِم دائمًا بروتوكولات protobuf الخفيفة في الرمز البرمجي من جهة العميل. تنشئ بروتوكولات protobuf العادية رموزًا برمجية مطوّلة للغاية، ما يزيد من حجم الرمز البرمجي لتطبيقك في ذاكرة الوصول العشوائي (RAM) (راجِع تقليل حجم الرمز البرمجي والموارد في تطبيقك) ويساهم في زيادة حجم حزمة APK.

لمزيد من المعلومات، يُرجى الاطّلاع على ملف readme الخاص بـ protobuf.

توخّي الحذر بشأن تسرّبات الذاكرة

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

تجنُّب الاستخدام الزائد للذاكرة

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

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

على سبيل المثال، يمكنك تخصيص عناصر مؤقتة متعددة ضمن حلقة for. أو يمكنك إنشاء عناصر Paint أو Bitmap جديدة داخل الدالة onDraw() الخاصة بأحد العروض. في كلتا الحالتين، ينشئ التطبيق الكثير من العناصر بسرعة وبكميات كبيرة. ويمكن أن تستهلك هذه العمليات بسرعة كل الذاكرة المتاحة في الجيل الشاب، ما يؤدي إلى حدوث حدث لجمع البيانات المُهمَلة.

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

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

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

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

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