מזעור ההשפעה של עדכונים קבועים

הבקשות שהאפליקציה שולחת לרשת הן גורם מרכזי להתרוקנות הסוללה, כי הן מפעילות רכיבי רדיו סלולריים או רכיבי Wi-Fi שצורכים חשמל. בנוסף להספק שנדרש לשליחה ולקבלה של מנות נתונים, מכשירי הרדיו האלה צורכים הספק נוסף רק כדי להידלק ולהישאר פעילים. פעולה פשוטה כמו בקשת רשת כל 15 שניות יכולה להשאיר את הרדיו הסלולרי פועל ברציפות ולגרום לניצול מהיר של הסוללה.

יש שלושה סוגים כלליים של עדכונים שוטפים:

  • פעילויות שהמשתמשים יוזמים. ביצוע עדכון על סמך התנהגות מסוימת של המשתמש, כמו משיכה לרענון.
  • התחלת השיחה באפליקציה. ביצוע עדכון על בסיס קבוע.
  • הפעולה מתבצעת על ידי השרת. מבצעים עדכון בתגובה להתראה משרת.

במאמר הזה נבחן כל אחד מהם ונדון בדרכים נוספות לאופטימיזציה שלהם כדי להפחית את התרוקנות הסוללה.

אופטימיזציה של בקשות שהמשתמשים יזמו

בקשות שמופעלות על ידי משתמש מתרחשות בדרך כלל בתגובה להתנהגות מסוימת של המשתמש. לדוגמה, באפליקציה שמשמשת לקריאת כתבות חדשותיות, יכול להיות שהמשתמש יוכל לבצע תנועת משיכה לרענון כדי לבדוק אם יש כתבות חדשות. אפשר להשתמש בטכניקות הבאות כדי להגיב לבקשות שהמשתמשים יזמו, תוך אופטימיזציה של השימוש ברשת.

ויסות נתונים (throttle) של בקשות משתמשים

יכול להיות שתרצו להתעלם מחלק מהבקשות שהמשתמשים יזמו אם אין בהן צורך, למשל, מחוות מרובות של משיכה למטה לרענון בפרק זמן קצר כדי לבדוק אם יש נתונים חדשים, בזמן שהנתונים הנוכחיים עדיין עדכניים. הטיפול בכל בקשה עלול לבזבז כמות משמעותית של חשמל כי הרדיו נשאר פעיל. גישה יעילה יותר היא להגביל את הבקשות שהמשתמשים יוזמים כך שרק בקשה אחת תוכל להישלח במשך פרק זמן מסוים, וכך להפחית את תדירות השימוש ברדיו.

שימוש במטמון

כשמטמינים במטמון את הנתונים של האפליקציה, יוצרים עותק מקומי של המידע שהאפליקציה צריכה להתייחס אליו. לאחר מכן, האפליקציה יכולה לגשת כמה פעמים לאותו עותק מקומי של המידע בלי לפתוח חיבור לרשת כדי לשלוח בקשות חדשות.

מומלץ לשמור נתונים במטמון בצורה אגרסיבית ככל האפשר, כולל משאבים סטטיים והורדות לפי דרישה כמו תמונות בגודל מלא. אתם יכולים להשתמש בכותרות של מטמון HTTP כדי לוודא שאסטרטגיית השמירה במטמון לא תגרום לאפליקציה להציג נתונים לא עדכניים. מידע נוסף על שמירת תגובות מהרשת במטמון זמין במאמר בנושא מניעת הורדות מיותרות.

ב-Android מגרסה 11 ואילך, האפליקציה יכולה להשתמש באותם מערכי נתונים גדולים שאפליקציות אחרות משתמשות בהם לתרחישי שימוש כמו למידת מכונה והפעלת מדיה. כשהאפליקציה צריכה לגשת למערך נתונים משותף, היא יכולה קודם לבדוק אם יש גרסה שמורה במטמון לפני שהיא מנסה להוריד עותק חדש. מידע נוסף על מערכי נתונים משותפים זמין במאמר גישה למערכי נתונים משותפים.

שימוש ברוחב פס גדול יותר כדי להוריד יותר נתונים בתדירות נמוכה יותר

כשמתחברים באמצעות רדיו אלחוטי, רוחב פס גבוה יותר בדרך כלל כרוך בצריכת סוללה גבוהה יותר. כלומר, צריכת האנרגיה ב-5G גבוהה יותר מזו שב-LTE, שגבוהה יותר מזו שב-3G.

כלומר, למרות שמצב הרדיו הבסיסי משתנה בהתאם לטכנולוגיית הרדיו, באופן כללי, ההשפעה היחסית של זמן ההמתנה של שינוי המצב על הסוללה גדולה יותר במכשירי רדיו עם רוחב פס גבוה יותר. מידע נוסף על זמן השהייה מופיע במאמר בנושא מכונת מצבי הרדיו.

במקביל, רוחב הפס הגבוה יותר מאפשר לבצע שליפה מראש (prefetch) בצורה אגרסיבית יותר, ולהוריד יותר נתונים באותו פרק זמן. אולי באופן פחות אינטואיטיבי, מכיוון שהעלות של הסוללה בזמן ההמתנה גבוהה יחסית, יעיל יותר להשאיר את הרדיו פעיל לפרקי זמן ארוכים יותר במהלך כל סשן העברה כדי להפחית את תדירות העדכונים.

לדוגמה, אם רדיו 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 מתמשך. כך מצמצמים את מספר החיבורים הקבועים ומאפשרים לפלטפורמה לבצע אופטימיזציה של רוחב הפס ולצמצם את ההשפעה על חיי הסוללה.