כשמשתמשים ב-Binder כדי לתקשר בין תהליכים, צריך להיזהר במיוחד כשהתהליך המרוחק נמצא במצב של מטמון או הקפאה. שיחות לאפליקציות שמאוחסנות במטמון או שהוקפאו עלולות לגרום לקריסה שלהן או לצרוך משאבים שלא לצורך.
מצבי אפליקציה במטמון ומוקפאים
מערכת Android שומרת אפליקציות במצבים שונים כדי לנהל משאבי מערכת כמו זיכרון ומעבד.
מצב שמור במטמון
אם לאפליקציה אין רכיבים שגלויים למשתמש, כמו פעילויות או שירותים, אפשר להעביר אותה למצב שמור במטמון. פרטים נוספים זמינים במאמר בנושא תהליכים ומחזור החיים של האפליקציה. אפליקציות שנשמרו במטמון נשמרות בזיכרון למקרה שהמשתמש יחזור אליהן, אבל לא מצופה שהן יפעלו באופן פעיל.
כשמבצעים קישור מתהליך של אפליקציה אחת לתהליך של אפליקציה אחרת, למשל באמצעות bindService, מצב התהליך של תהליך השרת משודרג כך שיהיה לפחות חשוב כמו תהליך הלקוח (אלא אם מצוין Context#BIND_WAIVE_PRIORITY). לדוגמה, אם הלקוח לא נמצא במצב שמור במטמון, גם השרת לא נמצא במצב הזה.
לעומת זאת, המצב של תהליך שרת לא קובע את המצב של הלקוחות שלו. לכן, יכול להיות שלשרת יהיו חיבורים של binder ללקוחות, בדרך כלל בצורה של קריאות חוזרות (callbacks), ובעוד שהתהליך המרוחק נמצא במצב שמור במטמון, השרת לא שמור במטמון.
כשמתכננים ממשקי API שבהם קריאות חוזרות (callback) נוצרות בתהליך עם הרשאות גבוהות ומועברות לאפליקציות, כדאי להשהות את השליחה של הקריאות החוזרות כשאפליקציה נכנסת למצב שמור במטמון, ולחדש את השליחה כשהיא יוצאת מהמצב הזה. כך נמנעת עבודה מיותרת בתהליכי אפליקציות שמאוחסנים בקובץ שמור.
כדי לעקוב אחרי הכניסה של אפליקציות למצב שמור במטמון או היציאה ממנו, משתמשים ב-ActivityManager.addOnUidImportanceListener:
Java
// in ActivityManager or Context
activityManager.addOnUidImportanceListener(
new ActivityManager.OnUidImportanceListener() { ... },
IMPORTANCE_CACHED);
Kotlin
// in ActivityManager or Context
activityManager.addOnUidImportanceListener({ uid, importance ->
// ...
}, IMPORTANCE_CACHED)
מצב קפוא
המערכת יכולה להקפיא אפליקציה שנשמרה במטמון כדי לחסוך במשאבים. כשמבצעים הקפאה של אפליקציה, היא לא מקבלת זמן מעבד ולא יכולה לבצע פעולות. פרטים נוספים זמינים במאמר בנושא הקפאת אפליקציות במטמון.
כשמפעילים תהליך ששולח טרנזקציית binder סינכרונית (לא oneway) לתהליך מרוחק אחר שמוקפא, המערכת משביתה את התהליך המרוחק. הפעולה הזו מונעת את ההשהיה של השרשור שקורא בתהליך הקורא לזמן בלתי מוגבל בזמן ההמתנה להפשרה של התהליך המרוחק, מה שעלול לגרום להרעבה של השרשור או לקיפאון באפליקציה הקוראת.
כשמתבצע תהליך ששולח טרנזקציית Binder אסינכרונית (oneway) לאפליקציה קפואה (בדרך כלל על ידי שליחת הודעה לפונקציית קריאה חוזרת, שהיא בדרך כלל שיטת oneway), הטרנזקציה נשמרת במאגר עד שהתהליך המרוחק מפסיק להיות קפוא. אם יש גלישה על גדות המאגר, תהליך האפליקציה של הנמען עלול לקרוס. בנוסף, יכול להיות שהנתונים של טרנזקציות שנשמרו בזיכרון הזמני יהיו ישנים מדי עד שהתהליך של האפליקציה יופשר ויעבד אותם.
כדי למנוע מצב שבו אפליקציות מוצפות באירועים ישנים או שהמאגרים שלהן מלאים מדי, חובה להשהות את השליחה של קריאות חוזרות (callback) בזמן שהתהליך של אפליקציית היעד קפוא.
כדי לעקוב אחרי מקרים שבהם אפליקציות קופאות או מפסיקות לקפוא, אפשר להשתמש ב-IBinder.addFrozenStateChangeCallback:
Java
// 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
}
Kotlin
// The binder token of the remote process
val binder: IBinder = service.getBinder()
// Keep track of frozen state
val remoteFrozen = AtomicBoolean(false)
// Update remoteFrozen when the remote process freezes or unfreezes
binder.addFrozenStateChangeCallback(myExecutor) { isFrozen ->
remoteFrozen.set(isFrozen)
}
// When dispatching callbacks to the remote process, pause dispatch if frozen:
if (!remoteFrozen.get()) {
// dispatch callback to remote process
}
C++
לקוד פלטפורמה באמצעות ממשקי API של C++ binder:
#include <binder/Binder.h>
#include <binder/IBinder.h>
// The binder token of the remote process
android::sp<android::IBinder> binder = service->getBinder();
// Keep track of frozen state
std::atomic<bool> remoteFrozen = false;
// Define a callback class
class MyFrozenStateCallback : public android::IBinder::FrozenStateChangeCallback {
public:
explicit MyFrozenStateCallback(std::atomic<bool>* frozenState) : mFrozenState(frozenState) {}
void onFrozenStateChanged(bool isFrozen) override {
mFrozenState->store(isFrozen);
}
private:
std::atomic<bool>* mFrozenState;
};
// Update remoteFrozen when the remote process freezes or unfreezes
if (binder != nullptr) {
binder->addFrozenStateChangeCallback(android::sp<android::IBinder::FrozenStateChangeCallback>::make(
new MyFrozenStateCallback(&remoteFrozen)));
}
// When dispatching callbacks to the remote process, pause dispatch if frozen:
if (!remoteFrozen.load()) {
// dispatch callback to remote process
}
שימוש ב-RemoteCallbackList
המחלקות RemoteCallbackList הן מחלקות עזר לניהול רשימות של קריאות חוזרות (callback) מסוג IInterface שתהליכים מרוחקים רושמים. המחלקות האלה מטפלות באופן אוטומטי בהתראות על סגירת binder ומספקות אפשרויות לטיפול בקריאות חוזרות לאפליקציות קפואות.
כשיוצרים RemoteCallbackList, אפשר לציין מדיניות קפואה של הפונקציה שנקראת:
-
FROZEN_CALLEE_POLICY_DROP: קריאות חוזרות לאפליקציות קפואות מושמטות ללא הודעה. כדאי להשתמש במדיניות הזו כשאירועים שקרו בזמן שהאפליקציה הייתה במטמון לא חשובים לאפליקציה, למשל אירועים של חיישנים בזמן אמת. -
FROZEN_CALLEE_POLICY_ENQUEUE_MOST_RECENT: אם מתבצע שידור של כמה קריאות חוזרות בזמן שהאפליקציה מוקפאת, רק הקריאה האחרונה מתווספת לתור ומועברת כשהאפליקציה מפסיקה להיות מוקפאת. האפשרות הזו שימושית לקריאות חוזרות מבוססות-מצב, שבהן רק עדכון המצב האחרון חשוב. לדוגמה, קריאה חוזרת שמודיעה לאפליקציה על עוצמת הקול הנוכחית של המדיה. -
FROZEN_CALLEE_POLICY_ENQUEUE_ALL: כל הקריאות החוזרות שמשודרות בזמן שהאפליקציה מוקפאת מתווספות לתור ומסופקות כשהאפליקציה מפסיקה להיות מוקפאת. חשוב להפעיל שיקול דעת כשמשתמשים במדיניות הזו, כי היא עלולה להוביל להצפת מאגרים אם יותר מדי קריאות חוזרות (callback) מתווספות לתור, או להצטברות של אירועים לא עדכניים.
בדוגמה הבאה מוצג איך ליצור ולהשתמש במופע RemoteCallbackList שמבטל קריאות חוזרות לאפליקציות שהוקפאו:
Java
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));
Kotlin
val callbacks =
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.