כבילות שירות ומצבי תהליך

תהליכי אפליקציות ב-Android לא מתבצעים בבידוד. אפליקציות מסתמכות לעיתים קרובות על שירותים שמסופקים על ידי אפליקציות אחרות או על ידי המערכת עצמה. כשמגדירים Service Binding כדי לחבר תהליך אחד לתהליך אחר, נוצרת תלות שמשפיעה באופן משמעותי על האופן שבו מסגרת Android מנהלת את הזיכרון.

מצבי תהליך וציוני OOM

ב-framework של Android נעשה שימוש במצבי תהליך כדי לעקוב אחרי החשיבות של כל תהליך שפועל. לאחר מכן, המצבים האלה משמשים את OomAdjuster להקצאת ערך של OOM Score Adjustment (oom_score_adj), בטווח של ‎-1,000 עד 1,000.

ערך נמוך יותר של 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 (Bound Top) תהליך שקשור לאפליקציה מובילה. 100
BFGS שירות שפועל בחזית ומוגבל (בדרך כלל מוגבל למערכת). 0
‫PREV (הקודם) התהליך האחרון שהמשתמש היה בו לפני התהליך הנוכחי. 700
CACHED אפליקציות שפועלות ברקע שאפשר להפסיק את הפעולה שלהן בלי לגרום נזק. ‫900 עד 999

ההשפעה של קישורי שירות

כשלקוח (למשל, אפליקציה במצב TOP) נקשר לשירות בתהליך שרת, תהליך השרת לרוב יורש עדיפות גבוהה יותר. כך השירות יישאר זמין כל עוד הלקוח צריך אותו.

דיאגרמה שמציגה את תהליך א' (למעלה) שקורא ל-bindService() דרך system_server, ומעלה את תהליך ב' ל-BTOP

שליטה בהורשה באמצעות סימונים של BIND

הורשה היא התנהגות ברירת המחדל כשמשתמשים ב-Context.BIND_AUTO_CREATE. עם זאת, מפתחים יכולים לקבוע איך הקישור ישפיע על חשיבות התהליך של היעד באמצעות דגלים שונים ב-bindService().

סימוני BIND חשובים לציון OOM

הדגלים הבאים רלוונטיים במיוחד כשמנהלים את העומס על הזיכרון בכל המערכת:

  • ‫BIND_AUTO_CREATE: הדגל הנפוץ ביותר. היא מוודאת שתהליך השירות יתחיל וימשיך לפעול כל עוד הקישור קיים. כברירת מחדל, הוא גם מעלה את עדיפות התהליך בשרת כך שתהיה זהה לזו של הלקוח.
  • ‫BIND_NOT_FOREGROUND: מונעת את העלאת התהליך של שירות היעד לעדיפות תזמון (עדיפות CPU) בחזית. עם זאת, עדיין אפשר להעלות את העדיפות של הזיכרון (oom_score_adj). האפשרות הזו שימושית לעבודות ברקע שלא צריכות להתחרות עם ממשק המשתמש על מחזורי CPU, אבל עדיין צריכות להיות מוגנות מפני סגירה.
  • ‫BIND_WAIVE_PRIORITY: דגל חזק מאוד שמורה למערכת לא להשפיע על התזמון או על העדיפות של ניהול הזיכרון של תהליך היעד. תהליך השירות ינוהל כאילו היה תהליך רגיל ברקע ברשימת ה-LRU, כך שהוא יהיה מועמד להפסקת פעולה בגלל חוסר זיכרון גם כשהוא משויך.
  • ‫BIND_ABOVE_CLIENT: מציין שהשירות חשוב יותר מאפליקציית הלקוח עצמה. כשהמערכת צריכה לפנות זיכרון, היא תעדיף להרוג את אפליקציית הלקוח לפני שהיא תהרוג את שרת מאגד (bound service). ההצפנה הזו "חזקה" יותר מ-BIND_AUTO_CREATE כי היא מספקת שכבת הגנה נוספת לשירות על חשבון הלקוח.
  • ‫BIND_NOT_PERCEPTIBLE: מוריד את רמת החשיבות של שירות היעד לרמה נמוכה מ-PERCEPTIBLE, וכך המערכת יכולה לפנות זיכרון כדי לפנות מקום לתהליכים קריטיים יותר שהמשתמש יכול לראות.

תרגול מעשי: צפייה באפקטים של קישור

נשתמש באפליקציית MemoryLab כדי להדגים איך קישור מאפליקציית TOP משפיע על המצב של תהליך נפרד.

1. הפעלת MemoryLab

הפקודה הבאה מפעילה את האפליקציה. אחרי שהיא נפתחת, חשוב לוודא שהאפליקציה נשארת בחזית (לא ללחוץ על לחצן הבית ולא לעבור לאפליקציות אחרות).

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

2. זיהוי תהליכים

לפני שמקשרים, צריך לבדוק את מצבי התהליך. ממשק המשתמש הראשי של MemoryLab פועל בתהליך אחד, ויש לו 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 (Bound TOP) עם ערך oom_score_adj של 100. ההגנה הזו משמעותית יותר טובה מההגנה של שירות רגיל שפועל ברקע (שיהיה ברמה 500 ומעלה). הסימון <=Proc{...} מראה איזה תהליך אחראי להעלאת העדיפות הזו.

5. העברה לרקע

לוחצים על הלחצן HOME במכשיר. בודקים שוב את המדינות:

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

עכשיו שני התהליכים עברו למצב עדיפות נמוכה יותר (PREV / oom_score_adj 700), כי תהליך הלקוח כבר לא TOP. (הערה: הערך LAST ב-state dump מתייחס למצב הפנימי 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 ואת קטגוריית ה-atrace‏ am:

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 של התהליך המרוחק ביחס לתהליך של ממשק המשתמש:

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, הפעלת זמן הריצה ויצירת אובייקט האפליקציה) כשמתרחשת אינטראקציית משתמש קריטית. קיימים קשרים אחרים כדי למנוע הפעלות במצב התחלתי (cold start) באפליקציות שצריכות לטפל בזרמים של אירועים ברקע.

הנה כמה דוגמאות מהחיים האמיתיים שאפשר לראות במכשיר טיפוסי:

VoiceInteractor

המשתמשים מצפים שהעוזר הדיגיטלי יהיה מוטמע במערכת ההפעלה של הטלפון, כדי שיוכלו להפעיל אותו באופן מיידי באמצעות מילת הפעלה או תנועת קלט מהירה, ושהאינטראקציה תהיה חלקה ורציפה.

כשמתרחשת הפעלה של העוזר הדיגיטלי (למשל, מילת ההפעלה "Ok Google" בטלפונים של Google Pixel), העוזר הדיגיטלי צריך להגיב באופן מיידי. כדי להבטיח זאת, system_server שומר על קישור קבוע לשירות האינטראקציה הקולית שנבחר על ידי המשתמש.

תרשים שמראה את הקישור של system_server לתהליך האינטראקטור של אפליקציית Google

אם בודקים את מצבי התהליך (למשל באמצעות dumpsys activity processes), יכול להיות שיוצג תהליך כמו com.google.android.googlequicksearchbox:interactor במצב BFGS (Bound Foreground Service), שמופעל על ידי קישור מ-system_server (UID 1000).

NotificationListenerService

בחלק מהקישורים בין המערכת לאפליקציה, המטרה היא לא השהיה, אלא מניעת הפעלות במצב התחלתי (cold start) בתדירות גבוהה. ‫NotificationListenerService, שירות שמקבל שיחות מהמערכת כשמתפרסמות או מוסרות התראות חדשות, הוא דוגמה מצוינת. משתמשים בטלפונים חכמים עשויים לקבל מאות התראות במהלך היום. אם המערכת מבטלת את הקישור של אפליקציה ל-NotificationListener, סביר להניח שהתהליך של האפליקציה יעבור למצב מטמון, והמערכת עשויה להרוג אותו באמצעות LMK.

כשמגיעה ההתראה הבאה – יכול להיות שזה יקרה כמה שניות אחר כך – המערכת תיאלץ להפעיל מחדש את התהליך של האפליקציה במצב התחלתי (cold start) כדי להעביר את האירוע. המחזור הקבוע הזה של הרג והתחלה קרה יצרוך הרבה יותר CPU וסוללה מאשר פשוט להשאיר את התהליך קשור ופעיל ברקע.

המסך '‎-1' (פיד חדשות) במפעיל האפליקציות

אפליקציות מודרניות של מרכז האפליקציות משלבות בדרך כלל את פונקציונליות הניווט הבסיסית (סמלים ווידג'טים של דף הבית) עם פיד חדשות שזמין באחד מהמסכים של מרכז האפליקציות ומשולב בצורה חלקה עם חוויית המשתמש של מרכז האפליקציות. יכול להיות שפיד החדשות מסופק על ידי אפליקציה אחרת. לדוגמה, ב-Google Pixel, מרכז האפליקציות משולב עם פיד שמסופק על ידי אפליקציית Google.

כשמחליקים ימינה במסך הבית כדי לראות את פיד החדשות, המעבר צריך להיות חלק. מרכז האפליקציות עושה זאת על ידי קישור לממשק שירות באפליקציה שמספקת את פיד החדשות, ושמירה על הקישור הזה פעיל כל עוד מרכז האפליקציות פעיל. כך התוכן בפיד מוצג ומוכן בזיכרון גם כשלא מסתכלים עליו.

דוגמאות נפוצות אחרות

  • מרכז האפליקציות (HOME_APP_ADJ): לאפליקציית מרכז האפליקציות (Home) יש משבצת מיוחדת משלה ברשימת העדיפויות. הוא לא תמיד קשור לשירות, אבל הוא מקבל את הערך HOME_APP_ADJ (בדרך כלל 600). המערכת מעדיפה להשאיר את מרכז האפליקציות פעיל, כי המשתמש חוזר אליו לעיתים קרובות. למעשה, המערכת תעדיף להפסיק את הפעולה של האפליקציה שהייתה בשימוש קודם (PREV_APP_ADJ = 700) מאשר להפסיק את הפעולה של מרכז האפליקציות, כי הפסקת הפעולה של מרכז האפליקציות תוביל לחוויית משתמש איטית כשיוצאים מכל אפליקציה, כי המשתמש יצטרך לחכות עד שמרכז האפליקציות יופעל מחדש.
  • עורך שיטות קלט (IME): כשמקלידים, המערכת מתחברת לאפליקציית המקלדת שבחרתם (למשל, Gboard). כך תהליך המקלדת נשאר במצב מורם גם אם המקלדת מוסתרת באופן זמני. כך תוכלו לוודא שהמקלדת תופיע מחדש באופן מיידי כשתקישו על שדה טקסט אחר.
  • תשלומים באמצעות NFC: כשמצמידים את הטלפון למסוף כדי לשלם, המערכת מתחברת לשירות התשלומים באמצעות NFC (למשל, Google Wallet). לרוב, העסקאות האלה כוללות דרישות מחמירות בזמן אמת ממסוף הסליקה של המוכר. אם אפליקציית התשלום נאלצה להתחיל במצב התחלתי (cold start), יכול להיות שהעסקה תסתיים בטיימ-אאוט ותיכשל.

הפשרות הנדרשות והירידה החדה בביצועים

הקישורים חיוניים לביצועים ולדיוק, אבל הם פוגעים בזיכרון של המערכת.

  • גמישות מופחתת: כל תהליך שמוגבל הוא תהליך ש-LMK לא יכול להפסיק בקלות. כך מצטמצם היתרון של תהליכים במטמון שהמערכת יכולה להשתמש בהם כדי לפנות זיכרון במצב של עומס.
  • החמרה של ירידה חדה בביצועים: אם יש יותר מדי תהליכים קשורים, יכול להיות שבמערכת לא יישארו תהליכי רקע שאפשר להפסיק. כשהעומס על הזיכרון גדל, המערכת תפסיק לפעול הרבה יותר מהר, כי היא נאלצת להפסיק תהליכים חשובים יותר או להשתמש במטמון הדפים באופן לא יעיל.

דפוסי אנטי-כבילת שירות נפוצים

מכיוון שקישורי שירות מעלים את רמת ההרשאות של oom_score_adj באופן ישיר, טעויות קלות במחזור החיים של הקישורים, באופן שבו הם נוצרים או במבנה שלהם, עלולות לגרום לשימוש בכמויות גדולות של זיכרון במצבים עם הרשאות (BTOP,‏ BFGS או PERC) למשך שעות. כשמתכננים או בודקים שירותים מאוגדים, כדאי להיזהר מהדפוסים הנפוצים הבאים שמעידים על בעיות.

שכחת unbindService() בלקוחות לטווח ארוך

קישור לשירות מ-Application singleton, ממנהל ברקע או מ-Activity שמפעיל את bindService() ב-onStart() בלי unbindService() תואם ב-onStop() גורם לדליפה של ServiceConnection.

כל עוד הקישור הזה פעיל, תהליך היעד מקבל בירושה את העדיפות הגבוהה של הלקוח. אם הלקוח הוא רכיב מערכת קבוע או אפליקציה שפועלת בחזית, תהליך השירות המצורף נשאר מוצמד ב-BFGS או ב-BTOP ללא הגבלת זמן (מופיע קרוב ל-100% ב-procstats), ומונע מ-LMK להחזיר את הזיכרון שלו גם כשהשירות לא פעיל לחלוטין.

פתרון: צריך להגדיר את ההתאמות של היקף השימוש רק למחזור החיים של הרכיב שזקוק להן, או להטמיע זמן קצוב ללא פעילות שקורא ל-unbindService() אחרי תקופה של חוסר פעילות. אל תבטלו את הקישור ואז תקשרו מחדש בכל קריאה נפרדת ל-RPC כי זה גורם להעמסת התהליך ולתקורה חוזרת של הגדרת binder. במקום זאת, כדאי לאחד פרצי עבודה מאחורי טיימר קצר של בלי פעילות (לדוגמה, 5 עד 30 שניות).

מיקום משותף של שירות מאוגד עם ממשק משתמש שדורש הרבה זיכרון

כברירת מחדל, כל הרכיבים בחבילת APK פועלים באותו תהליך. אם האפליקציה חושפת שירות קל משקל שמוגבל (כמו NotificationListenerService, ספק ווידג'טים או שירות פלאגין שהמערכת או מרכז האפליקציות קושרים אליו) באותו תהליך כמו Activity הראשי, התהליך כולו מקבל בירושה את המצב המוגבר של השירות (BFGS או PERC, בדרך כלל oom_score_adj מתוך 200 או פחות).

כשהמשתמש פותח את ממשק המשתמש של האפליקציה, התהליך מקצה היררכיות גדולות של תצוגות, מפות סיביות מפוענחות ומאגרי גרפיקה. כשהמשתמש עובר למקום אחר, התהליך לא יורד ל-CACHED (oom_score_adj מתוך 900 ומעלה) כי הקישור הפעיל לשירות שומר על רמת התהליך. כתוצאה מכך, נוצרות שתי בעיות:

  • אין דחיסה של זיכרון ברקע: המערכת CachedAppOptimizer דוחסת תהליכים רק אחרי שהם עוברים למצב CACHED.
  • אין חיתוך של זיכרון LRU במטמון או שחזור LMK: המערכת לא מספקת קריאות חוזרות לחיתוך ברקע שקשורות לרשימת LRU במטמון (גרסה TRIM_MEMORY_BACKGROUND ואילך) בזמן שקישור שירות מחזיק את התהליך במצב מורם. אם האפליקציה לא משחררת באופן מפורש משאבי ממשק משתמש ב-TRIM_MEMORY_UI_HIDDEN או ב-Activity.onStop(), הקצאות שיא של ממשק משתמש נשארות נעוצות ב-RAM עם ניקוד OOM מועדף, כך שה-LMK לא יכול בקלות להחזיר אותן.

פתרון: אם ממשק המשתמש מפסיק להיות גלוי, צריך להוציא מהזיכרון את מטמון ממשק המשתמש, את מפות הביטים המפוענחות ואת הפניות לתצוגה באופן מפורש באמצעות Activity.onStop(),‏ ComponentCallbacks2.onTrimMemory(TRIM_MEMORY_UI_HIDDEN),‏ Application.ActivityLifecycleCallbacks או ProcessLifecycleOwner. אפשרות אחרת היא להעביר את השירות שתמיד קשור לתהליך נפרד וקל משקל באמצעות מאפיין המניפסט android:process, כדי שהמערכת תוכל להעביר את תהליך ממשק המשתמש הראשי למצב CACHED ולצמצם את הזיכרון שלו או להקצות אותו מחדש באופן עצמאי.

השמטת דגלים לביטול עדיפות

התקשרות אל bindService() עם BIND_AUTO_CREATE בלבד מעבירה את התזמון המלא של המתקשר ואת העדיפות בזיכרון לשירות היעד. כשמפעילים אפליקציה בחזית שמתחברת לשירות ניתוח נתונים, רישום או אחזור מראש של נתונים ברקע באמצעות BIND_AUTO_CREATE בלבד, העובד ברקע מקודם בטעות ל-BTOP.

פתרון: כשמבצעים קישור לשירותים משניים או לשירותים שפועלים במקביל לאפליקציה ולא צריכים את אותה רמת הגנה כמו ממשק המשתמש של האפליקציה, משלבים את BIND_AUTO_CREATE עם BIND_WAIVE_PRIORITY,‏ BIND_NOT_FOREGROUND או BIND_NOT_PERCEPTIBLE כדי שהמערכת עדיין תוכל לנהל את תהליך היעד ברשימת ה-LRU שנשמרה במטמון.


← Locality | ↑ Up | System-wide →