ทำให้งานเบื้องหลังทำงานต่อไปด้วยกระบวนการที่ใช้ทรัพยากรน้อย

แอปจำนวนมากทำงานที่ถูกต้องเมื่อ 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 กับการแยกกระบวนการแบบแยกที่เก็บเฉพาะ Worker ไว้ที่ oom_score_adj 200 ขณะที่กระบวนการ UI ลดลงเป็นแคช
รูปที่ 1 การทำงานร่วมกันแบบกระบวนการเดียวเทียบกับการแยกกระบวนการแบบแยกส่วนระหว่างการทำงานในเบื้องหลัง

เมื่อกระบวนการ 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 จาก :bg Room โฮสต์บริการการทำให้เป็นโมฆะในกระบวนการหลัก ดังนั้น 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

  1. ปักหมุดแทร็ก oom_score_adj และ mem.rss.anon สำหรับทั้ง 2 กระบวนการเพื่อให้ปรากฏเคียงข้างกันที่ด้านบนของไทม์ไลน์
  2. ค้นหาการประทับเวลาที่ผู้ใช้กดปุ่มหน้าแรก
    • ในกระบวนการของผู้ปฏิบัติงาน oom_score_adj จะอยู่ที่ 200 และ mem.rss.anon จะคงที่และมีขนาดเล็ก
    • ในกระบวนการ UI oom_score_adj จะเพิ่มขึ้นจาก 0 เป็น 200 และคงอยู่ที่ระดับนั้นประมาณ 1 วินาทีขณะที่ Activity หยุดทำงาน จากนั้นจะเพิ่มขึ้นเป็น 700 (PREV) หลังจากหมดเวลาของแอปก่อนหน้า ระบบจะย้ายไปยังช่วงที่แคชไว้ ซึ่งระบบสามารถบีบอัดและเรียกคืนหน่วยความจำได้โดยไม่ส่งผลกระทบต่อผู้ปฏิบัติงาน
Perfetto จะติดตามกระบวนการ UI และกระบวนการของผู้ปฏิบัติงาน กระบวนการ UI oom_score_adj เพิ่มขึ้นจาก 0 เป็น 200 แล้วเป็น 700 หลังจากหน้าแรก ขณะที่กระบวนการของผู้ปฏิบัติงานยังคงอยู่ที่ 200 โดยมี RSS แบบไม่ระบุตัวตนประมาณ 19 MB
รูปที่ 2 แยกกระบวนการหลังจากที่ผู้ใช้กดปุ่มหน้าแรก

ในรูปที่ 2 กระบวนการ UI จะอยู่ที่ 200 เป็นเวลาสั้นๆ ขณะที่ Activity หยุดทำงาน แล้วเพิ่มขึ้นเป็น 700 ขณะที่ Worker อยู่ที่ 200 โดยมี RSS แบบไม่ระบุตัวตนประมาณ 19 MB เพื่อเปรียบเทียบ รูปที่ 3 แสดงการติดตาม 35 วินาทีของการกำหนดค่าที่อยู่ร่วมกัน โดยที่กระบวนการเดียวจะอยู่ที่ 50 แล้วจึงเป็น 200 พร้อมหน่วยความจำ UI ทั้งหมด

Perfetto ติดตามกระบวนการที่อยู่ร่วมกันรายการเดียว oom_score_adj เพิ่มขึ้นจาก 0 เป็น 50 หลังจาก Home และเป็น 200 ในอีกประมาณ 15 วินาทีต่อมา ขณะที่ RSS ที่ไม่ระบุตัวตนยังคงสูงอยู่
รูปที่ 3 กระบวนการที่อยู่ร่วมกันหลังจากที่ผู้ใช้กดปุ่มหน้าแรก

หากต้องการสำรวจมุมมองเหล่านี้ ให้เปิด 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 ตราบใดที่บริการที่ทำงานอยู่เบื้องหน้ายังทำงานอยู่


← คืนหน้าหน่วยความจำไปยังระบบปฏิบัติการ | ↑ ขึ้น