Keep background work alive with a lean process

بسیاری از برنامه‌ها وقتی واسط کاربر آن‌ها دیگر قابل‌مشاهده نیست، کارهای قانونی انجام می‌دهند، مانند بارگیری بسته‌های دارایی بزرگ، همگام‌سازی پایگاه‌های داده محلی، جاری‌سازی صوتی، ضبط تمرینات ورزشی، یا حفظ جلسه دستگاه متصل‌شده. وقتی این کار پس‌زمینه در همان فرایند سیستم‌عامل واسط کاربر پیش‌زمینه و موتور گرافیکی اجرا می‌شود، کل فرایند در سطح اهمیت بالاتری باقی می‌ماند (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 متوقف شود.
Comparison of single-process co-location keeping UI memory pinned
       at oom_score_adj 200 versus split-process isolation keeping only the
       worker at oom_score_adj 200 while the UI process drops to cached.
شکل ۱. مقایسه هم‌مکانی تک‌پردازشی با جداسازی پردازش تقسیم‌شده درطول کار پس‌زمینه‌ای.

وقتی فرایند واسط کاربر سنگینی در oom_score_adj = 200 باقی می‌ماند، چندین دسته بزرگ از حافظه مقیم باقی می‌ماند:

  • Activity متوقف‌شده و سلسله‌مراتب نما: تا زمانی که Activity به‌طور صریح تمام نشده باشد، Window، درخت نما، ساختارهای چیدمان، و وضعیت نمای آن در پشته Java قابل‌دسترسی باقی می‌مانند، درحالی‌که کاربر در برنامه دیگری است.
  • بافت‌های گرافیکی بومی و تخصیص‌های GPU: HardwareRenderer استاندارد بافت GPU و حافظه‌های پنهان خود را حفظ می‌کند، درحالی‌که Activity متوقف‌شده در فرایندی زندگی می‌کند که حافظه پنهان ندارد. برنامه‌ها و موتورهای بازی که بافت‌های Vulkan (VkDevice، VkSwapchainKHR، استخرهای توصیف‌گر، حافظه‌های پنهان خط لوله) یا OpenGL ES (EGLContext، بافت‌ها، میان‌گیرهای رأس) خودشان را ایجاد می‌کنند، ساختارهای درایور، برنامه‌های سایه‌زن، و میان‌گیرهای گرافیکی خود را نیز حفظ می‌کنند، مگراینکه موتور به‌طور صریح دستگاه گرافیکی را هنگام توقف از کار بیندازد.
  • تکه‌تکه‌سازی توده و سربار زمان اجرا: حتی وقتی برنامه‌ای حافظه‌های نهان را در onTrimMemory آزاد می‌کند، صفحه‌های تخصیص‌دهنده بومی نیمه‌پر، صفحه‌های کپی-در-نوشتن Zygote کثیف، کتابخانه‌های مشترک بارشده (.so)، و فراداده‌های زمان اجرا در حافظه خصوصی فرایند باقی می‌مانند. چون فرایند بالاتر از آستانه ذخیره‌شده در حافظه نهان است (oom_score_adj < 900)، پلاتفرم آن را متوقف نمی‌کند یا فشرده‌سازی برنامه ذخیره‌شده در حافظه نهان را روی آن اجرا نمی‌کند. برای بررسی دقیق‌تر این تأثیر با خدمات محدودکننده، هم‌مکان کردن خدمات محدودکننده با واسط کاربر با حافظه فشرده را ببینید.

چگونه فرایند پس‌زمینه‌ای سبک از برنامه‌تان محافظت می‌کند

نگه داشتن حافظه به‌اندازه پیش‌زمینه در وضعیت پس‌زمینه، کار پس‌زمینه خودتان را به‌خطر می‌اندازد:

  • محدودیت‌های حافظه پس‌زمینه سخت‌گیرانه‌تر از محدودیت‌های پیش‌زمینه است. در Android 17 و نسخه‌های بالاتر، محدودکننده حافظه حد حافظه غیرقابل‌مشاهده کوچک‌تری را برای برنامه‌تان اعمال می‌کند، درحالی‌که هیچ‌کدام از فرایندهای آن واسط کاربر قابل‌مشاهده‌ای را نشان نمی‌دهد، ازجمله زمانی که سرویس پیش‌زمینه‌ای اجرا می‌شود. این حد همه فرایندهای برنامه‌تان را باهم پوشش می‌دهد. اگر فرایندی که سرویس پیش‌زمینه‌ای شما را اجرا می‌کند همچنان حافظه نماپردازنده و میانای کاربر خود را حفظ کند، آن حافظه در حد کوچک‌تر محاسبه می‌شود. وقتی برنامه‌تان از حد مجاز فراتر می‌رود، هسته آن را محدود می‌کند و درعین‌حال صفحه‌های آن را بازیابی می‌کند، که باعث کند شدن بارگیری، همگام‌سازی، یا بازپخش درحال اجرا می‌شود. اگر برنامه شما پس‌از اتمام فضای مبادله همچنان به تخصیص دادن ادامه دهد، سیستم آن را متوقف می‌کند. برای بررسی اینکه آیا برنامه‌تان تحت‌تأثیر قرار گرفته است یا نه، محدودیت‌های حافظه برنامه را ببینید.
  • درصورت وجود فشار روی حافظه، فرایندهای بزرگ‌تر زودتر خاتمه می‌یابند. وقتی 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) فقط با پشته و کد خود کارگر اجرا می‌شود و هیچ نمای واسط کاربر یا بافت GPU ندارد.

اعلام بودجه‌های فرایند و حافظه

با تنظیم کردن android:process=":bg" در AndroidManifest.xml، پس‌زمینه Service را به زیرفرایند خصوصی اختصاص دهید. دو نقطه ابتدایی (:) فرایندی خصوصی ایجاد می‌کند که به شناسه کاربری برنامه‌تان محدود می‌شود.

در Android 17 QPR2 (سطح API‏ ۳۷.۲) و بالاتر، تقسیم فرایند را با بودجه حافظه برنامه برای هر فرایند در داخل بلوک <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" در همه سطوح API کار می‌کند.

راه‌اندازی Gate در Application.onCreate

Android کلاس Application شما را نمونه‌سازی می‌کند و Application.onCreate را در هر فرایندی که برای بسته‌تان ایجاد شده است فراخوانی می‌کند. یک اشتباه رایج هنگام استفاده از android:process=":bg" این است که کیت‌های ابزار سنگین واسط کاربر، استخرهای بارگیری تصویر، کیت‌های توسعه نرم‌افزار Analytics، یا مقداردهی اولیه موتور بومی (System.loadLibrary) را در یک روش Application.onCreate بدون محافظت رها کنید. انجام این کار باعث می‌شود فرایند :bg با همان ردپای خط مبنایی که سعی دارید از آن اجتناب کنید، متورم شود.

Application.getProcessName را که در Android نسخه ۹ (سطح API‏ ۲۸) و بالاتر دردسترس است در onCreate بررسی کنید و فقط وابستگی‌های موردنیاز فرایند فعلی را مقداردهی اولیه کنید:

کاتلین

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"
    }
}

جاوا

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 به‌عنوان فراخوان برگشتی، اهمیت هیچ‌کدام از فرایندها را تغییر نمی‌دهد.

پیش‌از لغو اتصال، فراخوانی برگشتی را لغو ثبت کنید تا پس‌از اینکه سیستم فرایند واسط کاربر را متوقف کرد، کارگر هرگز آن را فراخوانی نکند. مثال زیر فقط زمانی به worker متصل می‌شود که واسط کاربر نمایان باشد:

کاتلین

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)
    }
}

جاوا

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

با بررسی وضعیت‌های فرایند «مدیر فعالیت» و dumpsys meminfo پس‌از رفتن به صفحه اصلی، می‌توانید درستی‌سنجی کنید که تقسیم فرایندهای پیش‌زمینه و پس‌زمینه استفاده از حافظه افزایش‌یافته را جداسازی می‌کند. خروجی نمونه در این بخش از برنامه نمونه MemoryLab می‌آید که همان تخصیص بومی ۶۴ مگابایتی را در فرایند واسط کاربر خود در هر دو پیکربندی اجرا می‌کند و سرویس پیش‌زمینه تقسیم‌شده خود را در فرایند :remote میزبانی می‌کند.

۱. مقایسه وضعیت‌های فرایند و امتیازهای کمبود حافظه

بار کاری پس‌زمینه را شروع کنید، دکمه «صفحه اصلی» را فشار دهید تا Activity متوقف شود، و Activity Manager را پُرسمان کنید:

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

در معماری تک‌فرایندی، فرایند اصلی همچنان ارتقایافته باقی می‌ماند، حتی اگر Activity آن متوقف شده باشد. تا حدود ۱۵ ثانیه پس‌از خروج کاربر، Activity Manager فرایندی را که سرویس پیش‌زمینه‌ای را اجرا می‌کند در oom_score_adj = 50 (fg-service-act) نگه می‌دارد و سپس آن را به 200 (fg-service) کاهش می‌دهد. خروجی زیر ۲۰ ثانیه پس‌از فشار دادن «صفحه اصلی» ضبط شده است:

    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) را ازدست می‌دهد. در آن نقطه، سیستم آن را متوقف می‌کند و می‌تواند در هر زمانی بدون قطع کردن فرایند کارگر، آن را فشرده یا بازیابی کند.

۲. مقایسه 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، که تخصیص بومی ۶۴ مگابایتی را در 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

فرایند کارگر به کمتر از ۵ مگابایت PSS نیاز دارد، زیرا بیشتر کدها و صفحه‌های زمان اجرای خود را با Zygote هم‌رسانی می‌کند. در برنامه‌ای که با Vulkan یا OpenGL ES پردازش می‌کند، ردیف Graphics فرایند میانای کاربری شامل حافظه GPU نیز می‌شود. این نمونه در دستگاه مجازی Cuttlefish با پرداز نرم‌افزاری ضبط شده است، بنابراین Graphics در هر دو فرایند 0 است.

۳. بررسی حسابداری 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

فرایند کارگر کمتر از ۴ مگابایت و فرایند میانای کاربر حدود ۹۸ مگابایت شارژ می‌شود. هر فرایند memory.current خودش را دارد، اما «محدودکننده حافظه» محدودیت خود را روی cgroup والد که شامل همه فرایندهای برنامه شما است تنظیم می‌کند. هر دو خط همان app.memory.high از ۹۶۶٬۷۸۷٬۰۷۲ بایت را نشان می‌دهند، که حد نامرئی ۹۲۲ مگابایت در خروجی زیر است.

برای دیدن محدودیت‌های مرئی و نامرئی که «محدودکننده حافظه» بر دستگاه اعمال می‌کند، فرمان زیر را اجرا کنید:

adb shell am memory-limiter status

نمونه خروجی از دستگاهی با ۴ گیگابایت حافظه دسترسی تصادفی، که در آن حد نامرئی نصف حد مرئی است:

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 را در هر دو فرایند تجسم کنید، درحالی‌که کاربر از میانای کاربری پیش‌زمینه به «صفحه اصلی» می‌رود و کار پس‌زمینه ادامه دارد.

۱. ضبط کردن ردیابی

ردیابی ۱۵ ثانیه‌ای ضبط کنید که شمارنده‌های حافظه پردازش، حافظه GPU، و 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

۲. بازرسی قطعه‌های فرایند در واسط کاربر 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 برای فرایند واسط کاربر و فرایند کارگر ردیابی می‌کند. The UI
       process oom_score_adj rises from 0 to 200 and then 700 after Home, while
       the worker process stays at 200 with about 19 MB of anonymous RSS.
شکل ۲. وقتی کاربر دکمه «صفحه اصلی» را فشار می‌دهد، پردازش‌ها تقسیم می‌شود.

در شکل ۲، فرایند میانای کاربر برای مدت کوتاهی در 200 می‌ماند درحالی‌که Activity آن متوقف می‌شود و سپس به 700 افزایش می‌یابد، درحالی‌که فرایند کارگر با حدود ۱۹ مگابایت RSS ناشناس در 200 می‌ماند. برای مقایسه، شکل ۳ رد ۳۵ ثانیه‌ای از پیکربندی هم‌مکان را نشان می‌دهد که در آن فرایند واحد با تمام حافظه واسط کاربر خود در 50 و سپس 200 باقی می‌ماند.

Perfetto tracks for a single co-located process. oom_score_adj
       rises from 0 to 50 after Home and to 200 about 15 seconds later, while
       anonymous RSS stays high.
شکل ۳. فرایند هم‌مکان پس‌از اینکه کاربر دکمه «صفحه اصلی» را فشار می‌دهد.

برای کاوش این نماها، ردگیری دونیمه و ردگیری هم‌مکان را در میانای کاربری Perfetto باز کنید.

۳. پرسمان ردپای حافظه افزایش‌یافته با 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;

نمونه خروجی برای ردیابی تقسیم‌شده در شکل ۲:

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

خروجی نمونه برای ردیابی هم‌مکان در شکل ۳:

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

در پیکربندی تقسیم، فقط حدود ۱۹ مگابایت حافظه ناشناس در oom_score_adj = 200 پس‌از ثانیه اول باقی می‌ماند. در پیکربندی هم‌مکان، حدود ۱۱۷ مگابایت در 50 و سپس 200 تا زمانی که سرویس پیش‌زمینه‌ای اجرا می‌شود باقی می‌ماند.


← برگرداندن صفحه‌های حافظه به سیستم‌عامل | ↑ بالا