בקשות שהאפליקציה שולחת לרשת הן גורם משמעותי לניקוז הסוללה, כי הן מפעילות רכיבי רדיו סלולריים או Wi-Fi שצורכים חשמל. מעבר להספק שנדרש לשליחה ולקבלה של מנות נתונים, מכשירי הרדיו האלה צורכים הספק נוסף רק כדי להידלק ולהישאר פעילים. פעולה פשוטה כמו שליחת בקשה לרשת כל 15 שניות יכולה לגרום למכשיר הרדיו הסלולרי לפעול ברציפות ולנצל במהירות את הסוללה.
יש שלושה סוגים כלליים של עדכונים שוטפים:
- בהפעלת המשתמש. ביצוע עדכון על סמך התנהגות מסוימת של המשתמש, כמו משיכה לרענון.
- הפעולה מתבצעת מתוך האפליקציה. ביצוע עדכון על בסיס קבוע.
- הפעולה מתבצעת על ידי השרת. ביצוע עדכון בתגובה להתראה משרת.
במאמר הזה נבחן כל אחד מהגורמים האלה ונציג דרכים נוספות לאופטימיזציה שלהם כדי להפחית את צריכת הסוללה.
אופטימיזציה של בקשות שהמשתמשים יזמו
בקשות שמופעלות על ידי המשתמש מתרחשות בדרך כלל בתגובה להתנהגות מסוימת של המשתמש. לדוגמה, באפליקציה שמשמשת לקריאת מאמרי חדשות עדכניים, יכול להיות שהמשתמש יוכל לבצע משיכה לרענון כדי לבדוק אם יש מאמרים חדשים. אפשר להשתמש בטכניקות הבאות כדי להגיב לבקשות שהמשתמשים יזמו, תוך אופטימיזציה של השימוש ברשת.
הגבלת בקשות משתמשים
יכול להיות שתרצו להתעלם מחלק מהבקשות שהמשתמשים יזמו אם אין בהן צורך, למשל, מחוות מרובות של משיכה לרענון במהלך תקופה קצרה כדי לבדוק אם יש נתונים חדשים, בזמן שהנתונים הנוכחיים עדיין עדכניים. הטיפול בכל בקשה עלול לבזבז כמות משמעותית של חשמל כי הרדיו נשאר פעיל. גישה יעילה יותר היא להגביל את הבקשות שהמשתמשים יוזמים כך שרק בקשה אחת תוכל להישלח במשך תקופה מסוימת, וכך להפחית את תדירות השימוש ברדיו.
שימוש במטמון
כשמטמינים במטמון את הנתונים של האפליקציה, יוצרים עותק מקומי של המידע שהאפליקציה צריכה להתייחס אליו. לאחר מכן, האפליקציה יכולה לגשת כמה פעמים לאותו עותק מקומי של המידע בלי לפתוח חיבור לרשת כדי לשלוח בקשות חדשות.
מומלץ לשמור נתונים במטמון בצורה אגרסיבית ככל האפשר, כולל משאבים סטטיים והורדות לפי דרישה כמו תמונות בגודל מלא. אתם יכולים להשתמש בכותרות של מטמון HTTP כדי לוודא שאסטרטגיית השמירה במטמון לא תגרום לאפליקציה להציג נתונים לא עדכניים. מידע נוסף על שמירת תגובות מהרשת במטמון זמין במאמר בנושא מניעת הורדות מיותרות.
ב-Android מגרסה 11 ואילך, האפליקציה יכולה להשתמש באותם מערכי נתונים גדולים שאפליקציות אחרות משתמשות בהם לתרחישי שימוש כמו למידת מכונה והפעלת מדיה. כשהאפליקציה צריכה לגשת למערך נתונים משותף, היא יכולה קודם לבדוק אם יש גרסה שמורה במטמון לפני שהיא מנסה להוריד עותק חדש. מידע נוסף על מערכי נתונים משותפים זמין במאמר גישה למערכי נתונים משותפים.
שימוש ברוחב פס גדול יותר כדי להוריד יותר נתונים בתדירות נמוכה יותר
כשמתחברים באמצעות רדיו אלחוטי, רוחב פס גבוה יותר בדרך כלל כרוך בצריכת סוללה גבוהה יותר. כלומר, צריכת האנרגיה ב-5G גבוהה יותר מאשר ב-LTE, וצריכת האנרגיה ב-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 Cloud Messaging (FCM) הוא מנגנון קל משקל שמשמש להעברת נתונים משרת למופע ספציפי של אפליקציה. באמצעות FCM, השרת יכול להודיע לאפליקציה שפועלת במכשיר מסוים שיש נתונים חדשים שזמינים לה.
בשיטת הסקר, האפליקציה צריכה לשלוח פינג לשרת באופן קבוע כדי לבדוק אם יש נתונים חדשים. לעומת זאת, במודל מבוסס-האירועים הזה, האפליקציה יוצרת חיבור חדש רק כשהיא יודעת שיש נתונים להורדה. המודל מצמצם את החיבורים המיותרים ומפחית את זמן האחזור כשמעדכנים מידע באפליקציה.
ההטמעה של FCM מתבצעת באמצעות חיבור TCP/IP מתמשך. כך מצמצמים את מספר החיבורים הקבועים ומאפשרים לפלטפורמה לבצע אופטימיזציה של רוחב הפס ולצמצם את ההשפעה על חיי הסוללה.