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

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

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

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

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

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

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

צמצום בקשות של משתמשים

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

שימוש במטמון

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

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

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

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

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

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

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

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

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

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

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