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

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

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

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

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

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

تحرير الذاكرة تجاوبا مع الأحداث

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

يمكنك تنفيذ استدعاء 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) {

        // Determine which lifecycle or system event is raised.
        when (level) {

            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                /*
                   Release any UI objects that currently hold memory.

                   The user interface moves to the background.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                /*
                   Release any memory your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system
                   begins stopping background processes.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
            ComponentCallbacks2.TRIM_MEMORY_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process is one of the
                   first to be terminated.
                */
            }

            else -> {
                /*
                  Release any non-critical data structures.

                  The app receives an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
            }
        }
    }
}

لغة 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) {

        // Determine which lifecycle or system event is raised.
        switch (level) {

            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                /*
                   Release any UI objects that currently hold memory.

                   The user interface moves to the background.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:

                /*
                   Release any memory your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system
                   begins stopping background processes.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
            case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:

                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process is one of the
                   first to be terminated.
                */

                break;

            default:
                /*
                  Release any non-critical data structures.

                  The app receives an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
                break;
        }
    }
}

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

للسماح بعمليات تشغيل متعددة، يضع Android حدًا صارمًا لحجم الذاكرة المخصصة لكل تطبيق. ويختلف الحد الدقيق لحجم الذاكرة باختلاف الأجهزة استنادًا إلى حجم ذاكرة الوصول العشوائي المتوفرة على الجهاز بشكل عام. إذا وصل تطبيقك إلى سعة كومة الذاكرة المؤقتة وحاول تخصيص المزيد من الذاكرة، سيعرض النظام 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;
}

استخدام تركيبات رموز أكثر كفاءة في الذاكرة

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

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

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

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

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

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

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

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

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

توخي الحذر بشأن تجريدات التعليمات البرمجية

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

استخدام نماذج أوّلية بسيطة للبيانات المتسلسلة

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

لمزيد من المعلومات، راجِع ملف Protobuf التمهيدي.

تجنُّب استهلاك الذاكرة

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

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

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

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

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

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

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

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

إزالة الموارد والمكتبات التي تستهلك الكثير من الذاكرة

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

تقليل الحجم الإجمالي لحزمة APK

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

للحصول على مزيد من المعلومات عن تقليل الحجم الإجمالي لتطبيقك، يمكنك الاطّلاع على تقليل حجم تطبيقك.

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

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

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

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

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

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

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

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

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