การเชื่อมโยงบริการและสถานะกระบวนการ

กระบวนการของแอปใน Android ไม่ได้ทำงานแยกกัน แอปพลิเคชันมักต้องอาศัยบริการที่แอปพลิเคชันอื่นหรือระบบเองเป็นผู้ให้บริการ เมื่อกระบวนการหนึ่ง เชื่อมต่อกับอีกกระบวนการหนึ่งผ่านการผูกบริการ กระบวนการนั้นจะสร้างการพึ่งพาซึ่งส่งผล อย่างมากต่อวิธีที่เฟรมเวิร์ก Android จัดการหน่วยความจำ

สถานะกระบวนการและคะแนน OOM

เฟรมเวิร์ก Android ใช้สถานะกระบวนการ เพื่อติดตามความสำคัญของแต่ละกระบวนการที่กำลังทำงาน จากนั้น OomAdjuster จะใช้สถานะเหล่านี้เพื่อกำหนดค่าการปรับคะแนน OOM (oom_score_adj) ซึ่งมีค่าตั้งแต่ -1000 ถึง 1000

oom_score_adj ที่ต่ำลงหมายความว่ากระบวนการมีความสำคัญมากขึ้นและมีแนวโน้มน้อยลงที่จะถูกหยุดทำงานโดย Low Memory Killer (LMK)

สถานะกระบวนการทั่วไป

ตารางต่อไปนี้แสดงสถานะกระบวนการที่พบบ่อยที่สุดบางสถานะและค่า oom_score_adj โดยทั่วไป หากต้องการดูรายการที่สมบูรณ์และเป็นปัจจุบัน โปรดดู android.app.ActivityManager และ com.android.server.am.psc.Constants ในซอร์สโค้ด Android

สถานะกระบวนการ (ตัวย่อ) คำอธิบาย oom_score_adj โดยทั่วไป
PER (ถาวร) กระบวนการของระบบที่ต้องทำงานอยู่เสมอ (เช่น โทรคมนาคม) -800
TOP กระบวนการที่ผู้ใช้กำลังโต้ตอบด้วย 0
VIS (มองเห็นได้) กระบวนการมีกิจกรรมที่มองเห็นได้ (เช่น อยู่เบื้องหลังกล่องโต้ตอบโปร่งแสง) 100
PERC (รับรู้ได้) กระบวนการเบื้องหลังที่ผู้ใช้รับรู้ (เช่น การเล่นเพลง) 200
FGS กระบวนการที่โฮสต์บริการที่ทำงานอยู่เบื้องหน้า 0 ถึง 200 (แตกต่างกันไป)
BTOP (ผูกไว้กับ TOP) กระบวนการที่แอปพลิเคชัน TOP ผูกไว้ 100
BFGS บริการที่ทำงานอยู่เบื้องหน้าที่ผูกไว้ (โดยปกติจะผูกไว้กับระบบ) 0
PREV (ก่อนหน้า) กระบวนการล่าสุดที่ผู้ใช้ใช้งานก่อนกระบวนการปัจจุบัน 700
CACHED แอปเบื้องหลังที่หยุดทำงานได้อย่างปลอดภัย 900 ถึง 999

ผลกระทบของการผูกบริการ

เมื่อกระบวนการไคลเอ็นต์ (เช่น แอปในสถานะ TOP) ผูกกับบริการในกระบวนการเซิร์ฟเวอร์ กระบวนการเซิร์ฟเวอร์มักจะรับ สิทธิ์เข้าถึงลำดับความสำคัญที่สูงขึ้น ซึ่งจะช่วยให้บริการยังคงพร้อมใช้งานตราบใดที่ไคลเอ็นต์ต้องการ

แผนภาพแสดงกระบวนการ A (TOP) ที่เรียก bindService() ผ่าน system_server
ยกระดับกระบวนการ B เป็น BTOP

การควบคุมการรับสิทธิ์เข้าถึงด้วยแฟล็ก BIND

การรับสิทธิ์เข้าถึงเป็นลักษณะการทำงานเริ่มต้นเมื่อใช้ Context.BIND_AUTO_CREATE อย่างไรก็ตาม นักพัฒนาแอปสามารถควบคุมวิธีที่การผูกส่งผลต่อความสำคัญของกระบวนการเป้าหมายได้โดยใช้แฟล็กต่างๆ ใน bindService()

แฟล็ก BIND หลักสำหรับคะแนน OOM

แฟล็กต่อไปนี้มีความเกี่ยวข้องมากที่สุดเมื่อจัดการแรงกดดันด้านหน่วยความจำทั่วทั้งระบบ

  • BIND_AUTO_CREATE: แฟล็กที่ใช้บ่อยที่สุด ซึ่งจะช่วยให้กระบวนการบริการเริ่มต้นและทำงานอยู่ตราบใดที่การผูกยังคงอยู่ โดยค่าเริ่มต้น แฟล็กนี้จะเพิ่มลำดับความสำคัญของกระบวนการเซิร์ฟเวอร์ให้ตรงกับไคลเอ็นต์ด้วย
  • BIND_NOT_FOREGROUND: ป้องกันไม่ให้กระบวนการของบริการเป้าหมายได้รับการเพิ่มลำดับความสำคัญในการจัดกำหนดการ เบื้องหน้า (ลำดับความสำคัญของ CPU) อย่างไรก็ตาม แฟล็กนี้ยังคงอนุญาต ให้เพิ่มลำดับความสำคัญของหน่วยความจำ (oom_score_adj) ซึ่งมีประโยชน์สำหรับงานเบื้องหลังที่ไม่ควรแข่งขันกับ UI เพื่อใช้รอบ CPU แต่ควรได้รับการป้องกันไม่ให้ถูกหยุดทำงาน
  • BIND_WAIVE_PRIORITY: แฟล็กที่ทรงพลังมากซึ่งสั่งให้ระบบไม่ ส่งผลต่อลำดับความสำคัญในการจัดกำหนดการหรือการจัดการหน่วยความจำของกระบวนการเป้าหมาย ระบบจะจัดการกระบวนการบริการราวกับว่าเป็นกระบวนการเบื้องหลังปกติในรายการ LRU ซึ่งทำให้กระบวนการมีสิทธิ์ถูกหยุดทำงานเนื่องจากหน่วยความจำไม่เพียงพอแม้ว่าจะผูกไว้ก็ตาม
  • BIND_ABOVE_CLIENT: ระบุว่าบริการมีความสำคัญมากกว่าแอปไคลเอ็นต์เอง เมื่อระบบต้องการเรียกคืนหน่วยความจำ ระบบจะเลือกหยุดทำงานแอปไคลเอ็นต์ก่อนที่จะหยุดทำงานบริการที่มีผลผูกพัน แฟล็กนี้ "ทรงพลัง" กว่า BIND_AUTO_CREATE เนื่องจากมีการป้องกันบริการเพิ่มเติมโดยแลกกับการทำงานของไคลเอ็นต์
  • BIND_NOT_PERCEPTIBLE: ลดความสำคัญของบริการเป้าหมายให้อยู่ต่ำกว่า PERCEPTIBLEระดับ ซึ่งช่วยให้ระบบเรียกคืนหน่วยความจำเพื่อเพิ่มพื้นที่ว่าง สำหรับกระบวนการที่สำคัญกว่าซึ่งผู้ใช้รับรู้ได้

ลงมือปฏิบัติจริง: การสังเกตผลกระทบของการผูก

เราจะใช้แอปพลิเคชัน MemoryLab เพื่อแสดงให้เห็นว่าการผูกจากแอป TOP ส่งผลต่อสถานะของกระบวนการแยกต่างหากอย่างไร

1. เปิดใช้ MemoryLab

คำสั่งต่อไปนี้จะเปิดใช้แอป หลังจากที่แอปเปิดขึ้นแล้ว ตรวจสอบว่าแอปยังคงทำงานอยู่เบื้องหน้า (อย่ากดปุ่มโฮมหรือสลับแอปในตอนนี้)

adb shell am start -n com.android.memorylab/.MainActivity

2. ระบุกระบวนการ

ตรวจสอบสถานะกระบวนการก่อนการผูก MemoryLab เรียกใช้ UI หลักในกระบวนการหนึ่งและมี RemoteService ที่ทำงานในกระบวนการ :remote

adb shell dumpsys activity processes com.android.memorylab

ตัวอย่างข้อมูลโค้ดเอาต์พุต

  Process OOM control (154 total, non-act at 7, non-svc at 7):
    Proc #0: fg       T/A/TOP  LCMNFUATI  t: 0 13470:com.android.memorylab/u0a417 (top-activity)
        oom: max=1001 curRaw=0 setRaw=0 cur=0 set=0
        state: cur=TOP  set=TOP   lastRss=0.00 lastCachedRss=0.00

คุณจะเห็นกระบวนการหลัก com.android.memorylab ในสถานะ TOP กระบวนการ :remote ยังไม่เริ่มต้น

3. ทริกเกอร์การผูก

ส่งการออกอากาศไปยังแอปเพื่อทริกเกอร์การผูกบริการ

adb shell am broadcast -a com.android.memorylab.LEAK_BINDER

4. สังเกตสถานะที่เพิ่มขึ้น

ตรวจสอบสถานะกระบวนการอีกครั้ง

adb shell dumpsys activity processes | grep -A 10 "com.android.memorylab"

ตัวอย่างข้อมูลโค้ดเอาต์พุต

  Proc #  1: vis      F/ /BTOP ---NFUATI  t: 0 13560:com.android.memorylab:remote/u0a417 (service)
    com.android.memorylab/.RemoteService<=Proc{13470:com.android.memorylab/u0a417}
    oom: max=1001 curRaw=100 setRaw=100 cur=100 set=100
    state: cur=BTOP set=BTOP  lastRss=0.00 lastCachedRss=0.00

ตอนนี้กระบวนการ :remote กำลังทำงานและอยู่ในสถานะ BTOP (ผูกไว้กับ TOP) โดยมี oom_score_adj เป็น 100 ซึ่งได้รับการป้องกันมากกว่าบริการที่ทำงานอยู่เบื้องหลังทั่วไปอย่างมาก (ซึ่งจะมีค่า 500 ขึ้นไป) สัญกรณ์ <=Proc{...} แสดงกระบวนการที่รับผิดชอบในการเพิ่มลำดับความสำคัญนี้

5. ส่งไปยังเบื้องหลัง

กดปุ่มโฮม บนอุปกรณ์ ตรวจสอบสถานะอีกครั้ง

adb shell dumpsys activity processes | grep -A 10 "com.android.memorylab"

ตัวอย่างข้อมูลโค้ดเอาต์พุต

    Proc #  2: prev     b/ /LAST --------I  t: 0 13560:com.android.memorylab:remote/u0a417 (service)
        com.android.memorylab/.RemoteService<=Proc{13470:com.android.memorylab/u0a417}
        oom: max=1001 curRaw=700 setRaw=700 cur=700 set=700
        state: cur=LAST set=LAST  lastRss=0.00 lastCachedRss=0.00
    Proc #  1: prev     b/ /LAST --------I  t: 0 13470:com.android.memorylab/u0a417 (previous)
        oom: max=1001 curRaw=700 setRaw=700 cur=700 set=700
        state: cur=LAST set=LAST  lastRss=209MB lastCachedRss=0.00

ตอนนี้กระบวนการทั้ง 2 รายการเปลี่ยนไปอยู่ในสถานะลำดับความสำคัญที่ต่ำกว่า (PREV / oom_score_adj 700) เนื่องจากกระบวนการไคลเอ็นต์ไม่ได้อยู่ในสถานะ TOP อีกต่อไป (หมายเหตุ: LAST ในการดัมพ์สถานะหมายถึงสถานะภายใน LAST_ACTIVITY ซึ่ง แมปกับ PREV ในข้อมูลสรุประดับสูง)

การวิเคราะห์ด้วย procstats

เครื่องมือ procstats จะแสดงมุมมองย้อนหลังของสถานะเหล่านี้

# View stats for MemoryLab over the last hour
adb shell dumpsys procstats --hours 1 com.android.memorylab

ตัวอย่างข้อมูลโค้ดเอาต์พุต

  *   com.android.memorylab / u0a417 / v37:
      *   Prc com.android.memorylab / u0a417 / v37:
             TOTAL: 0.89% (0.00-0.00-0.00/0.00-0.00-0.00/210MB-210MB-210MB over 1)
               Top: 0.89% (0.00-0.00-0.00/0.00-0.00-0.00/210MB-210MB-210MB over 1)
      *   Prc com.android.memorylab:remote / u0a417 / v37:
             TOTAL: 0.19%
           Bnd Top: 0.19%

ในที่นี้ Bnd Top ระบุเปอร์เซ็นต์ของเวลาที่กระบวนการระยะไกลใช้ไปในการผูกไว้กับแอปพลิเคชันในสถานะ TOP

การบันทึกและวิเคราะห์การผูกด้วย Perfetto

แม้ว่า dumpsys จะแสดงภาพรวม แต่ Perfetto ช่วยให้คุณเห็นช่วงเวลาที่เกิดการผูกและวิธีที่คะแนน OOM เปลี่ยนไปแบบเรียลไทม์ได้อย่างแม่นยำ

1. บันทึกการติดตาม

ใช้การกำหนดค่าที่มี linux.process_stats และหมวดหมู่ am atrace

adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/service_bindings.perfetto-trace <<EOF
buffers: { size_kb: 65536 }
data_sources: {
    config {
        name: "linux.process_stats"
        process_stats_config { proc_stats_poll_ms: 100 }
    }
}
data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config { ftrace_events: "am/am_proc_bound" }
    }
}
duration_ms: 15000
EOF

2. ค้นหาการเปลี่ยนคะแนน OOM

การใช้ PerfettoSQL จะช่วยให้คุณเห็นวิธีที่คะแนน OOM ของกระบวนการระยะไกลเปลี่ยนไปเมื่อเทียบกับกระบวนการ UI

SELECT ts, p.name, value AS oom_score_adj
FROM counter c
JOIN process_counter_track t ON c.track_id = t.id
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.memorylab%'
  AND t.name = 'oom_score_adj'
ORDER BY ts;

3. ระบุเหตุการณ์การผูก

หากต้องการดูเวลาที่สร้างการพึ่งพาการผูกและกระบวนการที่เริ่มต้นการผูกอย่างแม่นยำ ให้ใช้การค้นหานี้

SELECT
    s.ts,
    p.name AS process_name,
    t.name AS thread_name,
    s.name AS slice_name
FROM slice s
JOIN thread_track tt ON s.track_id = tt.id
JOIN thread t USING (utid)
JOIN process p USING (upid)
WHERE s.name LIKE 'bindService:{com.android.memorylab%';

การผูกจากระบบไปยังแอป

ระบบ Android เองมักจะผูกกับบริการในแอปของบุคคลที่สามเพื่อมอบฟังก์ชันหลัก โดยเป้าหมายของการผูกเหล่านี้มักจะเป็นการลดเวลาในการตอบสนอง การทำให้กระบวนการทำงานและอยู่ในหน่วยความจำจะช่วยให้ระบบหลีกเลี่ยงค่าใช้จ่ายที่สูงของ "Cold Start" (การโหลด APK, การเริ่มต้นรันไทม์ และการสร้างออบเจ็กต์แอปพลิเคชัน) เมื่อเกิดการโต้ตอบของผู้ใช้ที่สำคัญ นอกจากนี้ยังมีการผูกอื่นๆ เพื่อป้องกันการเริ่มต้นแบบเย็นบ่อยครั้งสำหรับแอปที่ต้องจัดการสตรีมเหตุการณ์เบื้องหลัง

ต่อไปนี้คือตัวอย่างในโลกแห่งความเป็นจริงที่คุณสังเกตได้ในอุปกรณ์ทั่วไป

VoiceInteractor

ผู้ใช้คาดหวังว่าผู้ช่วยดิจิทัลจะฝังอยู่ในระบบปฏิบัติการของโทรศัพท์ สามารถเรียกผู้ช่วยได้ทันทีด้วยคำสั่งเสียงหรือท่าทางสัมผัสป้อนข้อมูลอย่างรวดเร็ว และการโต้ตอบจะเป็นไปอย่างราบรื่น

เมื่อเกิดทริกเกอร์ผู้ช่วย (เช่น คำสั่งเสียง "Ok Google" ในโทรศัพท์ Google Pixel) ผู้ช่วยดิจิทัลต้องตอบสนองทันที เพื่อให้เป็นเช่นนี้ system_server จะรักษาการผูกถาวรกับบริการการโต้ตอบด้วยเสียง ที่ผู้ใช้เลือก

แผนภาพแสดงการเชื่อมโยง system_server กับกระบวนการตัวโต้ตอบของแอป Google

หากคุณตรวจสอบสถานะกระบวนการ (เช่น ใช้ dumpsys activity processes) คุณอาจเห็นกระบวนการอย่าง com.google.android.googlequicksearchbox:interactor ในสถานะ BFGS (บริการที่ทำงานอยู่เบื้องหน้าที่ผูกไว้) ซึ่งทำงานอยู่โดยการผูกจาก system_server (UID 1000)

NotificationListenerService

สำหรับการผูกจากระบบไปยังแอปบางรายการ เป้าหมายไม่ใช่เวลาในการตอบสนอง แต่เป็นการป้องกันการเริ่มต้นแบบเย็นบ่อยครั้ง NotificationListenerServiceซึ่งเป็นบริการที่รับการเรียกจากระบบเมื่อมีการโพสต์หรือนำการแจ้งเตือนใหม่ออก เป็นตัวอย่างที่สำคัญ ผู้ใช้สมาร์ทโฟนทั่วไปอาจได้รับการแจ้งเตือนหลายร้อยรายการตลอดทั้งวัน หากระบบยกเลิกการผูกจาก Listener การแจ้งเตือน กระบวนการของแอปนั้นมีแนวโน้มที่จะเปลี่ยนไปอยู่ในสถานะแคชและอาจถูกหยุดทำงานโดย LMK

เมื่อมีการแจ้งเตือนครั้งถัดไปมาถึง ซึ่งอาจใช้เวลาเพียงไม่กี่วินาที ระบบจะถูกบังคับให้ Cold Start กระบวนการของแอปอีกครั้งเพียงเพื่อส่งเหตุการณ์ วงจรการหยุดทำงานและการเริ่มต้นแบบเย็นอย่างต่อเนื่องนี้จะใช้ CPU และแบตเตอรี่มากกว่าการผูกกระบวนการและทำให้กระบวนการทำงานอยู่เบื้องหลัง

หน้าจอ "-1" ของ Launcher (ฟีดข่าว)

แอป Launcher สมัยใหม่มักจะรวมฟังก์ชันการทำงานหลักในการนำทาง (ไอคอนและวิดเจ็ตหน้าแรก) เข้ากับฟีดข่าวที่พร้อมใช้งานในหน้าจอ Launcher หน้าจอหนึ่งและผสานรวมกับ UX ของ Launcher ได้อย่างราบรื่น แอปอื่นอาจเป็นผู้ให้บริการฟีดข่าว ตัวอย่างเช่น ใน Google Pixel, Launcher จะผสานรวมกับฟีดที่แอป Google เป็นผู้ให้บริการ

เมื่อคุณปัดไปทางซ้ายในหน้าจอหลักเพื่อดูฟีดข่าว การเปลี่ยนหน้าจอต้องราบรื่น Launcher ทำได้โดยการผูกกับอินเทอร์เฟซบริการในแอปที่ให้บริการฟีดข่าว และรักษาการผูกนั้นให้ทำงานอยู่ตราบใดที่ Launcher ยังทำงานอยู่ ซึ่งจะทำให้เนื้อหาฟีดแสดงผลและพร้อมใช้งานในหน่วยความจำแม้ว่าคุณจะไม่ได้ดูอยู่ก็ตาม

ตัวอย่างทั่วไปอื่นๆ

  • Launcher (HOME_APP_ADJ): แอป Launcher (หน้าแรก) มีช่องพิเศษของตัวเอง ในรายการลำดับความสำคัญ แม้ว่าจะไม่ได้ผูกไว้กับบริการเสมอไป แต่ระบบจะกำหนด HOME_APP_ADJ (โดยปกติคือ 600) ให้ ระบบจะเลือกทำให้ Launcher ทำงานอยู่เสมอ เนื่องจากผู้ใช้กลับมาใช้ Launcher บ่อยครั้ง ในความเป็นจริง ระบบจะเลือกหยุดทำงานแอปที่ใช้ก่อนหน้านี้ (PREV_APP_ADJ = 700) มากกว่าที่จะหยุดทำงาน Launcher เนื่องจากหากหยุดทำงาน Launcher จะทำให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ช้าลงเมื่อออกจากแอปใดก็ตาม เนื่องจากผู้ใช้จะต้องรอให้ Launcher เริ่มต้นแบบเย็น
  • ตัวแก้ไขวิธีการป้อนข้อมูล (IME): เมื่อคุณพิมพ์ ระบบจะผูกกับแอปแป้นพิมพ์ที่คุณ เลือก (เช่น Gboard) ซึ่งจะทำให้กระบวนการแป้นพิมพ์อยู่ในสถานะที่เพิ่มขึ้นแม้ว่าแป้นพิมพ์จะซ่อนอยู่ชั่วคราวก็ตาม ซึ่งจะช่วยให้แป้นพิมพ์ปรากฏขึ้นอีกครั้งได้ทันทีเมื่อคุณแตะช่องข้อความอื่น
  • การชำระเงินผ่าน NFC: เมื่อคุณแตะโทรศัพท์เพื่อชำระเงิน ระบบจะเชื่อมโยงกับบริการชำระเงิน (เช่น Google Wallet) ธุรกรรมเหล่านี้มักมีข้อกำหนดแบบเรียลไทม์ที่เข้มงวดจากเทอร์มินัลของร้านค้า หากแอปการชำระเงินต้อง Cold Start ธุรกรรมอาจหมดเวลาและล้มเหลว

ข้อดีข้อเสียและจุดที่ประสิทธิภาพตกฮวบ

แม้ว่าการผูกจะจำเป็นต่อประสิทธิภาพและความถูกต้อง แต่ก็ส่งผลเสียต่อสุขภาพหน่วยความจำของระบบ

  • ความยืดหยุ่นลดลง: กระบวนการที่ผูกไว้ทุกกระบวนการคือกระบวนการที่ LMK ไม่ สามารถหยุดทำงานได้อย่างง่ายดาย ซึ่งจะลด "การรองรับ" ของกระบวนการที่แคชไว้ซึ่งระบบสามารถใช้เพื่อเพิ่มหน่วยความจำว่างเมื่อมีแรงกดดัน
  • ทำให้จุดที่ประสิทธิภาพตกฮวบรุนแรงขึ้น: หากมีการผูกกระบวนการมากเกินไป ระบบอาจพบว่าไม่มีกระบวนการเบื้องหลังที่หยุดทำงานได้เลย เมื่อแรงกดดันด้านหน่วยความจำเพิ่มขึ้น ระบบจะ "ตกจากจุดที่ประสิทธิภาพตกฮวบ" เร็วขึ้นมาก เนื่องจากถูกบังคับให้หยุดทำงานกระบวนการที่สำคัญกว่าหรือทำให้แคชของเพจเกิดการสลับไปมา

← ความเฉพาะเจาะจง | ↑ ขึ้น | ทั่วทั้งระบบ →