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

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

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

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

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

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

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

ויסות מספר הבקשות של המשתמשים

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

שימוש במטמון

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

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

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

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

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

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

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

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

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