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