الحفاظ على استمرار العمل في الخلفية من خلال عملية بسيطة

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

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

لماذا يحتفظ العمل على عملية واحدة بذاكرة واجهة المستخدم؟

يدير Android الذاكرة وإنهاء العمليات على مستوى عملية نظام التشغيل (pid)، وليس على مستوى المكوّنات الفردية (Activity أو Service أو BroadcastReceiver). يخصّص "مدير النشاط" (OomAdjuster) لكل عملية قيمة oom_score_adj واحدة تعكس المكوّن النشط الأكثر أهمية أو الربط الوارد في تلك العملية:

  • عندما يتفاعل المستخدم مع Activity، يتم تشغيل العملية في الحالة TOP (oom_score_adj = 0).
  • عندما ينتقل المستخدم بعيدًا عن التطبيق ولا يتضمّن تطبيقك أي مكوّنات نشطة في الخلفية، تنتقل العملية إلى مجموعة PREV (oom_score_adj = 700) أو CACHED (oom_score_adj >= 900). في الحزمة المخزّنة مؤقتًا، يجمّد النظام الأساسي العملية باستخدام ميزة التجميد cgroup v2، ويضغط ذاكرتها باستخدام CachedAppOptimizer، ويسمح لـ lmkd بإنهاء العملية أولاً في ظل ضغط الذاكرة.
  • ومع ذلك، إذا كانت العملية نفسها تشغّل أيضًا خدمة تعمل في المقدّمة (مثل dataSync أو mediaPlayback أو connectedDevice أو location الأنواع، في oom_score_adj = 200) أو تستضيف خدمة يرتبط بها النظام أو عميل مهم آخر، تظل العملية بأكملها في تلك الأولوية المرتفعة حتى بعد إيقاف Activity.
مقارنة بين عملية واحدة مشتركة في الموقع تحافظ على تثبيت ذاكرة واجهة المستخدم
       عند oom_score_adj 200 مقابل عزل العمليات المنفصلة مع الحفاظ على
       العامل فقط عند oom_score_adj 200 بينما تنخفض عملية واجهة المستخدم إلى الذاكرة المؤقتة.
الشكل 1. مقارنة بين الموقع المشترك للعملية الواحدة وعزل العمليات المنفصلة أثناء العمل في الخلفية

عندما تظل عملية واجهة مستخدم ثقيلة في oom_score_adj = 200، تظل عدة فئات كبيرة من الذاكرة مقيمة:

  • إيقاف Activity وهيكلية طرق العرض: ما لم يتم إنهاء Activity بشكل صريح، تظل Window وشجرة طرق العرض وبُنى التنسيق وحالة طرق العرض قابلة للوصول إليها في كومة ذاكرة Java أثناء استخدام المستخدم لتطبيق آخر.
  • سياقات الرسومات الأصلية وتخصيصات وحدة معالجة الرسومات: يحتفظ HardwareRenderer العادي بسياق وحدة معالجة الرسومات وذاكرة التخزين المؤقت، بينما يتوفّر Activity المتوقف في عملية لا يتم تخزينها مؤقتًا. تحتفظ التطبيقات ومحرّكات الألعاب التي تنشئ سياقات Vulkan الخاصة بها (VkDevice وVkSwapchainKHR ومجموعات الواصفات وذاكرة التخزين المؤقت لمسارات المعالجة) أو سياقات OpenGL ES (EGLContext والزخارف ومخازن الرؤوس المؤقتة) أيضًا ببُنى برامج التشغيل وبرامج التظليل ومخازن الرسومات المؤقتة ما لم يزِل المحرّك جهاز الرسومات صراحةً عند إيقافه.
  • تجزئة الذاكرة الديناميكية ونفقات وقت التشغيل: حتى عندما يحرّر التطبيق مساحة التخزين المؤقت في onTrimMemory، تظل صفحات المخصِّص الأصلي الممتلئة جزئيًا، وصفحات Zygote غير الصالحة التي يتم نسخها عند الكتابة، والمكتبات المشتركة المحمَّلة (.so)، والبيانات الوصفية لوقت التشغيل مقيمة في الذاكرة الخاصة للعملية. بما أنّ العملية تتجاوز الحدّ الأقصى المسموح به للتخزين المؤقت (oom_score_adj < 900)، لا يجمّدها النظام الأساسي ولا يضغط التطبيق المخزَّن مؤقتًا. لإلقاء نظرة عن كثب على هذا التأثير في خدمات الربط، يُرجى الاطّلاع على تحديد موقع خدمة ربط مع واجهة مستخدم تستهلك قدرًا كبيرًا من الذاكرة.

كيفية حماية تطبيقك من خلال عملية في الخلفية لا تستهلك الكثير من الموارد

إنّ الاحتفاظ بذاكرة بحجم الذاكرة المستخدمة في المقدّمة أثناء تشغيل التطبيق في الخلفية يعرّض عمل التطبيق في الخلفية للخطر:

  • إنّ حدود الذاكرة في الخلفية أكثر صرامة من حدود الذاكرة في المقدّمة. في الإصدار 17 من نظام التشغيل Android والإصدارات الأحدث، يطبّق محدِّد الذاكرة حدًّا أصغر للذاكرة غير المرئية على تطبيقك عندما لا تعرض أي من عملياته واجهة مستخدم مرئية، بما في ذلك أثناء تشغيل خدمة تعمل في المقدّمة. ويشمل الحدّ الأقصى جميع عمليات تطبيقك معًا. إذا كانت العملية التي تشغّل الخدمة التي تعمل في المقدّمة لا تزال تحتفظ بواجهة المستخدم وذاكرة الرسومات، يتم احتساب هذه الذاكرة ضمن الحدّ الأقصى الأصغر. عندما يتجاوز تطبيقك الحدّ الأقصى، تحدّ النواة من سرعته أثناء استعادة صفحاته، ما يؤدي إلى إبطاء عملية التنزيل أو المزامنة أو التشغيل الجارية. إذا استمر تطبيقك في تخصيص الذاكرة بعد نفاد مساحة المبادلة، سيُنهي النظام عمله. للتحقّق مما إذا كان تطبيقك قد تأثّر، يُرجى الاطّلاع على حدود ذاكرة التطبيق.
  • يتم إنهاء العمليات الأكبر أولاً عند الضغط على الذاكرة. عندما يحتاج نظام التشغيل lmkd إلى إخلاء مساحة في الذاكرة ويتم ضبطه لاختيار العملية الأثقل في مجموعة الأهمية (ro.lmk.kill_heaviest_task)، يتم إنهاء عملية خدمة تعمل في المقدّمة تحتفظ أيضًا بمئات الميغابايت من ذاكرة واجهة المستخدم قبل العمليات الأصغر حجمًا ذات الأهمية نفسها، ويتوقف عملك في الخلفية معها. يُرجى الاطّلاع على التفاعل بين kswapd وlmkd.
  • يمكن تجميد حالة واجهة المستخدم المخزّنة مؤقتًا وضغطها وإعادة كتابتها في ذاكرة الفلاش. من خلال السماح لحالة واجهة المستخدم في المقدّمة بالانتقال إلى عملية مخزَّنة مؤقتًا، يمكن لنظام التشغيل تجميد العملية وضغط ذاكرتها في zRAM وإعادة كتابة صفحاتها الباردة في وحدة التخزين المحمولة. ويتقلّص استهلاك الذاكرة المقيمة بينما تظل حالة واجهة المستخدم سليمة، ما يمنح كلاً من العمل في الخلفية وواجهة المستخدم المخزّنة مؤقتًا أفضل فرصة للبقاء حتى يعود المستخدم.
  • تصبح ميزانيات الذاكرة لكل عملية عملية. العملية التي تنفّذ مهام في الخلفية فقط تتضمّن مجموعة عمل صغيرة يمكن توقّعها، لذا يمكنك منحها ميزانية ذاكرة محدودة بدون تقييد واجهة المستخدم.

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

عزل العمل في الخلفية في عملية مخصّصة

لإزالة واجهة المستخدم والرسومات أثناء العمل في الخلفية، يمكنك تشغيل مكوّنات الخلفية الطويلة الأمد في عملية خفيفة مخصّصة مع الاحتفاظ بواجهة المستخدم ومحرك الرسومات في العملية الرئيسية:

معالجة بيان ملف البيان المكوّنات المستضافة الحالة عندما يغادر المستخدم واجهة المستخدم أثناء العمل في الخلفية
عملية واجهة المستخدم الرئيسية مقطع الصوت التلقائي (com.example.app) Activity، وطرق عرض واجهة المستخدم، وذاكرة التخزين المؤقت للصور، وأداة عرض Vulkan أو OpenGL ES، ومحرّك الصوت والمشاهد تنخفض إلى PREV ثم CACHED (oom_score_adj >= 900). يتم تجميدها وضغطها وإعادة كتابتها على وحدة التخزين المحمولة.
عملية عامل في الخلفية android:process=":bg" العمليات في المقدّمة Service، أو أداة تنزيل مواد العرض في الخلفية، أو عامل المزامنة، أو مدير الأجهزة المتصلة يتم تشغيلها في FGS (oom_score_adj = 200) باستخدام الرمز البرمجي والكومة الخاصَّين بالعامل فقط، وبدون طرق عرض واجهة المستخدم أو سياقات وحدة معالجة الرسومات.

الإعلان عن ميزانيات العمليات والذاكرة

يمكنك تعيين الخلفية Service لعملية فرعية خاصة من خلال ضبط android:process=":bg" في AndroidManifest.xml. تؤدي النقطتان الرأسيتان في البداية (:) إلى إنشاء عملية خاصة تقتصر على معرّف UID الخاص بتطبيقك.

في الإصدار Android 17 QPR2 (مستوى واجهة برمجة التطبيقات 37.2) والإصدارات الأحدث، يمكنك دمج تقسيم العمليات مع ميزانيات ذاكرة التطبيق لكل عملية داخل مجموعة <processes> لكي يفرض نظام التشغيل حدًا أقصى صارمًا لمجموعة العمل على العامل في الخلفية بدون تقييد عملية واجهة المستخدم في المقدّمة:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:name=".HeavyApplication"
        android:label="@string/app_name">

        <!-- App-wide budgets that cover every process, including :bg -->
        <memory-budget android:maxMb="512" />
        <memory-budget
            android:maxMb="128"
            android:state="background" />

        <processes>
            <!-- Tight budget for the headless background worker process -->
            <process android:process=":bg">
                <memory-budget android:maxMb="32" />
            </process>
        </processes>

        <activity
            android:name=".MainActivity"
            android:exported="true" />

        <service
            android:name=".AssetDownloadService"
            android:process=":bg"
            android:foregroundServiceType="dataSync"
            android:exported="false" />

    </application>
</manifest>

يتم احتساب استخدام الذاكرة في :bg ضمن ميزانيتها الخاصة وميزانية التطبيق ككل. على الأجهزة التي تعمل بإصدارات أقدم من Android 17 QPR2، يتجاهل النظام <memory-budget>، بينما يعمل android:process=":bg" على جميع مستويات واجهة برمجة التطبيقات.

Gate initialization in Application.onCreate

ينشئ Android مثيلاً لفئة Application ويستدعي Application.onCreate في كل عملية يتم إنشاؤها لحزمتك. من الأخطاء الشائعة عند استخدام android:process=":bg" ترك مجموعات أدوات واجهة المستخدم الثقيلة أو مجموعات تحميل الصور أو حزم تطوير البرامج (SDK) للإحصاءات أو إعداد المحرك الأصلي (System.loadLibrary) داخل طريقة Application.onCreate غير محمية. يؤدي ذلك إلى تضخيم عملية :bg باستخدام البصمة الأساسية نفسها التي تحاول تجنُّبها.

تحقَّق من Application.getProcessName، المتوفّر في الإصدار 9 من نظام التشغيل Android (المستوى 28 من واجهة برمجة التطبيقات) والإصدارات الأحدث، داخل onCreate وابدأ فقط التبعيات التي تتطلّبها العملية الحالية:

Kotlin

class HeavyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        val processName = Application.getProcessName()
        if (processName != null && processName.endsWith(BG_PROCESS_SUFFIX)) {
            initBackgroundWorkerProcess()
            return
        }
        initMainUiProcess()
    }

    private fun initBackgroundWorkerProcess() {
        // Initialize only lightweight HTTP, storage, and sync primitives.
        // Avoid loading native rendering libraries or allocating UI caches.
    }

    private fun initMainUiProcess() {
        // Initialize UI frameworks, image caches, and native engines.
        System.loadLibrary("heavy_engine_jni")
    }

    companion object {
        private const val BG_PROCESS_SUFFIX = ":bg"
    }
}

Java

public final class HeavyApplication extends Application {
    private static final String BG_PROCESS_SUFFIX = ":bg";

    @Override
    public void onCreate() {
        super.onCreate();
        String processName = Application.getProcessName();
        if (processName != null && processName.endsWith(BG_PROCESS_SUFFIX)) {
            initBackgroundWorkerProcess();
            return;
        }
        initMainUiProcess();
    }

    private void initBackgroundWorkerProcess() {
        // Initialize only lightweight HTTP, storage, and sync primitives.
        // Avoid loading native rendering libraries or allocating UI caches.
    }

    private void initMainUiProcess() {
        // Initialize UI frameworks, image caches, and native engines.
        System.loadLibrary("heavy_engine_jni");
    }
}

المكتبات التي يتم إعدادها من خلال أداة توفير المحتوى، مثل Jetpack App Startup، يتم إعدادها فقط في العملية الرئيسية، لأنّ أداة توفير المحتوى التي تم تعريفها بدون android:process يتم تشغيلها في العملية الرئيسية. إذا كان الرمز البرمجي في :bg يعتمد على مكتبة من هذا النوع، عليك إعدادها بشكل صريح في initBackgroundWorkerProcess.

إذا كنت تطوّر لعبتك باستخدام محرّك ألعاب مثل Unity، يمكنك تنفيذ الخدمة التي تُشغَّل في الخلفية في مكتبة Android عادية أو مكوّن إضافي مكتوب بلغة Java أو Kotlin لا يشير إلى فئات المحرّك مثل UnityPlayer. بعد ذلك، لا يتم تحميل المكتبات المجمّعة من رموز برمجية أصلية للمحرك في عملية :bg. للتأكيد، تحقَّق من أنّ adb shell cat /proc/<pid>/maps لعملية :bg لا يسرد libunity.so أو libil2cpp.so.

التواصل بدون تثبيت عملية واجهة المستخدم

عندما يفتح المستخدم MainActivity أثناء تشغيل AssetDownloadService في :bg، غالبًا ما تحتاج واجهة المستخدم إلى عرض مستوى تقدّم عملية التنزيل المباشر. تتدفق أهمية العملية من العميل إلى العملية التي تستضيف الخدمة أو أداة توفير المحتوى التي يستخدمها، لذا فإن اتجاه كل تبعية مهم:

  • إنّ ربط واجهة المستخدم بالعامل آمن. عندما يرتبط MainActivity بـ AssetDownloadService، يرفع النظام :bg إلى مستوى أهمية واجهة المستخدم المرئية. عندما يغادر المستخدم، يتم تخزين عملية واجهة المستخدم مؤقتًا ولا يؤدي الربط إلى رفع أي شيء.
  • إنّ العامل الذي يعتمد على عملية واجهة المستخدم ليس آمنًا. إذا كان الرمز البرمجي في :bg يرتبط بخدمة أو يرسل طلبات إلى أداة توفير محتوى تعمل في العملية الرئيسية، يرفع النظام العملية الرئيسية إلى مستوى الأهمية العالية للعامل، ويظل تثبيت ذاكرة واجهة المستخدم مرة أخرى. بدلاً من ذلك، يمكنك جعل واجهة المستخدم تسجّل رد اتصال مع العامل، مثل Messenger أو واجهة رد اتصال AIDL. لا يؤدي تمرير كائن binder كعملية استدعاء إلى تغيير أهمية أي من العمليتين.

يجب إلغاء تسجيل دالة الاستدعاء قبل إلغاء الربط، لكي لا يستدعي العامل مطلقًا عملية واجهة المستخدم بعد أن يجمّدها النظام. يرتبط المثال التالي بالعامل فقط عندما تكون واجهة المستخدم مرئية:

Kotlin

class MainActivity : Activity() {
    private var downloadService: IBinder? = null

    private val downloadConnection = object : ServiceConnection {
        override fun onServiceConnected(
                name: ComponentName,
                service: IBinder
        ) {
            downloadService = service
            // Register a Messenger or AIDL callback for progress updates.
        }

        override fun onServiceDisconnected(name: ComponentName) {
            // The binding stays active and reconnects if the worker restarts.
            downloadService = null
        }
    }

    override fun onStart() {
        super.onStart()
        val intent = Intent(this, AssetDownloadService::class.java)
        bindService(intent, downloadConnection, Context.BIND_AUTO_CREATE)
    }

    override fun onStop() {
        super.onStop()
        if (downloadService != null) {
            // Unregister the progress callback here, before unbinding.
            downloadService = null
        }
        // Unbind the ServiceConnection even if onServiceConnected hasn't run
        // yet.
        unbindService(downloadConnection)
    }
}

Java

public final class MainActivity extends Activity {
    private IBinder downloadService;

    private final ServiceConnection downloadConnection =
            new ServiceConnection() {
                @Override
                public void onServiceConnected(
                        ComponentName name, IBinder service) {
                    downloadService = service;
                    // Register a Messenger or AIDL callback for progress
                    // updates.
                }

                @Override
                public void onServiceDisconnected(ComponentName name) {
                    // The binding stays active and reconnects if the worker
                    // restarts.
                    downloadService = null;
                }
            };

    @Override
    protected void onStart() {
        super.onStart();
        Intent intent = new Intent(this, AssetDownloadService.class);
        bindService(intent, downloadConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (downloadService != null) {
            // Unregister the progress callback here, before unbinding.
            downloadService = null;
        }
        // Unbind the ServiceConnection even if onServiceConnected hasn't run
        // yet.
        unbindService(downloadConnection);
    }
}

لمزيد من المعلومات حول كيفية تأثير علامات الربط في oom_score_adj على مستوى العمليات، يُرجى الاطّلاع على عمليات ربط الخدمة وحالات العمليات.

تنسيق الحالة المشتركة على القرص بأمان

بما أنّ العملية الرئيسية و":bg" يتم تشغيلهما في مساحات عناوين منفصلة مع أوقات تشغيل وواصفات ملفات منفصلة:

  • ‫SQLite وRoom: استخدِم ميزة "تسجيل الكتابة مسبقًا" (وضع دفتر اليومية التلقائي في Room على معظم الأجهزة) حتى تتمكّن العمليتان من قراءة قاعدة البيانات والكتابة فيها في الوقت نفسه. عدم الاتصال بـ enableMultiInstanceInvalidation من ":bg" تستضيف Room خدمة الإبطال في العملية الرئيسية، لذا يرتبط العامل بالعملية الرئيسية ويحافظ على ذاكرة واجهة المستخدم عند الأهمية المرتفعة للعامل. بدلاً من ذلك، اطلب من العامل الإبلاغ عن التغييرات من خلال عملية الاستدعاء التي تسجّلها واجهة المستخدم، واطلب من واجهة المستخدم إعادة الاستعلام عن قاعدة البيانات عند بدء تشغيلها.
  • الإعدادات المفضّلة: لا تشارك SharedPreferences في جميع العمليات. استخدِم DataStore في الرمز البرمجي المتعدّد العمليات أو مرِّر الحالة بشكل صريح من خلال الاتصال بين العمليات (IPC).
  • ملفات مواد العرض: يمكنك كتابة حزم مواد العرض التي تم تنزيلها في ملف مرحلي في :bg، ثم استدعاء fdatasync ثم posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED) في :bg بعد الكتابة (كما هو موضّح في إرجاع صفحات الذاكرة إلى نظام التشغيل)، وإعادة تسمية الملف تلقائيًا في مكانه. اسمح لواجهة المستخدم بالحصول على النتيجة عند بدء تشغيلها في المرة التالية وربطها بالعامل، بدلاً من استدعاء عملية واجهة مستخدم قد تكون مجمّدة.

مراقبة التأثير في واجهة adb shell

يمكنك التأكّد من أنّ تقسيم العمليات في المقدّمة والخلفية يعزل استخدام الذاكرة المرتفع من خلال فحص حالات عمليات "إدارة النشاط" و dumpsys meminfo بعد الانتقال إلى الشاشة الرئيسية. الناتج النموذجي في هذا القسم مأخوذ من نموذج تطبيق MemoryLab، الذي يشغّل عملية التخصيص نفسها بسعة 64 ميغابايت في عملية واجهة المستخدم في كلا الإعدادَين ويستضيف الخدمة التي تعمل في المقدّمة والمقسّمة في عملية :remote.

1. مقارنة حالات العمليات ونتائج نفاد الذاكرة (OOM)

ابدأ عبء العمل في الخلفية، واضغط على زر الشاشة الرئيسية لإيقاف Activity، ثم أرسِل طلبًا إلى "مدير النشاط":

adb shell dumpsys activity processes com.example.app | grep -E "Proc #|oom:|state:"

في البنية ذات العملية الواحدة، تظل العملية الرئيسية مرتفعة على الرغم من إيقاف Activity. لمدة 15 ثانية تقريبًا بعد مغادرة المستخدم، يحتفظ Activity Manager بعملية تشغّل خدمة تعمل في المقدّمة على oom_score_adj = 50 (fg-service-act) ثم يخفضها إلى 200 (fg-service). تم التقاط الناتج التالي بعد 20 ثانية من الضغط على زر الشاشة الرئيسية:

    Proc #1: prcp     F/S/FGS  ---NFUATI  t: 0 13794:com.android.memorylab/u0a284 (fg-service)
        oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200
        state: cur=FGS  set=FGS   lastRss=0.00 lastCachedRss=0.00

في بنية العمليات المنفصلة، تبقى عملية العامل فقط في oom_score_adj = 200، بينما تنخفض عملية واجهة المستخدم الرئيسية إلى PREV (oom_score_adj = 700) ثم إلى مجموعة البيانات المخزّنة مؤقتًا (oom_score_adj >= 900):

    Proc #3: prcp     F/S/FGS  ---NFUATI  t: 0 13544:com.android.memorylab:remote/u0a284 (fg-service)
        oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200
        state: cur=FGS  set=FGS   lastRss=0.00 lastCachedRss=0.00
    Proc #1: prev     b/ /LAST -------TI  t: 0 13312:com.android.memorylab/u0a284 (previous)
        oom: max=1001 curRaw=700 setRaw=700 cur=700 set=700
        state: cur=LAST set=LAST  lastRss=0.00 lastCachedRss=0.00

بعد انتهاء مهلة التطبيق السابق، تنتقل العملية الرئيسية إلى الحزمة المخزّنة مؤقتًا (cch وoom_score_adj = 900 وprevious-expired) وتفقد جميع إمكانات العملية (---------)، بما في ذلك وقت وحدة المعالجة المركزية الضمني (I). وعند هذه النقطة، يجمّدها النظام ويمكنه ضغطها أو استعادتها في أي وقت بدون مقاطعة عملية العامل.

2. مقارنة dumpsys meminfo في العمليات المختلفة

قارِن ملخّص dumpsys meminfo للعامل المميّز بعملية واجهة المستخدم:

adb shell dumpsys meminfo com.example.app:bg
adb shell dumpsys meminfo com.example.app

نموذج ناتج App Summary لعملية عامل MemoryLab في oom_score_adj = 200:

 App Summary
                       Pss(KB)                        Rss(KB)
                        ------                         ------
           Java Heap:      380                          13840
         Native Heap:     1548                           2832
                Code:      132                          55816
               Stack:      200                            208
            Graphics:        0                              0
       Private Other:      496
              System:     1926
             Unknown:                                    4168

           TOTAL PSS:     4682            TOTAL RSS:    76864       TOTAL SWAP PSS:      441

نموذج ناتج App Summary لعملية واجهة مستخدم MemoryLab في oom_score_adj = 700، والذي يحتوي على تخصيص أصلي بسعة 64 ميجابايت في Private Other:

 App Summary
                       Pss(KB)                        Rss(KB)
                        ------                         ------
           Java Heap:     1316                          19816
         Native Heap:    30408                          31596
                Code:     3148                         115796
               Stack:      656                            664
            Graphics:        0                              0
       Private Other:    67044
              System:    13687
             Unknown:                                   71092

           TOTAL PSS:   116259            TOTAL RSS:   238964       TOTAL SWAP PSS:      259

تحتاج عملية العامل إلى أقل من 5 ميغابايت من PSS، لأنّها تشارك معظم صفحات الرموز ووقت التشغيل مع Zygote. في التطبيقات التي تعرض الرسومات باستخدام Vulkan أو OpenGL ES، يتضمّن صف Graphics في عملية واجهة المستخدم أيضًا ذاكرة وحدة معالجة الرسومات. تم التقاط هذه العيّنة على جهاز Cuttlefish افتراضي مع عرض البرامج، لذا فإنّ Graphics هو 0 في كلتا العمليتين.

3- فحص حسابات cgroup لكل عملية

لكل عملية ضمن معرّف UID دليل cgroup v2 خاص بها داخل مجموعة cgroup رئيسية لتطبيقك. يسرد الأمر التالي oom_score_adj وmemory.current وmemory.high على مستوى التطبيق لكل عملية من عمليات تطبيقك:

adb shell '
  for PID in $(pidof com.example.app com.example.app:bg); do
    CG=/sys/fs/cgroup$(sed -n "s/^0:://p" /proc/$PID/cgroup)
    echo "pid=$PID ($(tr -d "\0" < /proc/$PID/cmdline))" \
      "adj=$(cat /proc/$PID/oom_score_adj)" \
      "memory.current=$(cat $CG/memory.current)" \
      "app.memory.high=$(cat ${CG%/*}/memory.high)"
  done
'

مثال على الناتج في MemoryLab بعد الضغط على "الصفحة الرئيسية":

pid=15301 (com.android.memorylab) adj=700 memory.current=103260160 app.memory.high=966787072
pid=16518 (com.android.memorylab:remote) adj=200 memory.current=3608576 app.memory.high=966787072

يتم تحميل عملية العامل أقل من 4 ميغابايت، بينما يتم تحميل عملية واجهة المستخدم حوالي 98 ميغابايت. لكل عملية memory.current خاص بها، ولكن يضبط "محدِّد الذاكرة" الحدّ الأقصى على مجموعة التحكّم الرئيسية التي تحتوي على جميع عمليات تطبيقك. يعرض كلا السطرين app.memory.high نفسه الذي يبلغ 966,787,072 بايت، وهو الحدّ الأقصى غير المرئي الذي يبلغ 922 ميغابايت في الناتج التالي.

للاطّلاع على الحدود المرئية وغير المرئية التي يطبّقها "محدِّد الذاكرة" على الجهاز، نفِّذ الأمر التالي:

adb shell am memory-limiter status

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

Memory limiter
  enabled                  limits=true              killing=true             ignored=none
  visible:                 memHigh=1843MB           memMax=2048MB            swapMax=1024MB
  not-visible:             memHigh=922MB            memMax=1024MB            swapMax=1024MB

ملاحظة التأثير في Perfetto

تتيح لك أداة Perfetto عرض المخطط الزمني لـ oom_score_adj وRSS في كلتا العمليتين أثناء انتقال المستخدم من واجهة المستخدم في المقدّمة إلى الشاشة الرئيسية مع استمرار العمل في الخلفية.

1. تسجيل تتبُّع

سجِّل تتبُّعًا لمدة 15 ثانية يرصد عدّادات ذاكرة العمليات وذاكرة وحدة معالجة الرسومات وتغييرات oom_score_adj أثناء بدء مهمة في الخلفية في تطبيقك والانتقال إلى الشاشة الرئيسية:

adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/process-splitting.perfetto-trace <<'EOF'
buffers: {
  size_kb: 32768
  fill_policy: DISCARD
}
data_sources: {
  config {
    name: "linux.process_stats"
    process_stats_config {
      scan_all_processes_on_start: true
      proc_stats_poll_ms: 250
    }
  }
}
data_sources: {
  config {
    name: "android.gpu.memory"
  }
}
data_sources: {
  config {
    name: "linux.ftrace"
    ftrace_config {
      ftrace_events: "oom/oom_score_adj_update"
      ftrace_events: "gpu_mem/gpu_mem_total"
    }
  }
}
duration_ms: 15000
EOF
adb pull /data/misc/perfetto-traces/process-splitting.perfetto-trace

2. فحص مسارات العمليات في واجهة مستخدم Perfetto

افتح التتبُّع في واجهة مستخدم Perfetto ووسِّع مجموعات العمليات لـ com.example.app وcom.example.app:bg:

  1. ثبِّت مسارَي oom_score_adj وmem.rss.anon لكلتا العمليتَين ليظهرا جنبًا إلى جنب في أعلى المخطط الزمني.
  2. ابحث عن الطابع الزمني الذي يضغط فيه المستخدم على زر الشاشة الرئيسية:
    • في عملية العامل، يظل oom_score_adj عند 200 ويظل mem.rss.anon ثابتًا وصغيرًا.
    • في عملية واجهة المستخدم، يرتفع oom_score_adj من 0 إلى 200، ويظل هناك لمدة ثانية تقريبًا أثناء توقف Activity، ثم يرتفع إلى 700 (PREV). بعد انتهاء مهلة التطبيق السابق، ينتقل إلى النطاق المخزّن مؤقتًا، حيث يمكن للنظام ضغط ذاكرته واستعادتها بدون التأثير على العامل.
مسارات Perfetto لعملية واجهة المستخدم وعملية العامل ترتفع قيمة oom_score_adj لعملية واجهة المستخدم من 0 إلى 200 ثم إلى 700 بعد الضغط على زر الشاشة الرئيسية، بينما تظل قيمة oom_score_adj لعملية العامل عند 200 مع حوالي 19 ميغابايت من حجم مجموعة العمل (RSS) المجهول.
الشكل 2. يتم تقسيم العمليات بعد أن يضغط المستخدم على زر الشاشة الرئيسية.

في الشكل 2، تظل عملية واجهة المستخدم لفترة وجيزة عند 200 بينما يتوقف Activity ثم يرتفع إلى 700، بينما يظل العامل عند 200 مع حوالي 19 ميغابايت من RSS المجهول. للمقارنة، يعرض الشكل 3 تتبُّعًا لمدة 35 ثانية لإعدادات الموقع المشترك، حيث تظل العملية الواحدة عند 50 ثم 200 مع كل ذاكرة واجهة المستخدم الخاصة بها.

مسارات Perfetto لعملية واحدة مشتركة في الموقع. يرتفع oom_score_adj من 0 إلى 50 بعد الانتقال إلى الشاشة الرئيسية وإلى 200 بعد حوالي 15 ثانية، بينما يظل حجم مجموعة العمليات المجهولة الهوية مرتفعًا.
الشكل 3 عملية مشتركة بعد أن يضغط المستخدم على زر الشاشة الرئيسية

لاستكشاف طرق العرض هذه، افتح التتبُّع المقسّم والتتبُّع في الموقع المشترك في واجهة مستخدم Perfetto.

3- الاستعلام عن استهلاك الذاكرة المرتفع باستخدام PerfettoSQL

نفِّذ طلب PerfettoSQL التالي في علامة التبويب طلب البحث (SQL) لقياس متوسط حجم مجموعة العمل (RSS) المجهولة والمستندة إلى الملفات التي تحتفظ بها كل عملية في تطبيقك عند كل مستوى oom_score_adj:

INCLUDE PERFETTO MODULE android.memory.process;

SELECT
  process_name,
  score AS oom_score_adj,
  ROUND(SUM(dur) / 1e9, 2) AS duration_sec,
  ROUND(SUM(anon_rss * dur) / SUM(dur) / 1048576.0, 1) AS avg_anon_mb,
  ROUND(SUM(file_rss * dur) / SUM(dur) / 1048576.0, 1) AS avg_file_mb
FROM memory_oom_score_with_rss_and_swap_per_process
WHERE process_name GLOB 'com.example.app*'
GROUP BY process_name, score
ORDER BY process_name, score;

مثال على الناتج للتتبُّع المقسَّم في الشكل 2:

process_name                  oom_score_adj  duration_sec  avg_anon_mb  avg_file_mb
com.android.memorylab                     0          4.11         93.3        110.4
com.android.memorylab                   200          1.06        126.8        110.5
com.android.memorylab                   700          9.72        115.1        110.5
com.android.memorylab:remote              0          0.01         NULL         NULL
com.android.memorylab:remote            200         12.87         18.9         53.7

مثال على الناتج للتتبُّع في الموقع نفسه في الشكل 3:

process_name           oom_score_adj  duration_sec  avg_anon_mb  avg_file_mb
com.android.memorylab              0          4.10         96.0        110.4
com.android.memorylab             50         16.04        117.6        110.5
com.android.memorylab            200         14.68        116.5        110.6

في الإعدادات المقسَّمة، تبقى حوالي 19 ميغابايت فقط من الذاكرة المجهولة في oom_score_adj = 200 بعد الثانية الأولى. في الإعدادات المشتركة، يظل حوالي 117 ميغابايت في 50 ثم 200 طوال مدة تشغيل الخدمة التي تعمل في المقدّمة.


← إرجاع صفحات الذاكرة إلى نظام التشغيل | ↑ للأعلى