מקפיא לאפליקציות שנשמרו במטמון

‫Android מגרסה 11 (רמת API‏ 30) ואילך תומך בהקפאה של אפליקציות שנשמרו במטמון. התכונה הזו מפסיקה את ההפעלה של תהליכים שנשמרו במטמון ומפחיתה את השימוש במשאבים על ידי אפליקציות שמתנהגות בצורה לא תקינה, שעשויות לנסות לפעול בזמן שהן שמורות במטמון.

התכונה 'הקפאת אפליקציות בזיכרון המטמון' שומרת את האפליקציות ב-RAM, אבל לא מאפשרת להן לפעול במעבד. אם מערכת Android קובעת שאפליקציה לא צריכה לבצע פעולות אבל יכול להיות שהיא תידרש בעתיד, היא מקפיאה את תהליך האפליקציה במקום לסיים אותו. כך נמנעת הפעלה במצב התחלתי (cold start) כשצריך להשתמש באפליקציה שוב.

מערכת Android מקפיאה אפליקציות שנשמרו במטמון על ידי העברת התהליכים שלהן לקבוצת בקרה (cgroup) קפואה. כך מצטמצמת צריכת ה-CPU הפעילה והסרק כשיש אפליקציות פעילות במטמון. אפשר להפעיל את ההקפאה של האפליקציות באמצעות דגל הגדרת מערכת או אפשרות למפתחים.

ב-Android 14 (רמת API‏ 34) ומעלה, ההקפאה של אפליקציות שנשמרו במטמון כוללת את ההתנהגויות החזקות הבאות:

  • תהליכי אפליקציות במצב שמור במטמון מוקפאים 10 שניות אחרי הכניסה למצב שמור במטמון.
  • המערכת מבטלת את ההקפאה של תהליך אפליקציה שהוקפא באופן מיידי במהלך אירוע במחזור החיים. האירועים האלה כוללים קבלת intent, הפעלה של job service או חידוש של activity על ידי המשתמש.

‫ActivityManagerService מנהל את כל התהליכים של האפליקציה ומקבל החלטות לגבי מחזור החיים של האפליקציה. ‫CachedAppOptimizer אחראי להקפאת תהליך האפליקציה.

כשמקפיאים תהליך של אפליקציה, כל השרשורים שלה מושעים ולא יכולים לבצע עבודת CPU עד שמבטלים את ההקפאה. כתוצאה מכך, האפליקציה לא יכולה לבצע איסוף אשפה (GC) ולא יכולה להגיב לאירועים של ניהול זיכרון. מידע נוסף זמין במאמר ComponentCallbacks2.onTrimMemory(int). כדי להתאים את המערכת לשינוי הזה, החל מ-Android 14:

  • אפליקציות עם מופע Activity גלוי מקבלות הודעה על TRIM_MEMORY_UI_HIDDEN ברגע שהן עוברות לרקע. אפליקציות שנשארות במחזור חיים בלי ממשק משתמש, כמו אפליקציות עם שירות שפועל בחזית, עשויות לקבל TRIM_MEMORY_BACKGROUND. אירועים אחרים של חיתוך לא מועברים, כי כשהאפליקציות עומדות בדרישות לאירועים האלה, הן אמורות להיות בהקפאה.
  • זמן קצר אחרי הכניסה למצב שמור במטמון, יכול להיות שהמערכת תבקש מהסביבה של זמן הריצה של האפליקציה לבצע איסוף אשפה (GC) כהכנה למצב קפוא פוטנציאלי.
  • כשמעבדים אפליקציה קפואה, יכול להיות שיתרחשו שלבים נוספים של דחיסת זיכרון, כמו כתיבת דפים מלוכלכים לאחסון גיבוי והחלפת דפים אנונימיים ל-ZRAM.
  • אם כל התהליכים של אפליקציה מסוימת מוקפאים, המערכת מסיימת את כל שקעי ה-TCP הפעילים שהאפליקציה מנהלת. כך נמנעת שליחה של פינגים מסוג TCP keepalive מצד השרת של השקע, שיכולים להפעיל את המודם של המכשיר.

תהליכי אפליקציה במטמון מופשרים כשמצב התהליך שלהם משתנה ממצב במטמון למצב עם חשיבות גבוהה יותר. כדי לצמצם את מספר האירועים של ביטול ההקפאה ב-Android 14 ובגרסאות מתקדמות יותר, המערכת מכניסה לתור שידורים רשומים בהקשר בזמן שהאפליקציה במצב שמור במטמון. שידורים רשומים בהקשר הם מקלטים שאפליקציה רושמת באופן דינמי על ידי קריאה ל-Context.registerReceiver. המערכת מעבירה את השידורים האלה בתור רק אחרי שהאפליקציה מפסיקה להיות קפואה. לעומת זאת, המערכת לא מוסיפה לתור שידורים שמוצהרים במניפסט. שידורים שהוצהרו במניפסט הם מקלטים שהוצהרו באופן סטטי ב-AndroidManifest.xml באמצעות הרכיב <receiver>. המערכת מבטלת את ההקפאה של האפליקציה ששמורה במטמון באופן מיידי כדי להעביר שידורים שהוגדרו במניפסט.

ההשפעה על תקינות המערכת

מערכת Android מפסיקה את התהליך של האפליקציה הכי פחות בשימוש שנשמרה במטמון, אם יש יותר מ-MAX_CACHED_PROCESSES תהליכים של אפליקציות שנשמרו במטמון. במכשירים נתמכים עם Android מגרסה 14 ואילך, הערך של MAX_CACHED_PROCESSES גדל באופן משמעותי, ומאפשר למכשירים לשמור ב-RAM הרבה יותר תהליכי אפליקציות במטמון.

שמירה של יותר אפליקציות במטמון בזיכרון ה-RAM מובילה לירידה של עד 30% בהפעלות במצב התחלתי (cold start), והירידה הזו משתנה בהתאם לנפח הכולל של זיכרון ה-RAM במכשיר. במקביל, צריכת המעבד (CPU) על ידי אפליקציות שנשמרו במטמון מצטמצמת, מה שמוביל לחיסכון משמעותי בסוללה.

החרגות של מקפיאים

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

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

הטמעה של הקפאת אפליקציות

הכלי להקפאת אפליקציות בזיכרון המטמון משתמש בכלי להקפאה cgroup v2 של ליבת המערכת. אפשר להפעיל את התכונה במכשירים שנשלחים עם ליבת מערכת הפעלה תואמת. מפעילים את האפשרות למפתחים השהיית הביצוע של אפליקציות שנשמרו במטמון או מגדירים את הדגל של תצורת המכשיר activity_manager_native_boot use_freezer לערך true. לדוגמה:

adb shell device_config put activity_manager_native_boot use_freezer true && adb reboot

ההקפאה מושבתת כשמגדירים את הדגל use_freezer לערך false או כשמשביתים את האפשרות למפתחים. לדוגמה:

adb shell device_config put activity_manager_native_boot use_freezer false && adb reboot

אפשר לשנות את ההגדרה הזו על ידי שינוי הגדרת המכשיר בגרסת תוכנה או בעדכון.

כדי לבטל את ההגדרה של MAX_CACHED_PROCESSES, למשל כדי להגדיר את הערך ל-1024 לצורך בדיקה:

adb shell device_config put activity_manager max_cached_processes 1024
adb shell device_config set_sync_disabled_for_tests persistent

כדי לבטל את ההגדרה של MAX_CACHED_PROCESSES:

adb shell device_config delete activity_manager max_cached_processes
adb shell device_config set_sync_disabled_for_tests none

החל מ-Android 16 (רמת API‏ 36) ואילך, מערכת Android מספקת ממשקי API ציבוריים רשמיים, כמו IBinder.FrozenStateChangeCallback ו-IBinder.addFrozenStateChangeCallback, כדי לעקוב אחרי מצב ההקפאה או הביטול של הקפאת תהליכים מרוחקים. רכיבים שמתקשרים עם אפליקציות שעשויות להיכנס למטמון יכולים להשתמש בממשקי ה-API האלה כדי לעקוב אחרי מצב ההקפאה של תהליכים מרוחקים.

דרישות לגבי מכשירים וליבת מערכת

הקפאת אפליקציות במטמון דורשת תמיכה ב-cgroup v2 של ליבת המערכת. בנוסף, כדי לקבל הודעות על שינוי מצב ההקפאה באמצעות IBinder.FrozenStateChangeCallback, נדרשת תמיכה במנהל ההתקן של binder בליבה, שזמינה כברירת מחדל בליבות הנפוצות של Android‏ (ACK) ובדימויים של ליבה גנרית (GKI) החל מ-Android 14 (רמת API‏ 34) ואילך.

כדי לבדוק אם מכשיר תומך ביכולות האלה, אפשר להשתמש בפקודות adb רגילות, באופן הבא:

  • בודקים אם ההקפאה מופעלת במכשיר (בגרסאות build של משתמש או לניפוי באגים):

    adb shell device_config get activity_manager_native_boot use_freezer

    או מוודאים שהמערכת מקפיאה תהליכים באופן פעיל:

    adb shell dumpsys activity | grep -A 20 "Apps frozen:"
  • בדיקת התמיכה בבקר ההקפאה של cgroup v2 (לכל מכשיר):

    מוודאים ש-freezer מופיע ברשימת בקרי cgroup v2 הזמינים:

    adb shell cat /sys/fs/cgroup/cgroup.controllers

    לחלופין, במכשיר עם הרשאות בסיס או בגרסת userdebug, מוודאים שצומת ההקפאה של cgroup v2 מותקן ב-cgroup צאצא:

    adb root && adb shell ls /sys/fs/cgroup/uid_0/cgroup.freeze

    אם הקובץ הזה קיים, הליבה תומכת ב-cgroup v2 freezer.

  • בדיקה אם יש תמיכה בהתראות על שינוי מצב ההקפאה (לכל מכשיר):

    במכשירים עם Android מגרסה 14 ואילך עם מנהלי התקנים תואמים של Generic Kernel Image (GKI) binder, IBinder.addFrozenStateChangeCallback מתבצעת הרשמה של קריאות חוזרות בהצלחה. אם מנהל ההתקן של ה-binder הבסיסי של הליבה לא תומך בהתראות על הקפאה, השיטה מחזירה UnsupportedOperationException.

טיפול בתכונות בהתאמה אישית

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

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

מצבי כשל נפוצים

כשמפסיקים את התהליכים של האפליקציה, תקשורת לא תקינה בין תהליכים (IPC) או תזמון משימות עלולים להוביל לסגירה של האפליקציה או להתנהגות לא צפויה.

טרנזקציות סינכרוניות של Binder לתהליכים קפואים

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

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

כדי למנוע את הבעיה הזו, חשוב לוודא שאפליקציות הלקוח משליכות הפניות IBinder מיד אחרי הקריאה ל-Context.unbindService.

ניהול קריאות חוזרות מרחוק לתהליכי לקוח

שירותים ורכיבי מערכת ששומרים על קריאות חוזרות (callback) ארוכות טווח של binder לתהליכי לקוח יכולים למנוע כשלים סינכרוניים והצפות של מאגרים אסינכרוניים על ידי מעקב אחרי מצב הלקוח הקפוא:

  • רישום מאזין לשינוי מצב: משתמשים ב-IBinder.addFrozenStateChangeCallback באסימוני קלסר של לקוחות נכנסים כדי לקבל התראות כשתהליך הלקוח נכנס למקפיא או יוצא ממנו.
  • השהיית השליחה בזמן שהמסך קפוא: כשלקוח נכנס ל-STATE_FROZEN, המערכת משהה את שליחת הקריאות החוזרות או את עדכוני הסטטוס לאותו לקוח.
  • המשך ושליחה אחרי ביטול ההקפאה: כשהלקוח עובר ל-STATE_UNFROZEN, המערכת ממשיכה לשלוח את הקריאות החוזרות ומעבירה את העדכונים הנחוצים שנשלחו בקבוצות או אוחדו.
  • שימוש ב-RemoteCallbackList: שירותי מערכת שמשתמשים ב-RemoteCallbackList יכולים להגדיר מדיניות של callee קפוא כדי להשהות אוטומטית את השליחה של קריאות חוזרות ולחדש אותה בלי לשמור על לוגיקה של מעקב ידני. מידע נוסף זמין במאמר בנושא המלצות להקפאת Binder לשירותי מערכת.

גלישת חוצץ בטרנזקציה אסינכרונית של Binder

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

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

ביצוע חוזר של משימות מתוזמנות אחרי ביטול ההקפאה

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

כדי למנוע גל של ביצועים כשהאפליקציה מפסיקה להיות קפואה, צריך להשתמש ב-scheduleWithFixedDelay במקום ב-scheduleAtFixedRate למשימות ברקע. אפשר גם להשתמש ב-WorkManager.

בדיקה ופתרון בעיות של הקפאת אפליקציות

כדי לוודא שהכלי להקפאת אפליקציות פועל כמו שצריך או כדי לפתור בעיות שקשורות להקפאה, אפשר להשתמש בכלים ובפקודות האבחון הבאים:

פקודות במנהל הפעילויות

אפשר להשתמש בפקודות adb shell am כדי לשלוט באופן ידני בהקפאה ובדחיסה של תהליך ספציפי:

  • כדי להקפיא תהליך:

    adb shell am freeze <process>
  • כדי לבטל את הקיפאון של תהליך:

    adb shell am unfreeze <process>
  • כדי לכפות דחיסה מלאה של הזיכרון בתהליך מסוים:

    adb shell am compact full <process>

בדיקה של Logcat

אפשר לצפות ב-logcat כדי לראות רשומות קפואות ולא קפואות בכל פעם שתהליך עובר אל או יוצא מ-freezer:

adb logcat | grep -i "\(freezing\|froze\)"

הפלט של יומני הסיבות לביטול ההקפאה כולל ערכים ממוספרים מ-enum של UnfreezeReason protocol buffer.

בדיקת קובץ Dumpsys

כדי לבדוק אם יש רשימה של תהליכים קפואים, משתמשים בפקודה dumpsys activity:

adb shell dumpsys activity | grep -A 20 "Apps frozen:"

בודקים אם הקובץ /sys/fs/cgroup/uid_0/cgroup.freeze קיים.

ApplicationExitInfo

כדי לשאול מה הסיבה לסיום תהליך קודם, אפשר לעיין במאמר בנושא ActivityManager.getHistoricalProcessExitReasons. אם תהליך של אפליקציה הסתיים בגלל בעיה שקשורה להקפאה, כמו קבלת טרנזקציית binder סינכרונית בזמן ההקפאה, סיבת היציאה מוגדרת כ-ApplicationExitInfo.REASON_FREEZER.

Perfetto tracing

אירועים שקשורים למקפיא נפלטים למסלול שנקרא Freezer בתהליך system_server בעקבות Perfetto:

  • הפרוסות Freeze ו-Unfreeze מציינות מתי המצב של התהליך משתנה.
  • אירועים מסוג updateAppFreezeStateLSP מוצגים כשהשרת של המערכת בוחן מחדש את תכונות התהליך כדי לקבל החלטות לגבי הקפאה או ביטול הקפאה.

אפשר לבדוק את האירועים האלה ישירות בממשק המשתמש של Perfetto או לנתח אותם באמצעות PerfettoSQL:

INCLUDE PERFETTO MODULE slices.with_context;
SELECT *
FROM process_slice
WHERE process_name = "system_server"
AND track_name = "Freezer"
AND (name LIKE "Freeze %" OR name LIKE "Unfreeze %");

בספרייה הרגילה של PerfettoSQL, אירועי ההקפאה מסוכמים גם בטבלה android_freezer_events.