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

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

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

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

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

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

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

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

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

שימוש במטמון

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

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

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

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

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

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

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

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

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

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

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