Minimalizuj wpływ regularnych aktualizacji

Żądania wysyłane przez aplikację do sieci są główną przyczyną rozładowywania baterii, ponieważ włączają energochłonne moduły komórkowe lub Wi-Fi. Oprócz energii potrzebnej do wysyłania i odbierania pakietów te radia zużywają dodatkową energię tylko na włączenie i utrzymanie aktywności. Nawet tak prosta czynność jak wysyłanie żądania sieci co 15 sekund może powodować ciągłe włączanie radia komórkowego i szybkie zużywanie baterii.

Wyróżniamy 3 rodzaje regularnych aktualizacji:

  • Rozwijanie inicjowane przez użytkownika Przeprowadzanie aktualizacji na podstawie zachowania użytkownika, np. gestu przeciągnięcia w dół w celu odświeżenia.
  • Zainicjowane przez aplikację wykonywać aktualizację cyklicznie;
  • Inicjowane przez serwer Przeprowadzanie aktualizacji w odpowiedzi na powiadomienie z serwera.

W tym artykule omówimy każdy z tych elementów i przedstawimy dodatkowe sposoby ich optymalizacji, aby zmniejszyć zużycie baterii.

Optymalizowanie żądań inicjowanych przez użytkowników

Żądania inicjowane przez użytkownika zwykle pojawiają się w odpowiedzi na jego działanie. Na przykład aplikacja do czytania najnowszych artykułów może umożliwiać użytkownikowi wykonanie gestu przeciągnięcia w dół, aby sprawdzić, czy nie pojawiły się nowe artykuły. Aby odpowiadać na żądania zainicjowane przez użytkownika i jednocześnie optymalizować wykorzystanie sieci, możesz zastosować te techniki:

Ograniczanie liczby żądań użytkowników

Możesz zignorować niektóre żądania zainicjowane przez użytkownika, jeśli nie są potrzebne, np. wielokrotne gesty przeciągnięcia w dół w krótkim czasie w celu sprawdzenia nowych danych, gdy bieżące dane są nadal aktualne. Obsługa każdego żądania może powodować znaczne zużycie energii, ponieważ radio pozostaje aktywne. Bardziej efektywnym podejściem jest ograniczenie żądań inicjowanych przez użytkownika, tak aby w określonym czasie można było wysłać tylko jedno żądanie, co zmniejsza częstotliwość korzystania z radia.

Używanie pamięci podręcznej

Pamięć podręczna danych aplikacji to lokalna kopia informacji, do których aplikacja musi się odwoływać. Aplikacja może wtedy wielokrotnie uzyskiwać dostęp do tej samej lokalnej kopii informacji bez konieczności otwierania połączenia sieciowego w celu wysyłania nowych żądań.

Dane należy buforować tak często, jak to możliwe, w tym zasoby statyczne i pobierane na żądanie, takie jak obrazy w pełnym rozmiarze. Możesz używać nagłówków pamięci podręcznej HTTP, aby mieć pewność, że strategia buforowania nie spowoduje wyświetlania w aplikacji nieaktualnych danych. Więcej informacji o buforowaniu odpowiedzi sieciowych znajdziesz w artykule Unikanie zbędnych pobrań.

Na Androidzie 11 i nowszych aplikacja może używać tych samych dużych zbiorów danych co inne aplikacje w przypadku zastosowań takich jak uczenie maszynowe i odtwarzanie multimediów. Gdy aplikacja potrzebuje dostępu do udostępnionego zbioru danych, może najpierw sprawdzić, czy jest dostępna wersja w pamięci podręcznej, zanim spróbuje pobrać nową kopię. Więcej informacji o udostępnionych zbiorach danych znajdziesz w artykule Uzyskiwanie dostępu do udostępnionych zbiorów danych.

Wykorzystywanie większej przepustowości do rzadszego pobierania większej ilości danych

W przypadku połączenia radiowego większa przepustowość zwykle wiąże się z większym zużyciem baterii. Oznacza to, że 5G zazwyczaj zużywa więcej energii niż LTE, które z kolei jest bardziej energochłonne niż 3G.

Oznacza to, że chociaż stan radia zależy od technologii radiowej, ogólnie rzecz biorąc, względny wpływ na baterię czasu potrzebnego na zmianę stanu jest większy w przypadku radia o większej przepustowości. Więcej informacji o czasie oczekiwania znajdziesz w artykule The radio state machine (w języku angielskim).

Jednocześnie większa przepustowość oznacza, że możesz bardziej agresywnie pobierać z wyprzedzeniem więcej danych w tym samym czasie. Może się to wydawać mniej oczywiste, ale ponieważ koszt baterii w końcowej fazie jest stosunkowo wyższy, bardziej efektywne jest utrzymywanie aktywnego radia przez dłuższy czas podczas każdej sesji przesyłania, aby zmniejszyć częstotliwość aktualizacji.

Jeśli np. radio LTE ma dwukrotnie większą przepustowość i dwukrotnie większy koszt energii niż 3G, podczas każdej sesji należy pobrać 4 razy więcej danych, czyli nawet 10 MB. Podczas pobierania tak dużej ilości danych ważne jest, aby wziąć pod uwagę wpływ wstępnego pobierania na dostępną pamięć lokalną i regularnie czyścić pamięć podręczną wstępnego pobierania.

Możesz użyć ConnectivityManager, aby zarejestrować odbiornik domyślnej sieci, oraz TelephonyManager, aby zarejestrować PhoneStateListener, który określa bieżący typ połączenia urządzenia. Gdy typ połączenia jest znany, możesz odpowiednio zmodyfikować procedury wstępnego pobierania:

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;
}

Optymalizowanie żądań inicjowanych przez aplikacje

Żądania inicjowane przez aplikację są zwykle wysyłane zgodnie z harmonogramem, np. aplikacja wysyła dzienniki lub dane analityczne do usługi backendu. W przypadku żądań inicjowanych przez aplikacje weź pod uwagę ich priorytet, możliwość łączenia ich w pakiety oraz możliwość odroczenia ich do czasu, gdy urządzenie będzie się ładować lub będzie połączone z siecią bez limitu danych. Te żądania można zoptymalizować, starannie planując ich wykonanie i korzystając z bibliotek takich jak WorkManager.

Zbiorcze żądania sieciowe

Na urządzeniu mobilnym włączenie radia, nawiązanie połączenia i utrzymanie radia w stanie aktywności zużywa dużo energii. Z tego powodu przetwarzanie poszczególnych żądań w losowych momentach może zużywać dużo energii i skracać czas pracy na baterii. Bardziej efektywnym podejściem jest umieszczenie w kolejce zestawu żądań sieciowych i przetworzenie ich razem. Dzięki temu system płaci za włączenie radia tylko raz, a aplikacja nadal otrzymuje wszystkie potrzebne dane.

Korzystanie z WorkManagera

Możesz użyć biblioteki WorkManager, aby wykonywać zadania zgodnie z harmonogramem, który uwzględnia spełnienie określonych warunków, takich jak dostępność sieci i stan zasilania. Załóżmy na przykład, że masz podklasę Worker o nazwie DownloadHeadlinesWorker, która pobiera najnowsze nagłówki wiadomości. To zadanie może być wykonywane co godzinę, pod warunkiem że urządzenie jest połączone z siecią bez limitu danych i bateria urządzenia nie jest bliska rozładowania. W przypadku problemów z pobieraniem danych można zastosować niestandardową strategię ponawiania, jak pokazano poniżej:

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);

Oprócz WorkManagera platforma Android udostępnia kilka innych narzędzi, które pomagają tworzyć efektywne harmonogramy wykonywania zadań sieciowych, takich jak odpytywanie. Więcej informacji o korzystaniu z tych narzędzi znajdziesz w przewodniku po przetwarzaniu w tle.

Optymalizowanie żądań inicjowanych przez serwer

Żądania inicjowane przez serwer są zwykle wysyłane w odpowiedzi na powiadomienie z serwera. Na przykład aplikacja do czytania najnowszych artykułów z wiadomościami może otrzymać powiadomienie o nowej partii artykułów, które pasują do preferencji użytkownika, a następnie je pobrać.

Wysyłanie aktualizacji serwera za pomocą Komunikacji w chmurze Firebase

Komunikacja w chmurze Firebase (FCM) to lekki mechanizm służący do przesyłania danych z serwera do konkretnej instancji aplikacji. Za pomocą FCM serwer może powiadamiać aplikację działającą na konkretnym urządzeniu o dostępności nowych danych.

W porównaniu z odpytywaniem, w przypadku którego aplikacja musi regularnie wysyłać pingi do serwera, aby wysyłać zapytania o nowe dane, ten model oparty na zdarzeniach umożliwia aplikacji tworzenie nowego połączenia tylko wtedy, gdy wie, że są dane do pobrania. Model minimalizuje niepotrzebne połączenia i skraca czas oczekiwania podczas aktualizowania informacji w aplikacji.

FCM jest wdrażana za pomocą trwałego połączenia TCP/IP. Minimalizuje to liczbę trwałych połączeń i umożliwia platformie optymalizację przepustowości oraz zmniejszenie związanego z tym wpływu na żywotność baterii.