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

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

העברת הודעות בענן ב-Firebase ‏(FCM) היא מנגנון קל משקל שמשמש להעברת נתונים משרת למופע ספציפי של אפליקציה. באמצעות FCM, השרת יכול להודיע לאפליקציה שפועלת במכשיר מסוים שיש נתונים חדשים שזמינים לה.

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

ההטמעה של FCM מתבצעת באמצעות חיבור TCP/IP קבוע. כך מצמצמים את מספר החיבורים הקבועים ומאפשרים לפלטפורמה לבצע אופטימיזציה של רוחב הפס ולצמצם את ההשפעה על חיי הסוללה.