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

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