בקשות שהאפליקציה שולחת לרשת הן גורם מרכזי לניקוז הסוללה, כי הן מפעילות רכיבי רדיו סלולריים או רכיבי Wi-Fi שצורכים חשמל. בנוסף להספק שנדרש לשליחה ולקבלה של מנות מידע, מכשירי הרדיו האלה צורכים הספק נוסף רק כדי להידלק ולהישאר פעילים. פעולה פשוטה כמו בקשת רשת כל 15 שניות יכולה לגרום למכשיר הרדיו הסלולרי לפעול ברציפות ולנצל במהירות את הסוללה.
יש שלושה סוגים כלליים של עדכונים שוטפים:
- בהפעלת המשתמש. ביצוע עדכון על סמך התנהגות מסוימת של המשתמש, כמו משיכה לרענון.
- התחלת השיחה באפליקציה. ביצוע עדכון על בסיס קבוע.
- הפעולה מתבצעת על ידי השרת. מבצעים עדכון בתגובה להתראה משרת.
במאמר הזה נבחן כל אחד מהם ונדון בדרכים נוספות לאופטימיזציה שלהם כדי להפחית את צריכת הסוללה.
אופטימיזציה של בקשות שהמשתמשים יזמו
בקשות שמופעלות על ידי משתמש מתרחשות בדרך כלל בתגובה להתנהגות מסוימת של המשתמש. לדוגמה, באפליקציה שמשמשת לקריאת כתבות חדשותיות, יכול להיות שהמשתמש יוכל לבצע משיכה לרענון כדי לבדוק אם יש כתבות חדשות. אפשר להשתמש בטכניקות הבאות כדי להגיב לבקשות שהמשתמשים יזמו, תוך אופטימיזציה של השימוש ברשת.
ויסות נתונים (throttle) של בקשות משתמשים
יכול להיות שתרצו להתעלם מחלק מהבקשות שהמשתמשים יזמו אם אין בהן צורך, למשל מחוות מרובות של משיכה לרענון בפרק זמן קצר כדי לבדוק אם יש נתונים חדשים, בזמן שהנתונים הנוכחיים עדיין עדכניים. הטיפול בכל בקשה עלול לבזבז כמות משמעותית של חשמל כי הרדיו יישאר פעיל. גישה יעילה יותר היא להגביל את הבקשות שהמשתמשים יוזמים כך שרק בקשה אחת תוכל להישלח במשך פרק זמן מסוים, וכך להפחית את התדירות שבה נעשה שימוש ברדיו.
שימוש במטמון
כשמטמינים במטמון את הנתונים של האפליקציה, יוצרים עותק מקומי של המידע שהאפליקציה צריכה להתייחס אליו. לאחר מכן, האפליקציה יכולה לגשת שוב ושוב לאותו עותק מקומי של המידע בלי לפתוח חיבור לרשת כדי לשלוח בקשות חדשות.
מומלץ לשמור נתונים במטמון בצורה אגרסיבית ככל האפשר, כולל משאבים סטטיים והורדות לפי דרישה כמו תמונות בגודל מלא. אתם יכולים להשתמש בכותרות של מטמון HTTP כדי לוודא שאסטרטגיית השמירה במטמון לא תגרום לאפליקציה להציג נתונים לא עדכניים. מידע נוסף על שמירת תגובות מהרשת במטמון זמין במאמר בנושא מניעת הורדות מיותרות.
ב-Android 11 ואילך, האפליקציה יכולה להשתמש באותם מערכי נתונים גדולים שאפליקציות אחרות משתמשות בהם לתרחישי שימוש כמו למידת מכונה והפעלת מדיה. כשהאפליקציה צריכה לגשת למערך נתונים משותף, היא יכולה קודם לבדוק אם יש גרסה שמורה במטמון לפני שהיא מנסה להוריד עותק חדש. מידע נוסף על מערכי נתונים משותפים זמין במאמר גישה למערכי נתונים משותפים.
שימוש ברוחב פס גדול יותר כדי להוריד יותר נתונים בתדירות נמוכה יותר
כשמתחברים באמצעות רדיו אלחוטי, רוחב פס גבוה יותר בדרך כלל כרוך בצריכת סוללה גבוהה יותר. כלומר, צריכת האנרגיה ב-5G גבוהה יותר מזו שב-LTE, שגבוהה יותר מזו שב-3G.
כלומר, למרות שמצב הרדיו הבסיסי משתנה בהתאם לטכנולוגיית הרדיו, באופן כללי, ההשפעה היחסית של זמן ההמתנה של שינוי המצב על הסוללה גדולה יותר במקרה של מכשירי רדיו עם רוחב פס גבוה יותר. מידע נוסף על זמן השהייה זמין במאמר בנושא מכונת מצבים של הרדיו.
במקביל, רוחב הפס הגבוה יותר מאפשר לבצע אחזור מראש בצורה אגרסיבית יותר, ולהוריד יותר נתונים באותו פרק זמן. אולי באופן פחות אינטואיטיבי, מכיוון שהעלות של הסוללה בזמן ההמתנה גבוהה יחסית, יעיל יותר להשאיר את הרדיו פעיל למשך תקופות ארוכות יותר במהלך כל סשן העברה כדי להפחית את תדירות העדכונים.
לדוגמה, אם רדיו LTE כולל רוחב פס כפול ועלות אנרגיה כפולה של 3G, כדאי להוריד פי ארבעה נתונים במהלך כל סשן – או עד 10MB. כשמורידים כל כך הרבה נתונים, חשוב להביא בחשבון את ההשפעה של האחזור המקדים על האחסון המקומי הזמין ולנקות את מטמון האחזור המקדים באופן קבוע.
אפשר להשתמש ב-ConnectivityManager כדי לרשום מאזין לרשת ברירת המחדל, וב-TelephonyManager כדי לרשום PhoneStateListener כדי לקבוע את סוג החיבור הנוכחי של המכשיר. אחרי שמגלים את סוג החיבור, אפשר לשנות את שגרות האחזור מראש בהתאם:
Kotlin
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val tm = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager private var hasWifi = false private var hasCellular = false private var cellModifier: Float = 1f private val networkCallback = object : ConnectivityManager.NetworkCallback() { // Network capabilities have changed for the network override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities ) { super.onCapabilitiesChanged(network, networkCapabilities) hasCellular = networkCapabilities .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) hasWifi = networkCapabilities .hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } } private val phoneStateListener = object : PhoneStateListener() { override fun onPreciseDataConnectionStateChanged( dataConnectionState: PreciseDataConnectionState ) { cellModifier = when (dataConnectionState.networkType) { TelephonyManager.NETWORK_TYPE_LTE or TelephonyManager.NETWORK_TYPE_HSPAP -> 4f TelephonyManager.NETWORK_TYPE_EDGE or TelephonyManager.NETWORK_TYPE_GPRS -> 1/2f else -> 1f } } private class NetworkState { private var defaultNetwork: Network? = null private var defaultCapabilities: NetworkCapabilities? = null fun setDefaultNetwork(network: Network?, caps: NetworkCapabilities?) = synchronized(this) { defaultNetwork = network defaultCapabilities = caps } val isDefaultNetworkWifi get() = synchronized(this) { defaultCapabilities?.hasTransport(TRANSPORT_WIFI) ?: false } val isDefaultNetworkCellular get() = synchronized(this) { defaultCapabilities?.hasTransport(TRANSPORT_CELLULAR) ?: false } val isDefaultNetworkUnmetered get() = synchronized(this) { defaultCapabilities?.hasCapability(NET_CAPABILITY_NOT_METERED) ?: false } var cellNetworkType: Int = TelephonyManager.NETWORK_TYPE_UNKNOWN get() = synchronized(this) { field } set(t) = synchronized(this) { field = t } private val cellModifier: Float get() = synchronized(this) { when (cellNetworkType) { TelephonyManager.NETWORK_TYPE_LTE or TelephonyManager.NETWORK_TYPE_HSPAP -> 4f TelephonyManager.NETWORK_TYPE_EDGE or TelephonyManager.NETWORK_TYPE_GPRS -> 1 / 2f else -> 1f } } val prefetchCacheSize: Int get() = when { isDefaultNetworkWifi -> MAX_PREFETCH_CACHE isDefaultNetworkCellular -> (DEFAULT_PREFETCH_CACHE * cellModifier).toInt() else -> DEFAULT_PREFETCH_CACHE } } private val networkState = NetworkState() private val networkCallback = object : ConnectivityManager.NetworkCallback() { // Network capabilities have changed for the network override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities ) { networkState.setDefaultNetwork(network, networkCapabilities) } override fun onLost(network: Network?) { networkState.setDefaultNetwork(null, null) } } private val telephonyCallback = object : TelephonyCallback(), TelephonyCallback.PreciseDataConnectionStateListener { override fun onPreciseDataConnectionStateChanged(dataConnectionState: PreciseDataConnectionState) { networkState.cellNetworkType = dataConnectionState.networkType } } connectivityManager.registerDefaultNetworkCallback(networkCallback) telephonyManager.registerTelephonyCallback(telephonyCallback) private val prefetchCacheSize: Int get() { return when { hasWifi -> MAX_PREFETCH_CACHE hasCellular -> (DEFAULT_PREFETCH_CACHE * cellModifier).toInt() else -> DEFAULT_PREFETCH_CACHE } } }
Java
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); private boolean hasWifi = false; private boolean hasCellular = false; private float cellModifier = 1f; private ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() { @Override public void onCapabilitiesChanged( @NonNull Network network, @NonNull NetworkCapabilities networkCapabilities ) { super.onCapabilitiesChanged(network, networkCapabilities); hasCellular = networkCapabilities .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR); hasWifi = networkCapabilities .hasTransport(NetworkCapabilities.TRANSPORT_WIFI); } }; private PhoneStateListener phoneStateListener = new PhoneStateListener() { @Override public void onPreciseDataConnectionStateChanged( @NonNull PreciseDataConnectionState dataConnectionState ) { switch (dataConnectionState.getNetworkType()) { case (TelephonyManager.NETWORK_TYPE_LTE | TelephonyManager.NETWORK_TYPE_HSPAP): cellModifier = 4; Break; case (TelephonyManager.NETWORK_TYPE_EDGE | TelephonyManager.NETWORK_TYPE_GPRS): cellModifier = 1/2.0f; Break; default: cellModifier = 1; Break; } } }; cm.registerDefaultNetworkCallback(networkCallback); tm.listen( phoneStateListener, PhoneStateListener.LISTEN_PRECISE_DATA_CONNECTION_STATE ); public int getPrefetchCacheSize() { if (hasWifi) { return MAX_PREFETCH_SIZE; } if (hasCellular) { return (int) (DEFAULT_PREFETCH_SIZE * cellModifier); } return DEFAULT_PREFETCH_SIZE; }
אופטימיזציה של בקשות שהתחילו באפליקציה
בקשות שמופעלות על ידי אפליקציה מתרחשות בדרך כלל לפי לוח זמנים, למשל אפליקציה ששולחת יומנים או ניתוח נתונים לשירות עורפי. כשמטפלים בבקשות שהופעלו על ידי אפליקציה, צריך לקחת בחשבון את העדיפות של הבקשות האלה, אם אפשר לאגד אותן יחד ואם אפשר לדחות אותן עד שהמכשיר יוטען או יתחבר לרשת ללא הגבלת נפח. אפשר לבצע אופטימיזציה של הבקשות האלה באמצעות תזמון מדויק ושימוש בספריות כמו WorkManager.
בקשות רשת באצווה
במכשיר נייד, התהליך של הפעלת הרדיו, יצירת חיבור ושמירה על הרדיו במצב פעיל צורך כמות גדולה של חשמל. לכן, עיבוד של בקשות בודדות בזמנים אקראיים עלול לצרוך כמות משמעותית של חשמל ולקצר את חיי הסוללה. גישה יעילה יותר היא להוסיף קבוצה של בקשות רשת לתור ולעבד אותן יחד. כך המערכת משלמת את עלות החשמל של הפעלת הרדיו רק פעם אחת, ועדיין מקבלת את כל הנתונים שאפליקציה מבקשת.
שימוש ב-WorkManager
אפשר להשתמש בספרייה WorkManager כדי לבצע עבודה בלוח זמנים יעיל, תוך התחשבות בתנאים ספציפיים, כמו זמינות הרשת וסטטוס ההפעלה. לדוגמה, נניח שיש לכם מחלקת משנה Worker בשם DownloadHeadlinesWorker שמחלצת את הכותרות האחרונות של החדשות. אפשר לתזמן את העובד הזה כך שיפעל כל שעה, בתנאי שהמכשיר מחובר לרשת ללא הגבלת נפח והסוללה של המכשיר לא חלשה. אם יש בעיות באחזור הנתונים, הוא ינסה שוב לפי אסטרטגיית ניסיון חוזר מותאמת אישית, כמו שמוצג בהמשך:
Kotlin
val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) .setRequiresBatteryNotLow(true) .build() val request = PeriodicWorkRequestBuilder<DownloadHeadlinesWorker>(1, TimeUnit.HOURS) .setConstraints(constraints) .setBackoffCriteria(BackoffPolicy.LINEAR, 1L, TimeUnit.MINUTES) .build() WorkManager.getInstance(context).enqueue(request)
Java
Constraints constraints = new Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) .setRequiresBatteryNotLow(true) .build(); WorkRequest request = new PeriodicWorkRequest.Builder(DownloadHeadlinesWorker.class, 1, TimeUnit.HOURS) .setBackoffCriteria(BackoffPolicy.LINEAR, 1L, TimeUnit.MINUTES) .build(); WorkManager.getInstance(this).enqueue(request);
בנוסף ל-WorkManager, פלטפורמת Android מספקת כמה כלים אחרים שיעזרו לכם ליצור תזמון יעיל להשלמת משימות רשת, כמו סקרים. מידע נוסף על השימוש בכלים האלה זמין במדריך לעיבוד ברקע.
אופטימיזציה של בקשות שמופעלות על ידי השרת
בקשות שיוצאות מהשרת מתרחשות בדרך כלל בתגובה להתראה משרת. לדוגמה, אפליקציה שמשמשת לקריאת מאמרים חדשים עשויה לקבל התראה על קבוצה חדשה של מאמרים שתואמים להעדפות ההתאמה האישית של המשתמש, ואז להוריד אותם.
שליחת עדכוני שרת באמצעות העברת הודעות בענן ב-Firebase
העברת הודעות בענן ב-Firebase (FCM) היא מנגנון קל משקל שמשמש להעברת נתונים משרת למופע ספציפי של אפליקציה. באמצעות FCM, השרת יכול להודיע לאפליקציה שפועלת במכשיר מסוים שיש נתונים חדשים שזמינים לה.
במודל מבוסס-האירועים הזה, האפליקציה יוצרת חיבור חדש רק כשהיא יודעת שיש נתונים להורדה. זאת בניגוד למודל מבוסס-הסקרים, שבו האפליקציה צריכה לשלוח פינג לשרת באופן קבוע כדי לבדוק אם יש נתונים חדשים. המודל מצמצם את החיבורים המיותרים ומפחית את זמן האחזור כשמעדכנים מידע באפליקציה.
ההטמעה של FCM מתבצעת באמצעות חיבור TCP/IP קבוע. כך מצטמצם מספר החיבורים הקבועים, והפלטפורמה יכולה לבצע אופטימיזציה של רוחב הפס ולצמצם את ההשפעה על חיי הסוללה.