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

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

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

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

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