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

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

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

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

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

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

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

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

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

שימוש במטמון

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

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

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

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

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

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

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

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

אפשר להשתמש ConnectivityManager להרשמה את ה-listener של רשת ברירת המחדל, 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 (FCM) הוא מודל פשוט מנגנון המשמש לשידור נתונים משרת למופע מסוים של אפליקציה. באמצעות FCM, השרת יכול להודיע לאפליקציה שפועלת במכשיר מסוים יש נתונים חדשים זמינים עבורו.

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

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