แอปจำนวนมากทำงานที่ถูกต้องเมื่อ UI ไม่ปรากฏอีกต่อไป เช่น การดาวน์โหลดแพ็กเนื้อหาขนาดใหญ่ การซิงค์ฐานข้อมูลในเครื่อง การสตรีมเสียง การบันทึกการออกกำลังกาย หรือการคงเซสชันของอุปกรณ์ที่เชื่อมต่อไว้ เมื่อการทำงานเบื้องหลังนี้ทำงานภายในกระบวนการของระบบปฏิบัติการเดียวกันกับ UI เบื้องหน้าและเครื่องมือกราฟิก กระบวนการทั้งหมดจะยังคงอยู่ในระดับความสำคัญที่สูงขึ้น (oom_score_adj ระหว่าง 0 และ 250) พร้อมกับหน่วยความจำทั้งหมดที่ UI จัดสรรไว้
หน้านี้อธิบายว่าเหตุใดการดำเนินการดังกล่าวจึงส่งผลเสียต่อแอปของคุณเองรวมถึงส่วนอื่นๆ ของอุปกรณ์ วิธีเปลี่ยนงานที่ทำงานอยู่เบื้องหลังให้เป็นกระบวนการที่มีน้ำหนักเบาโดยเฉพาะโดยใช้ android:process และวิธียืนยันการประหยัดหน่วยความจำโดยใช้ adb shell และ Perfetto
เหตุใดการทำงานแบบกระบวนการเดียวจึงเก็บหน่วยความจำ UI ไว้
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) ในที่เก็บข้อมูลที่แคชไว้ แพลตฟอร์มจะหยุดกระบวนการทำงานชั่วคราวด้วย Freezer ของ cgroup v2, บีบอัดหน่วยความจำด้วยCachedAppOptimizerและให้lmkdสิ้นสุดกระบวนการทำงานก่อนภายใต้แรงกดดันด้านหน่วยความจำ - อย่างไรก็ตาม หากกระบวนการเดียวกันนี้เรียกใช้บริการที่ทำงานอยู่เบื้องหน้าด้วย (เช่น
dataSync,mediaPlayback,connectedDeviceหรือlocationประเภทที่oom_score_adj = 200) หรือโฮสต์บริการที่ระบบหรือไคลเอ็นต์สำคัญอื่นๆ ผูกไว้ กระบวนการทั้งหมดจะยังคงอยู่ที่ลำดับความสำคัญที่ยกระดับนั้นแม้ว่าจะหยุดActivityแล้วก็ตาม
เมื่อกระบวนการ UI ที่ใช้ทรัพยากรมากยังคงอยู่ที่ oom_score_adj = 200 หน่วยความจำหลายหมวดหมู่ขนาดใหญ่จะยังคงอยู่
Activityและลำดับชั้นการแสดงผลที่หยุดทำงาน: เว้นแต่Activityจะเสร็จสิ้นอย่างชัดเจนWindow, แผนผังการแสดงผล, โครงสร้างเลย์เอาต์ และสถานะการแสดงผลจะยังคงเข้าถึงได้ในฮีป Java ขณะที่ผู้ใช้อยู่ในแอปอื่น- บริบทกราฟิกแบบเนทีฟและการจัดสรร GPU:
HardwareRendererมาตรฐานจะเก็บ GPU context และแคชไว้ ขณะที่Activityที่หยุดทำงานจะอยู่ในกระบวนการที่ไม่ได้แคชไว้ แอปและเกมเอนจินที่สร้างบริบท Vulkan (VkDevice,VkSwapchainKHR, พูลข้อบ่งชี้, แคชไปป์ไลน์) หรือ OpenGL ES (EGLContext, พื้นผิว, บัฟเฟอร์เวอร์เท็กซ์) ของตนเองจะยังคงรักษาโครงสร้างไดรเวอร์ โปรแกรมเฉดสี และบัฟเฟอร์กราฟิกไว้ด้วย เว้นแต่เอนจินจะทำลายอุปกรณ์กราฟิกอย่างชัดเจนเมื่อหยุดทำงาน - การกระจาย Fragment ของฮีปและค่าใช้จ่ายรันไทม์: แม้ว่าแอปจะปล่อยแคชใน
onTrimMemoryแต่หน้าตัวจัดสรรเนทีฟที่ป้อนข้อมูลบางส่วน หน้าคัดลอกเมื่อเขียน Zygote ที่สกปรก ไลบรารีที่ใช้ร่วมกันที่โหลด (.so) และข้อมูลเมตารันไทม์จะยังคงอยู่ในหน่วยความจำส่วนตัวของกระบวนการ เนื่องจากกระบวนการนี้อยู่เหนือเกณฑ์ที่แคชไว้ (oom_score_adj < 900) แพลตฟอร์มจึงไม่หยุดกระบวนการนี้หรือเรียกใช้การบีบอัดแอปที่แคชไว้ ดูข้อมูลเพิ่มเติมเกี่ยวกับผลกระทบนี้กับบริการที่มีผลผูกพันได้ที่การจัดวางบริการที่มีผลผูกพันร่วมกับ UI ที่ใช้หน่วยความจำมาก
กระบวนการเบื้องหลังที่มีประสิทธิภาพช่วยปกป้องแอปของคุณได้อย่างไร
การเก็บหน่วยความจำขนาดเท่ากับที่ใช้ในเบื้องหน้าไว้ในสถานะเบื้องหลังจะทำให้งานที่ทำงานอยู่เบื้องหลังมีความเสี่ยงดังนี้
- ขีดจำกัดหน่วยความจำในเบื้องหลังจะเข้มงวดกว่าขีดจำกัดในเบื้องหน้า ใน Android 17 ขึ้นไป Memory Limiter จะใช้ขีดจำกัดหน่วยความจำไม่แสดงผลที่น้อยลงกับแอปของคุณขณะที่ไม่มีกระบวนการใดแสดง UI ที่มองเห็นได้ รวมถึงขณะที่บริการที่ทำงานอยู่เบื้องหน้าทำงานอยู่ ขีดจำกัดนี้ครอบคลุมกระบวนการทั้งหมดของแอปรวมกัน หากกระบวนการที่เรียกใช้บริการที่ทำงานอยู่เบื้องหน้ายังคงเก็บหน่วยความจำ UI และหน่วยความจำกราฟิกไว้ หน่วยความจำนั้นจะนับรวมในขีดจำกัดที่น้อยลง เมื่อแอปใช้หน่วยความจำเกินขีดจำกัด เคอร์เนลจะจำกัดการทำงานของแอปขณะเรียกคืนหน้าเว็บ ซึ่งจะทำให้การดาวน์โหลด การซิงค์ หรือการเล่นที่ทำงานอยู่ช้าลง หากแอปยังคงจัดสรรหลังจากใช้พื้นที่สลับข้อมูลจนหมด ระบบจะหยุดแอปนั้น หากต้องการตรวจสอบว่าแอปได้รับผลกระทบหรือไม่ โปรดดูขีดจำกัดหน่วยความจำของแอป
- ระบบจะสิ้นสุดกระบวนการที่ใหญ่กว่าก่อนเมื่อหน่วยความจำมีแรงกดดัน เมื่อ
lmkdต้องเพิ่มหน่วยความจำและได้รับการกำหนดค่าให้เลือกกระบวนการที่ใช้หน่วยความจำมากที่สุดในที่เก็บข้อมูลที่มีความสำคัญ (ro.lmk.kill_heaviest_task) กระบวนการบริการที่ทำงานอยู่เบื้องหน้าซึ่งใช้หน่วยความจำ UI หลายร้อย MB จะสิ้นสุดลงก่อนกระบวนการที่เล็กกว่าซึ่งมีความสำคัญเท่ากัน และงานที่ทำงานอยู่เบื้องหลังจะหยุดลงด้วย ดูการโต้ตอบระหว่าง kswapd และ lmkd - สถานะ UI ที่แคชไว้สามารถหยุดทำงาน บีบอัด และเขียนกลับไปยัง Flash ได้ การอนุญาตให้สถานะ UI เบื้องหน้าย้ายไปยังกระบวนการที่แคชไว้จะช่วยให้ระบบปฏิบัติการหยุดกระบวนการชั่วคราว บีบอัดหน่วยความจำลงใน zRAM และเขียนหน้าเว็บที่ไม่ได้ใช้งานกลับไปยังพื้นที่เก็บข้อมูล Flash หน่วยความจำที่ใช้จะลดลงขณะที่สถานะ UI ยังคงอยู่เหมือนเดิม ซึ่งจะช่วยให้ทั้งงานที่ทำงานอยู่เบื้องหลังและ UI ที่แคชไว้มีโอกาสสูงสุดที่จะคงอยู่จนกว่าผู้ใช้จะกลับมา
- งบประมาณหน่วยความจำต่อกระบวนการจะนำไปใช้ได้จริง กระบวนการที่เรียกใช้เฉพาะงานเบื้องหลังจะมีชุดการทำงานขนาดเล็กที่คาดการณ์ได้ คุณจึงกำหนดงบประมาณหน่วยความจำที่จำกัดได้โดยไม่จำกัด UI
การเรียกใช้กระบวนการที่ 2 อาจดูเหมือนเป็นการเพิ่มหน่วยความจำที่ใช้ทั้งหมดของแอป เนื่องจากรันไทม์ที่ 2 มีต้นทุนพื้นฐานเพียงเล็กน้อย การเปรียบเทียบดังกล่าวจะใช้ได้ก็ต่อเมื่อกระบวนการเดียวสามารถทำให้ UI และสถานะกราฟิกทั้งหมดเป็นอิสระได้อย่างสมบูรณ์เมื่อเปลี่ยนไปทำงานในเบื้องหลัง ซึ่งแอปและชุดเครื่องมือ UI ไม่สามารถทำได้อย่างน่าเชื่อถือในทางปฏิบัติ การปล่อยให้ระบบปฏิบัติการหยุดทำงาน บีบอัด และเขียนกระบวนการ UI ที่แคชไว้แยกต่างหากกลับเข้าไปใหม่จะใช้หน่วยความจำจริงน้อยลงมากในกรณีทั่วไป ช่วยให้แอปของคุณอยู่ภายในขีดจำกัดที่ไม่แสดง และลดการสิ้นสุดการทำงานเนื่องจากหน่วยความจำเหลือน้อยสำหรับแอปของคุณและทั่วทั้งอุปกรณ์
แยกงานเบื้องหลังในกระบวนการเฉพาะ
หากต้องการกำจัด UI และกราฟิกที่ค้างอยู่ระหว่างการทำงานในเบื้องหลัง ให้เรียกใช้คอมโพเนนต์เบื้องหลังที่ทำงานเป็นเวลานานในกระบวนการเฉพาะที่มีน้ำหนักเบา โดยยังคง UI และเครื่องมือกราฟิกไว้ในกระบวนการหลัก ดังนี้
| กระบวนการ | การประกาศไฟล์ Manifest | คอมโพเนนต์ที่โฮสต์ | สถานะเมื่อผู้ใช้ออกจาก UI ระหว่างการทำงานในเบื้องหลัง |
|---|---|---|---|
| กระบวนการ UI หลัก | ค่าเริ่มต้น (com.example.app) |
Activity, มุมมอง UI, แคชรูปภาพ, ตัวแสดงผล Vulkan หรือ OpenGL ES, เครื่องมือเสียงและฉาก
|
ลดลงเหลือ PREV แล้วลดลงเหลือ CACHED (oom_score_adj >= 900) มีการหยุดทำงาน บีบอัด และเขียนกลับไปยัง Flash
|
| กระบวนการของผู้ทำงานในเบื้องหลัง | android:process=":bg" |
Serviceที่ทำงานอยู่เบื้องหน้า, เครื่องมือดาวน์โหลดชิ้นงานที่ทำงานอยู่เบื้องหลัง, เวิร์กเกอร์การซิงค์ หรือโปรแกรมจัดการอุปกรณ์ที่เชื่อมต่อ
|
ทำงานที่ FGS (oom_score_adj = 200) โดยมีเฉพาะฮีปและโค้ดของเวิร์กเกอร์เอง และไม่มีมุมมอง UI หรือบริบท GPU
|
ประกาศงบประมาณกระบวนการและหน่วยความจำ
กำหนดพื้นหลัง Service ให้กับกระบวนการย่อยส่วนตัวโดยตั้งค่า android:process=":bg" ใน AndroidManifest.xml เครื่องหมายโคลอนนำหน้า (:) จะสร้างกระบวนการส่วนตัวที่กำหนดขอบเขตเป็น UID ของแอป
ใน Android 17 QPR2 (ระดับ API 37.2) ขึ้นไป ให้รวมการแยกกระบวนการเข้ากับงบประมาณหน่วยความจำของแอปต่อกระบวนการภายในบล็อก <processes> เพื่อให้ระบบปฏิบัติการบังคับใช้เพดานชุดการทำงานที่เข้มงวดกับเวิร์กเกอร์ที่ทำงานอยู่เบื้องหลังโดยไม่จำกัดกระบวนการ UI ที่ทำงานอยู่เบื้องหน้า ดังนี้
<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" คือการปล่อยให้ชุดเครื่องมือ UI ขนาดใหญ่, พูลการโหลดรูปภาพ, Analytics SDK หรือการเริ่มต้นเครื่องมือแบบเนทีฟ (System.loadLibrary) อยู่ภายในเมธอด Application.onCreate ที่ไม่มีการป้องกัน การทำเช่นนี้จะทำให้กระบวนการ :bg มีขนาดใหญ่ขึ้นด้วยฟุตพริ้นท์พื้นฐานเดียวกันกับที่คุณพยายามหลีกเลี่ยง
ตรวจสอบ Application.getProcessName ซึ่งพร้อมใช้งานใน Android 9 (ระดับ API 28) ขึ้นไปภายใน onCreate และเริ่มต้นเฉพาะทรัพยากร Dependency ที่กระบวนการปัจจุบันต้องการ
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");
}
}
ไลบรารีที่เริ่มต้นผ่าน Content Provider เช่น Jetpack App Startup จะเริ่มต้นในกระบวนการหลักเท่านั้น เนื่องจาก Content Provider ที่ประกาศโดยไม่มี android:process จะทำงานในกระบวนการหลัก หากโค้ดใน :bg ขึ้นอยู่กับไลบรารีดังกล่าว ให้เริ่มต้นไลบรารีอย่างชัดเจนใน initBackgroundWorkerProcess
หากคุณสร้างด้วยเกมเอนจิน เช่น Unity ให้ใช้บริการที่ทำงานอยู่เบื้องหลังในไลบรารีหรือปลั๊กอิน Android ธรรมดาที่เขียนด้วย Java หรือ Kotlin ซึ่งไม่ได้อ้างอิงคลาสเอนจิน เช่น UnityPlayer จากนั้นกระบวนการ :bg จะไม่โหลดไลบรารีแบบเนทีฟของเครื่องมือ หากต้องการยืนยัน ให้ตรวจสอบว่า adb shell cat /proc/<pid>/maps สำหรับกระบวนการ :bg ไม่ได้แสดง libunity.so หรือ libil2cpp.so
สื่อสารโดยไม่ต้องปักหมุดกระบวนการ UI
เมื่อผู้ใช้เปิด MainActivity ไว้ขณะที่ AssetDownloadService ทำงานใน :bg UI มักจะต้องแสดงความคืบหน้าในการดาวน์โหลดแบบเรียลไทม์ ความสำคัญของกระบวนการจะไหลจากไคลเอ็นต์ไปยังกระบวนการที่โฮสต์บริการหรือ Content Provider ที่ใช้ ดังนั้นทิศทางของการขึ้นต่อกันแต่ละรายการจึงมีความสำคัญ ดังนี้
- การเชื่อมโยง UI กับ Worker นั้นปลอดภัย เมื่อ
MainActivityเชื่อมโยงกับAssetDownloadServiceระบบจะเพิ่ม:bgให้มีความสำคัญเท่ากับ UI ที่มองเห็นได้ เมื่อผู้ใช้ออกจากแอป กระบวนการ UI จะถูกแคชและการเชื่อมโยงจะไม่ยกระดับสิ่งใดอีกต่อไป - ผู้ปฏิบัติงานที่ขึ้นอยู่กับกระบวนการ UI ไม่ปลอดภัย หากโค้ดใน
:bgผูกกับบริการหรือค้นหา Content Provider ที่ทำงานในกระบวนการหลัก ระบบจะยกระดับกระบวนการหลักให้มีความสำคัญสูงขึ้นเทียบเท่ากับเวิร์กเกอร์ และหน่วยความจำ UI จะถูกตรึงไว้อีกครั้ง ให้ UI ลงทะเบียน Callback กับผู้ปฏิบัติงานแทน เช่นMessengerหรืออินเทอร์เฟซ Callback ของ AIDL การส่งออบเจ็กต์ Binder เป็น Callback จะไม่เปลี่ยนความสำคัญของกระบวนการใดๆ
ยกเลิกการลงทะเบียน Callback ก่อนยกเลิกการเชื่อมโยง เพื่อไม่ให้ผู้ปฏิบัติงานเรียกใช้กระบวนการ UI หลังจากที่ระบบหยุดการทำงานชั่วคราว ตัวอย่างต่อไปนี้จะเชื่อมโยงกับเวิร์กเกอร์เฉพาะในขณะที่ UI ปรากฏเท่านั้น
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: ใช้การบันทึกแบบ Write-Ahead (โหมดสมุดบันทึกเริ่มต้นใน Room บนอุปกรณ์ส่วนใหญ่) เพื่อให้ทั้ง 2 กระบวนการอ่านและเขียนฐานข้อมูลพร้อมกันได้ ไม่ต้องโทรหา
enableMultiInstanceInvalidationจาก:bgRoom โฮสต์บริการการทำให้เป็นโมฆะในกระบวนการหลัก ดังนั้น Worker จะเชื่อมโยงกับกระบวนการหลักและเก็บหน่วยความจำ UI ไว้ที่ความสำคัญที่เพิ่มขึ้นของ Worker แต่ให้ผู้ปฏิบัติงานรายงานการเปลี่ยนแปลงผ่าน Callback ที่ UI ลงทะเบียนไว้ และให้ UI ค้นหาฐานข้อมูลอีกครั้งเมื่อเริ่มต้นทำงาน - ค่ากำหนด: อย่าแชร์
SharedPreferencesในกระบวนการต่างๆ ใช้ DataStore ในโค้ดแบบหลายกระบวนการหรือส่งสถานะอย่างชัดเจนผ่าน IPC - ไฟล์ชิ้นงาน: เขียน Asset Pack ที่ดาวน์โหลดลงในไฟล์ทดลองใช้ใน
:bg, เรียกใช้fdatasyncแล้วเรียกใช้posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED)ใน:bgหลังจากเขียน (ดังที่แสดงในคืนหน้าหน่วยความจำไปยังระบบปฏิบัติการ) และเปลี่ยนชื่อไฟล์ให้ถูกต้อง ให้ UI รับผลลัพธ์เมื่อเริ่มทำงานครั้งถัดไปและเชื่อมโยงกับ Worker แทนที่จะเรียกใช้กระบวนการ UI ที่อาจค้างอยู่
สังเกตผลลัพธ์ใน adb shell
คุณสามารถยืนยันได้ว่าการแยกกระบวนการเบื้องหน้าและกระบวนการเบื้องหลังจะแยกการใช้งานหน่วยความจำที่เพิ่มขึ้นโดยตรวจสอบสถานะกระบวนการของ Activity Manager และ dumpsys meminfo หลังจากไปยังหน้าจอหลัก เอาต์พุตตัวอย่างในส่วนนี้มาจากแอปตัวอย่าง MemoryLab ซึ่งเรียกใช้การจัดสรรแบบเนทีฟขนาด 64 MB เดียวกันในกระบวนการ UI ในการกำหนดค่าทั้ง 2 แบบ และโฮสต์บริการที่ทำงานอยู่เบื้องหน้าที่แยกไว้ในกระบวนการ :remote
1. เปรียบเทียบสถานะกระบวนการและคะแนน OOM
เริ่มภาระงานในเบื้องหลัง กดปุ่มหน้าแรกเพื่อให้ Activity หยุด และค้นหา Activity Manager โดยใช้คำสั่งต่อไปนี้
adb shell dumpsys activity processes com.example.app | grep -E "Proc #|oom:|state:"
ในสถาปัตยกรรมแบบกระบวนการเดียว กระบวนการหลักจะยังคงทำงานอยู่แม้ว่า Activity จะหยุดทำงานแล้วก็ตาม เป็นเวลาประมาณ 15 วินาทีหลังจากที่ผู้ใช้ออกจากแอป ตัวจัดการกิจกรรมจะเก็บกระบวนการที่เรียกใช้บริการที่ทำงานอยู่เบื้องหน้าที่ 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 ขณะที่กระบวนการ UI หลักจะลดลงเหลือ 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) และสูญเสียความสามารถของกระบวนการทั้งหมด (---------) รวมถึงเวลา CPU โดยนัย (I) ณ จุดนั้น ระบบจะหยุดการทำงานของกระบวนการและสามารถบีบอัดหรือเรียกคืนได้ทุกเมื่อโดยไม่รบกวนกระบวนการของผู้ปฏิบัติงาน
2. เปรียบเทียบ dumpsys meminfo ในกระบวนการต่างๆ
เปรียบเทียบข้อมูลสรุป dumpsys meminfo ของผู้ปฏิบัติงานที่ยกระดับกับกระบวนการ UI ดังนี้
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 สำหรับกระบวนการ UI ของ MemoryLab ที่ oom_score_adj = 700 ซึ่งเก็บการจัดสรรแบบเนทีฟขนาด 64 MB ไว้ใน 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 น้อยกว่า 5 MB เนื่องจากแชร์โค้ดและหน้าเว็บรันไทม์ส่วนใหญ่กับ Zygote ในแอปที่แสดงผลด้วย Vulkan หรือ OpenGL ES แถว Graphics ของกระบวนการ UI จะรวมหน่วยความจำ GPU ด้วย
ตัวอย่างนี้บันทึกในอุปกรณ์เสมือนจริง Cuttlefish ที่มีการแสดงผลซอฟต์แวร์ ดังนั้น Graphics จึงเป็น 0 ในทั้ง 2 กระบวนการ
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 MB ขณะที่กระบวนการ UI จะใช้หน่วยความจำประมาณ 98 MB แต่ละกระบวนการมี memory.current ของตัวเอง แต่ตัวจำกัดหน่วยความจำจะกำหนดขีดจำกัดใน cgroup ระดับบนสุดซึ่งมีกระบวนการทั้งหมดของแอป บรรทัดทั้งสองแสดง app.memory.high เดียวกันที่ 966,787,072 ไบต์ ซึ่งเป็นขีดจำกัดที่มองไม่เห็น 922 MB ในเอาต์พุตต่อไปนี้
หากต้องการดูขีดจำกัดที่มองเห็นได้และมองไม่เห็นซึ่งตัวจำกัดหน่วยความจำใช้กับอุปกรณ์ ให้เรียกใช้คำสั่งต่อไปนี้
adb shell am memory-limiter status
ตัวอย่างเอาต์พุตจากอุปกรณ์ที่มี RAM ขนาด 4 GB ซึ่งขีดจำกัดที่ไม่แสดงเป็นครึ่งหนึ่งของขีดจำกัดที่แสดง
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 ในทั้ง 2 กระบวนการขณะที่ผู้ใช้ย้ายจาก UI เบื้องหน้าไปยังหน้าจอหลักในขณะที่งานเบื้องหลังยังคงดำเนินต่อไป
1. บันทึกการติดตาม
บันทึกการติดตาม 15 วินาทีที่บันทึกตัวนับหน่วยความจำของกระบวนการ หน่วยความจำ 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
2. ตรวจสอบแทร็กกระบวนการใน Perfetto UI
เปิดการติดตามใน Perfetto UI และขยายกลุ่มกระบวนการสำหรับ com.example.app และ com.example.app:bg
- ปักหมุดแทร็ก
oom_score_adjและmem.rss.anonสำหรับทั้ง 2 กระบวนการเพื่อให้ปรากฏเคียงข้างกันที่ด้านบนของไทม์ไลน์ - ค้นหาการประทับเวลาที่ผู้ใช้กดปุ่มหน้าแรก
- ในกระบวนการของผู้ปฏิบัติงาน
oom_score_adjจะอยู่ที่200และmem.rss.anonจะคงที่และมีขนาดเล็ก - ในกระบวนการ UI
oom_score_adjจะเพิ่มขึ้นจาก0เป็น200และคงอยู่ที่ระดับนั้นประมาณ 1 วินาทีขณะที่Activityหยุดทำงาน จากนั้นจะเพิ่มขึ้นเป็น700(PREV) หลังจากหมดเวลาของแอปก่อนหน้า ระบบจะย้ายไปยังช่วงที่แคชไว้ ซึ่งระบบสามารถบีบอัดและเรียกคืนหน่วยความจำได้โดยไม่ส่งผลกระทบต่อผู้ปฏิบัติงาน
- ในกระบวนการของผู้ปฏิบัติงาน
ในรูปที่ 2 กระบวนการ UI จะอยู่ที่ 200 เป็นเวลาสั้นๆ ขณะที่ Activity หยุดทำงาน แล้วเพิ่มขึ้นเป็น 700 ขณะที่ Worker อยู่ที่ 200 โดยมี RSS แบบไม่ระบุตัวตนประมาณ 19 MB เพื่อเปรียบเทียบ รูปที่ 3 แสดงการติดตาม 35 วินาทีของการกำหนดค่าที่อยู่ร่วมกัน โดยที่กระบวนการเดียวจะอยู่ที่ 50 แล้วจึงเป็น 200 พร้อมหน่วยความจำ UI ทั้งหมด
หากต้องการสำรวจมุมมองเหล่านี้ ให้เปิด Trace แบบแยกและ Trace ที่อยู่ร่วมกันใน Perfetto UI
3. ค้นหาหน่วยความจำที่ใช้ซึ่งเพิ่มขึ้นด้วย PerfettoSQL
เรียกใช้การค้นหา PerfettoSQL ต่อไปนี้ในแท็บการค้นหา (SQL) เพื่อวัด RSS แบบไม่ระบุตัวตนและ 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 MB เท่านั้นที่ยังคงอยู่ที่ oom_score_adj = 200 หลังจากผ่านไป 1 วินาทีแรก ในการกำหนดค่าแบบอยู่ร่วมกัน หน่วยความจำประมาณ 117 MB จะอยู่ที่ 50 และ 200 ตราบใดที่บริการที่ทำงานอยู่เบื้องหน้ายังทำงานอยู่
← คืนหน้าหน่วยความจำไปยังระบบปฏิบัติการ | ↑ ขึ้น