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

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

מצבי אפליקציה במטמון ומוקפאים

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

מצב שמור במטמון

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

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

לעומת זאת, המצב של תהליך שרת לא קובע את המצב של הלקוחות שלו. לכן, יכול להיות שלשרת יהיו חיבורים של binder ללקוחות, בדרך כלל בצורה של קריאות חוזרות (callbacks), ובעוד שהתהליך המרוחק נמצא במצב שמור במטמון, השרת לא שמור במטמון.

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

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

// in ActivityManager or Context
activityManager.addOnUidImportanceListener(
    new UidImportanceListener() { ... },
    IMPORTANCE_CACHED);

מצב קפוא

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

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

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

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

כדי לעקוב אחרי מקרים שבהם אפליקציות קופאות או מפסיקות לקפוא, משתמשים ב-IBinder.addFrozenStateChangeCallback:

// The binder token of the remote process
IBinder binder = service.getBinder();

// Keep track of frozen state
AtomicBoolean remoteFrozen = new AtomicBoolean(false);

// Update remoteFrozen when the remote process freezes or unfreezes
binder.addFrozenStateChangeCallback(
    myExecutor,
    new IBinder.FrozenStateChangeCallback() {
        @Override
        public void onFrozenStateChanged(boolean isFrozen) {
            remoteFrozen.set(isFrozen);
        }
    });

// When dispatching callbacks to the remote process, pause dispatch if frozen:
if (!remoteFrozen.get()) {
    // dispatch callback to remote process
}

שימוש ב-RemoteCallbackList

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

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

  • FROZEN_CALLEE_POLICY_DROP: קריאות חוזרות (callback) לאפליקציות קפואות מושמטות ללא הודעה. משתמשים במדיניות הזו כשאירועים שקרו בזמן שהאפליקציה הייתה במטמון לא חשובים לאפליקציה, למשל אירועים של חיישנים בזמן אמת.
  • FROZEN_CALLEE_POLICY_ENQUEUE_MOST_RECENT: אם כמה קריאות חוזרות (callback) משודרות בזמן שאפליקציה מוקפאת, רק האחרונה מתווספת לתור ומועברת כשהאפליקציה מפסיקה להיות מוקפאת. האפשרות הזו שימושית לקריאות חוזרות מבוססות-מצב, שבהן רק עדכון המצב האחרון חשוב. לדוגמה, קריאה חוזרת שמודיעה לאפליקציה על עוצמת הקול הנוכחית של המדיה.
  • FROZEN_CALLEE_POLICY_ENQUEUE_ALL: כל הקריאות החוזרות שמשודרות בזמן שהאפליקציה מוקפאת מתווספות לתור ומועברות כשהאפליקציה מפסיקה להיות מוקפאת. חשוב להפעיל שיקול דעת כשמשתמשים במדיניות הזו, כי היא עלולה להוביל להצפת מאגרים אם יותר מדי קריאות חוזרות (callback) מתווספות לתור, או להצטברות של אירועים לא עדכניים.

בדוגמה הבאה מוצג איך ליצור ולהשתמש במופע RemoteCallbackList שמבטל קריאות חוזרות לאפליקציות שהוקפאו:

RemoteCallbackList<IMyCallbackInterface> callbacks =
        new RemoteCallbackList.Builder<IMyCallbackInterface>(
                        RemoteCallbackList.FROZEN_CALLEE_POLICY_DROP)
                .setExecutor(myExecutor)
                .build();

// Registering a callback:
callbacks.register(callback);

// Broadcasting to all registered callbacks:
callbacks.broadcast((callback) -> callback.onSomeEvent(eventData));

אם משתמשים ב-FROZEN_CALLEE_POLICY_DROP, המערכת מפעילה את callback.onSomeEvent() רק אם התהליך שמארח את הקריאה החוזרת לא מוקפא.

שירותי מערכת ואינטראקציות עם אפליקציות

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

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

מעקב אחרי מצבי אפליקציות משירותי מערכת

שירותי מערכת שפועלים ב-system_server או כדמונים מקומיים יכולים גם להשתמש בממשקי ה-API שתוארו קודם כדי לעקוב אחרי החשיבות של תהליכי האפליקציה והמצב שלהם (מוקפאים או לא).

  • ActivityManager.addOnUidImportanceListener: שירותי מערכת יכולים לרשום מאזין כדי לעקוב אחרי שינויים בחשיבות של UID. כשמקבלים שיחה או שיחת חזרה מ-binder מאפליקציה, השירות יכול להשתמש ב-Binder.getCallingUid() כדי לקבל את ה-UID ולשייך אותו למצב החשיבות שמתועד על ידי ה-listener. ההגדרה הזו מאפשרת לשירותי המערכת לדעת אם אפליקציית השיחות נמצאת במצב שמור במטמון.

  • IBinder.addFrozenStateChangeCallback: כששירות מערכת מקבל אובייקט binder מאפליקציה (לדוגמה, כחלק מהרשמה לקריאות חוזרות), הוא צריך לרשום FrozenStateChangeCallback באותו מופע ספציפי של IBinder. ההודעה הזו מועברת ישירות לשירות המערכת כשתהליך האפליקציה שמארח את ה-binder הזה קופא או מפסיק לקפוא.

המלצות לשירותי מערכת

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

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

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