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

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

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

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

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