بسیاری از برنامهها وقتی واسط کاربر آنها دیگر قابلمشاهده نیست، کارهای قانونی انجام میدهند، مانند
بارگیری بستههای دارایی بزرگ، همگامسازی پایگاههای داده محلی، جاریسازی صوتی،
ضبط تمرینات ورزشی، یا حفظ جلسه دستگاه متصلشده. وقتی
این کار پسزمینه در همان فرایند سیستمعامل واسط کاربر پیشزمینه و
موتور گرافیکی اجرا میشود، کل فرایند در سطح اهمیت بالاتری باقی میماند
(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 باقی میماند، چندین دسته بزرگ
از حافظه مقیم باقی میماند:
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 را ازهم باز کنید:
- قطعههای
oom_score_adjوmem.rss.anonرا برای هر دو فرایند سنجاق کنید تا در بالای خط زمان کنار یکدیگر نشان داده شوند. - مُهر زمان را در جایی که کاربر دکمه «صفحه اصلی» را فشار میدهد پیدا کنید:
- در فرایند کارگر،
oom_score_adjدر200میماند وmem.rss.anonثابت و کوچک میماند. - در فرایند میانای کاربر،
oom_score_adjاز0به200افزایش مییابد، حدود یک ثانیه در آنجا میماند درحالیکهActivityمتوقف میشود، و سپس به700(PREV) افزایش مییابد. پساز اتمام مهلت برنامه قبلی، به محدوده ذخیرهشده در حافظه نهان منتقل میشود، جایی که سیستم میتواند حافظه آن را بدون تأثیر بر کارگر فشرده و بازیابی کند.
- در فرایند کارگر،
در شکل ۲، فرایند میانای کاربر برای مدت کوتاهی در 200 میماند درحالیکه Activity آن متوقف میشود و سپس به 700 افزایش مییابد، درحالیکه فرایند کارگر با حدود ۱۹ مگابایت RSS ناشناس در 200 میماند. برای مقایسه، شکل ۳ رد ۳۵ ثانیهای از پیکربندی هممکان را نشان میدهد که در آن فرایند واحد با تمام حافظه واسط کاربر خود در 50 و سپس 200 باقی میماند.
برای کاوش این نماها، ردگیری دونیمه و ردگیری هممکان را در میانای کاربری 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 تا زمانی که سرویس پیشزمینهای
اجرا میشود باقی میماند.
← برگرداندن صفحههای حافظه به سیستمعامل | ↑ بالا