קריאות ל-Android API כוללות בדרך כלל זמן אחזור וחישוב משמעותיים לכל קריאה. לכן, אחסון במטמון בצד הלקוח הוא שיקול חשוב בתכנון ממשקי API מועילים, נכונים ובעלי ביצועים טובים.
מוטיבציה
ממשקי API שחשופים למפתחי אפליקציות ב-Android SDK מוטמעים בדרך כלל כקוד לקוח ב-Android Framework, שמבצע קריאה של Binder IPC לשירות מערכת בתהליך פלטפורמה, שתפקידו לבצע חישוב מסוים ולהחזיר תוצאה ללקוח. בדרך כלל, שלושה גורמים עיקריים משפיעים על זמן האחזור של הפעולה הזו:
- תקורה של IPC: זמן האחזור של קריאה בסיסית ל-IPC הוא בדרך כלל פי 10,000 מזמן האחזור של קריאה בסיסית לשיטה בתוך תהליך.
- תחרות בצד השרת: יכול להיות שהעבודה שבוצעה בשירות המערכת בתגובה לבקשת הלקוח לא תתחיל באופן מיידי, למשל אם שרשור של שרת עסוק בטיפול בבקשות אחרות שהגיעו מוקדם יותר.
- חישוב בצד השרת: יכול להיות שהעבודה עצמה לטיפול בבקשה בשרת תדרוש עבודה משמעותית.
אפשר למנוע את כל שלושת הגורמים האלה של זמן האחזור על ידי הטמעת מטמון בצד הלקוח, בתנאי שהמטמון:
- נכון: המטמון בצד הלקוח אף פעם לא מחזיר תוצאות שיהיו שונות מהתוצאות שהשרת היה מחזיר.
- יעילה: לעיתים קרובות בקשות של לקוחות מוגשות מהמטמון, למשל אם שיעור ההיטים במטמון גבוה.
- יעילה: המטמון בצד הלקוח מנצל ביעילות את המשאבים בצד הלקוח, למשל על ידי ייצוג נתונים שנשמרו במטמון בצורה קומפקטית, ועל ידי אי-אחסון של יותר מדי תוצאות שנשמרו במטמון או נתונים לא עדכניים בזיכרון של הלקוח.
כדאי לשקול לשמור את תוצאות השרת במטמון בלקוח
אם לקוחות שולחים לעיתים קרובות את אותה בקשה כמה פעמים, והערך המוחזר לא משתנה לאורך זמן, כדאי להטמיע מטמון בספריית הלקוח לפי מפתחות של פרמטרים של בקשות.
כדאי להשתמש ב-IpcDataCache
בהטמעה:
public class BirthdayManager {
private final IpcDataCache.QueryHandler<User, Birthday> mBirthdayQuery =
new IpcDataCache.QueryHandler<User, Birthday>() {
@Override
public Birthday apply(User user) {
return mService.getBirthday(user);
}
};
private static final int BDAY_CACHE_MAX = 8; // Maximum birthdays to cache
private static final String BDAY_API = "getUserBirthday";
private final IpcDataCache<User, Birthday> mCache
new IpcDataCache<User, Birthday>(
BDAY_CACHE_MAX, MODULE_SYSTEM, BDAY_API, BDAY_API, mBirthdayQuery);
/** @hide **/
@VisibleForTesting
public static void clearCache() {
IpcDataCache.invalidateCache(MODULE_SYSTEM, BDAY_API);
}
public Birthday getBirthday(User user) {
return mCache.query(user);
}
}
דוגמה מלאה זמינה במאמר android.app.admin.DevicePolicyManager
.
IpcDataCache
זמין לכל קוד המערכת, כולל מודולים ראשיים.
יש גם את PropertyInvalidatedCache
, שהוא כמעט זהה אבל גלוי רק למסגרת. כשהדבר אפשרי, עדיף להשתמש ב-IpcDataCache
.
ביטול התוקף של מטמון בשינויים בצד השרת
אם הערך שהוחזר מהשרת עשוי להשתנות לאורך זמן, צריך להטמיע קריאה חוזרת (callback) כדי לעקוב אחרי השינויים, ולרשום קריאה חוזרת כדי לבטל את תוקף המטמון בצד הלקוח בהתאם.
ביטול תוקף של מטמון בין מקרי בדיקת יחידה
בחבילת בדיקות יחידה, אפשר לבדוק את קוד הלקוח באמצעות בדיקה כפולה (double) במקום באמצעות השרת האמיתי. אם כן, חשוב לנקות את המטמון בצד הלקוח בין תרחישים של בדיקות. כך מקרי הבדיקה יהיו הרמטיים יחסית זה לזה, ולא יפריעו זה לזה.
@RunWith(AndroidJUnit4.class)
public class BirthdayManagerTest {
@Before
public void setUp() {
BirthdayManager.clearCache();
}
@After
public void tearDown() {
BirthdayManager.clearCache();
}
...
}
כשכותבים בדיקות CTS שמפעילות לקוח API שמשתמש במטמון באופן פנימי, המטמון הוא פרט הטמעה שלא נחשף למחבר ה-API. לכן, בדיקות CTS לא צריכות לדרוש ידע מיוחד כלשהו לגבי מטמון שמשמש בקוד הלקוח.
ניתוח היטים והחמצומים במטמון
משתמשים עם הרשאות IpcDataCache
ו-PropertyInvalidatedCache
יכולים להדפיס נתונים סטטיסטיים בזמן אמת:
adb shell dumpsys cacheinfo
...
Cache Name: cache_key.is_compat_change_enabled
Property: cache_key.is_compat_change_enabled
Hits: 1301458, Misses: 21387, Skips: 0, Clears: 39
Skip-corked: 0, Skip-unset: 0, Skip-bypass: 0, Skip-other: 0
Nonce: 0x856e911694198091, Invalidates: 72, CorkedInvalidates: 0
Current Size: 1254, Max Size: 2048, HW Mark: 2049, Overflows: 310
Enabled: true
...
שדות
היטים:
- הגדרה: מספר הפעמים שנתון נדרש נמצא במטמון.
- משמעות: מדד שמציין אחזור נתונים יעיל ומהיר, שמפחית את אחזור הנתונים הלא הכרחי.
- בדרך כלל, עדיף לקבל מספרים גבוהים יותר.
הפעולה מנקה:
- הגדרה: מספר הפעמים שהמטמון נמחק בגלל ביטול התוקף.
- סיבות לביטול:
- ביטול תוקף: נתונים מיושנים מהשרת.
- ניהול מקום: פנוי מקום לנתונים חדשים כשהמטמון מלא.
- מספרים גבוהים עשויים להצביע על נתונים שמשתנים לעיתים קרובות ועל חוסר יעילות פוטנציאלי.
החמצות:
- הגדרה: מספר הפעמים שבהן המטמון לא הצליח לספק את הנתונים המבוקשים.
- גורמים:
- אחסון בזיכרון מטמון לא יעיל: נפח הזיכרון המטמון קטן מדי או שהוא לא מאחסן את הנתונים הנכונים.
- נתונים שמשתנים לעיתים קרובות.
- בקשות בפעם הראשונה.
- אם המספרים גבוהים, יכול להיות שיש בעיות שקשורות לאחסון במטמון.
דילוגים:
- הגדרה: מקרים שבהם לא נעשה שימוש במטמון בכלל, למרות שאפשר היה לעשות זאת.
- סיבות לדילוג:
- סתימה: ספציפית לעדכונים של Android Package Manager, השבתה מכוונת של שמירת נתונים במטמון בגלל כמות גדולה של קריאות במהלך האתחול.
- לא מוגדר: המטמון קיים אבל לא אותחל. המזהה החד-פעמי לא הוגדר, כלומר המטמון מעולם לא בוטל.
- עקיפה: החלטה מכוונת לדלג על המטמון.
- מספרים גבוהים מצביעים על חוסר יעילות פוטנציאלי בשימוש במטמון.
הפעולה מבטלת את התוקף של:
- הגדרה: התהליך של סימון נתונים שנשמרו במטמון כמיושנים או לא עדכניים.
- משמעות: האות הזה מצביע על כך שהמערכת פועלת עם הנתונים העדכניים ביותר, וכך מונעת שגיאות ואי-עקביות.
- בדרך כלל מופעל על ידי השרת שבבעלותו הנתונים.
הגודל הנוכחי:
- הגדרה: מספר הפריטים הנוכחי ששמורים במטמון.
- משמעות: מדד שמציין את ניצול המשאבים של המטמון ואת ההשפעה הפוטנציאלית שלו על ביצועי המערכת.
- בדרך כלל, ככל שהערך גבוה יותר, כך המטמון משתמש בזיכרון גדול יותר.
גודל מקסימלי:
- הגדרה: נפח האחסון המקסימלי שהוקצה למטמון.
- משמעות: קובעת את הקיבולת של המטמון ואת היכולת שלו לאחסן נתונים.
- הגדרת גודל מקסימלי מתאים עוזרת לאזן בין יעילות המטמון לבין השימוש בזיכרון. כשמגיעים לגודל המקסימלי, המערכת מוציאה את הרכיב שלא נעשה בו שימוש לאחרונה כדי להוסיף רכיב חדש. הדבר עשוי להצביע על חוסר יעילות.
High Water Mark:
- הגדרה: הגודל המקסימלי שאליו הגיעה המטמון מאז היצירה שלו.
- משמעות: הנתון הזה מספק תובנות לגבי השימוש המקסימלי במטמון ועומס פוטנציאלי על הזיכרון.
- מעקב אחרי נקודת השיא יכול לעזור לכם לזהות אזורים שבהם יש צווארי בקבוק או אזורים שבהם אפשר לבצע אופטימיזציה.
מלאי יתר:
- הגדרה: מספר הפעמים שבהן המטמון חרג מהגודל המקסימלי שלו והיה צריך להוציא נתונים כדי לפנות מקום לערכים חדשים.
- משמעות: המדד הזה מציין לחץ במטמון וירידה פוטנציאלית בביצועים כתוצאה מהוצאת נתונים.
- אם מספרי ה-overflow גבוהים, יכול להיות שצריך לשנות את גודל המטמון או לבדוק מחדש את שיטת האחסון במטמון.
אפשר למצוא את אותם נתונים סטטיסטיים גם בדוח על באג.
שינוי הגודל של המטמון
למטמון יש גודל מקסימלי. כשמגיעים לגודל המקסימלי של המטמון, המערכת מסירה את הרשומות לפי סדר LRU.
- שמירת מעט מדי רשומות במטמון עלולה להשפיע לרעה על שיעור ההיטים במטמון.
- שמירת יותר מדי רשומות במטמון מגבירה את השימוש בזיכרון של המטמון.
מוצאים את האיזון המתאים לתרחיש לדוגמה.
ביטול קריאות לקוח מיותרות
לקוחות עשויים לשלוח את אותה שאילתה לשרת כמה פעמים בפרק זמן קצר:
public void executeAll(List<Operation> operations) throws SecurityException {
for (Operation op : operations) {
for (Permission permission : op.requiredPermissions()) {
if (!permissionChecker.checkPermission(permission, ...)) {
throw new SecurityException("Missing permission " + permission);
}
}
op.execute();
}
}
כדאי להשתמש שוב בתוצאות של קריאות קודמות:
public void executeAll(List<Operation> operations) throws SecurityException {
Set<Permission> permissionsChecked = new HashSet<>();
for (Operation op : operations) {
for (Permission permission : op.requiredPermissions()) {
if (!permissionsChecked.add(permission)) {
if (!permissionChecker.checkPermission(permission, ...)) {
throw new SecurityException(
"Missing permission " + permission);
}
}
}
op.execute();
}
}
כדאי לשקול שימוש בצד הלקוח בזיכרון של תגובות קודמות מהשרת
אפליקציות לקוח עשויות לשלוח שאילתות ל-API בקצב מהיר יותר מהיכולת של שרת ה-API ליצור תשובות חדשות בעלות משמעות. במקרה כזה, גישה יעילה היא לשמור בזיכרון את התגובה האחרונה של השרת שמוצגת בצד הלקוח, יחד עם חותמת זמן, ולהחזיר את התוצאה השמורה בזיכרון בלי לשלוח שאילתה לשרת אם התוצאה השמורה בזיכרון עדכנית מספיק. מחבר הלקוח של ה-API יכול לקבוע את משך הזמן של שמירת המידע בזיכרון.
לדוגמה, אפליקציה עשויה להציג למשתמש נתונים סטטיסטיים של תעבורת הרשת על ידי שליחת שאילתה לגבי הנתונים הסטטיסטיים בכל פריים שמצויר:
@UiThread
private void setStats() {
mobileRxBytesTextView.setText(
Long.toString(TrafficStats.getMobileRxBytes()));
mobileRxPacketsTextView.setText(
Long.toString(TrafficStats.getMobileRxPackages()));
mobileTxBytesTextView.setText(
Long.toString(TrafficStats.getMobileTxBytes()));
mobileTxPacketsTextView.setText(
Long.toString(TrafficStats.getMobileTxPackages()));
}
האפליקציה עשויה לצייר פריימים ב-60 Hz. אבל באופן היפותטי, קוד הלקוח ב-TrafficStats
עשוי לבחור לשלוח לשרת שאילתה לגבי נתונים סטטיסטיים לכל היותר פעם בשנייה, ואם נשלחת שאילתה תוך שנייה משאילתה קודמת, הוא יחזיר את הערך האחרון שנצפה.
מותר לעשות זאת כי במסמכי התיעוד של ה-API לא מצוין משהו לגבי רעננות התוצאות שמוחזרות.
participant App code as app
participant Client library as clib
participant Server as server
app->clib: request @ T=100ms
clib->server: request
server->clib: response 1
clib->app: response 1
app->clib: request @ T=200ms
clib->app: response 1
app->clib: request @ T=300ms
clib->app: response 1
app->clib: request @ T=2000ms
clib->server: request
server->clib: response 2
clib->app: response 2
כדאי להשתמש ביצירת קוד בצד הלקוח במקום בשאילתות לשרת
אם תוצאות השאילתה ידועות לשרת בזמן ה-build, כדאי לבדוק אם הן ידועות גם ללקוח בזמן ה-build, ואם אפשר להטמיע את ה-API לגמרי בצד הלקוח.
כדאי להשתמש בקוד האפליקציה הבא כדי לבדוק אם המכשיר הוא שעון (כלומר, אם פועלת בו מערכת Wear OS):
public boolean isWatch(Context ctx) {
PackageManager pm = ctx.getPackageManager();
return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
}
המאפיין הזה של המכשיר ידוע בזמן ה-build, במיוחד בזמן יצירת ה-Framework לתמונת האתחול של המכשיר הזה. הקוד מצד הלקוח של hasSystemFeature
יכול להחזיר תוצאה ידועה באופן מיידי, במקום לשלוח שאילתה לשירות המערכת המרוחק PackageManager
.
ביטול כפילויות של קריאות חזרה מהשרת בלקוח
לבסוף, לקוח ה-API יכול לרשום קריאות חזרה (callbacks) לשרת ה-API כדי לקבל התראות על אירועים.
בדרך כלל אפליקציות רושמות מספר קריאות חזרה לאותו מידע בסיסי. במקום שהשרת יעדכן את הלקוח פעם לכל קריאה חוזרת רשומה באמצעות IPC, בספריית הלקוח צריכה להיות קריאה חוזרת רשומה אחת באמצעות IPC עם השרת, ולאחר מכן צריך לעדכן כל קריאה חוזרת רשומה באפליקציה.
digraph d_front_back {
rankdir=RL;
node [style=filled, shape="rectangle", fontcolor="white" fontname="Roboto"]
server->clib
clib->c1;
clib->c2;
clib->c3;
subgraph cluster_client {
graph [style="dashed", label="Client app process"];
c1 [label="my.app.FirstCallback" color="#4285F4"];
c2 [label="my.app.SecondCallback" color="#4285F4"];
c3 [label="my.app.ThirdCallback" color="#4285F4"];
clib [label="android.app.FooManager" color="#F4B400"];
}
subgraph cluster_server {
graph [style="dashed", label="Server process"];
server [label="com.android.server.FooManagerService" color="#0F9D58"];
}
}