Minimalizuj wpływ regularnych aktualizacji

Prośby wysyłane przez aplikację do sieci są główną przyczyną wyczerpywania się baterii, ponieważ powodują włączanie radiotelefonów komórkowych lub Wi-Fi, które zużywają dużo energii. Oprócz energii potrzebnej do wysyłania i odbierania pakietów te urządzenia zużywają dodatkową energię na włączanie i utrzymywanie w stanie gotowości. Nawet proste żądanie sieci co 15 sekund może spowodować ciągłe włączanie radia w urządzeniu mobilnym i szybkie zużycie energii z baterii.

Istnieją 3 ogólne typy regularnych aktualizacji:

  • Inicjowane przez użytkownika. wykonywanie aktualizacji na podstawie pewnych działań użytkownika, takich jak gest przeciągania w dół w celu odświeżenia;
  • Inicjowane przez aplikację. Przeprowadzanie aktualizacji cyklicznie.
  • Inicjowane przez serwer. Przeprowadzanie aktualizacji w odpowiedzi na powiadomienie z serwera.

W tym artykule omawiamy każdy z tych elementów i podajemy dodatkowe sposoby na optymalizację ich działania w celu zmniejszenia zużycia baterii.

Optymalizacja żądań inicjowanych przez użytkownika

Żądania inicjowane przez użytkownika zwykle występują w odpowiedzi na jego działania. Na przykład aplikacja używana do czytania najnowszych wiadomości może umożliwić użytkownikowi wykonanie gestu „przeciągnij, by odświeżyć” w celu sprawdzenia dostępności nowych artykułów. Aby odpowiadać na żądania inicjowane przez użytkownika, jednocześnie optymalizując korzystanie z sieci, możesz użyć tych technik.

ograniczanie liczby żądań użytkowników,

Możesz zignorować niektóre inicjowane przez użytkownika żądania, jeśli nie są potrzebne, np. wielokrotne gesty przeciągania w dół w celu odświeżenia, wykonywane w krótkim czasie, aby sprawdzić, czy nie ma nowych danych, gdy aktualne dane są jeszcze świeże. Wykonywanie każdego żądania może spowodować marnowanie dużej mocy, ponieważ nie pozwalałoby na wybudzenie radia. Bardziej wydajnym podejściem jest ograniczanie żądań inicjowanych przez użytkownika, tak aby w danym przedziale czasu można było wysłać tylko jedno żądanie, co ogranicza częstotliwość korzystania z radia.

Korzystanie z pamięci podręcznej

Buforowanie danych aplikacji powoduje utworzenie lokalnej kopii informacji, do których aplikacja musi się odwoływać. Aplikacja może wielokrotnie uzyskiwać dostęp do tej samej lokalnej kopii informacji bez konieczności nawiązywania połączenia z siecią w celu wysyłania nowych żądań.

Dane, w tym zasoby statyczne i materiały do pobrania na żądanie, takie jak obrazy w pełnym rozmiarze, najlepiej przechowywać w pamięci podręcznej. Możesz używać nagłówków pamięci podręcznej HTTP, aby mieć pewność, że Twoja strategia dotycząca pamięci podręcznej nie spowoduje wyświetlania przez aplikację nieaktualnych danych. Więcej informacji o przechowywaniu odpowiedzi sieci w pamięci podręcznej znajdziesz w artykule Unikanie zbędnych pobrań.

W Androidzie 11 i nowszych Twoja aplikacja może korzystać z tych samych dużych zbiorów danych, których używają inne aplikacje do takich zastosowań jak uczenie maszynowe czy odtwarzanie multimediów. Gdy aplikacja potrzebuje dostępu do współdzielonego zbioru danych, może najpierw sprawdzić, czy istnieje wersja w pamięci podręcznej, zanim spróbuje pobrać nową kopię. Więcej informacji o udostępnionych zbiorach danych znajdziesz w artykule Dostęp do udostępnionych zbiorów danych.

Użyj większej przepustowości, aby rzadziej pobierać więcej danych

W przypadku połączenia przez łącze bezprzewodowe większa przepustowość wiąże się zwykle z większym zużyciem energii przez baterię, co oznacza, że 5G zużywa zwykle więcej energii niż LTE, a to z kolei jest droższe niż 3G.

Oznacza to, że chociaż stan radia różni się w zależności od technologii radiowej, ogólnie rzecz biorąc wpływ czasu trwania opóźnienia po zmianie stanu jest większy w przypadku urządzeń radiowych o większej przepustowości. Więcej informacji o czasie oczekiwania znajdziesz w artykule Automat stanów radiowych (w języku angielskim).

Jednocześnie większa przepustowość oznacza, że możesz szybciej pobierać dane wstępne, pobierając więcej danych w tym samym czasie. Może mniej intuicyjnie, ponieważ koszt baterii w czasie trwania sesji przesyłania jest stosunkowo wyższy, więc korzystniej jest też utrzymywać radio aktywne przez dłuższy czas podczas każdej sesji przesyłania, aby zmniejszyć częstotliwość aktualizacji.

Jeśli na przykład radio LTE ma dwukrotnie większą przepustowość i dwa razy wyższe koszty energii niż 3G, podczas każdej sesji należy pobrać 4 razy więcej danych, czyli potencjalnie nawet 10 MB. Podczas pobierania tak dużej ilości danych należy wziąć pod uwagę wpływ wstępnego pobierania na dostępnej pamięci lokalnej i regularnie opróżniać pamięć podręczną.

Za pomocą ConnectivityManager możesz zarejestrować odbiornik w sieci domyślnej, a TelephonyManager do zarejestrowania PhoneStateListener, aby określić bieżący typ połączenia urządzenia. Gdy poznasz typ połączenia, możesz odpowiednio zmodyfikować swoje 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 aplikację

Żądania inicjowane przez aplikację zwykle występują zgodnie z harmonogramem, np. aplikacja wysyła logi lub analizy do usługi backendu. Gdy zajmujesz się żądaniami inicjowanymi przez aplikację, zastanów się nad ich priorytetem, zastanów się, czy mogą one być grupowane i czy można je odroczyć do czasu ładowania urządzenia lub połączenia z siecią bez pomiaru. Te żądania można zoptymalizować, stosując ostrożne harmonogramowanie i korzystając z bibliotek takich jak WorkManager.

Zbiorcze żądania sieciowe

W przypadku urządzenia mobilnego proces włączania radia, nawiązywania połączenia i utrzymywania aktywności radia wymaga dużej mocy obliczeniowej. Dlatego przetwarzanie poszczególnych żądań w losowych momentach może znacznie zużywać energię i skracać czas pracy na baterii. Skuteczniejszym sposobem jest umieszczenie w kolejce żądań sieciowych i przetwarzanie ich razem. Dzięki temu system pokryje koszt energii, który wystarczy do jednego włączenia radia, nie rezygnując z pobierania wszystkich danych żądanych przez aplikację.

Używanie WorkManagera

Korzystając z biblioteki WorkManager, możesz wykonywać zadania zgodnie z efektywnym harmonogramem, który uwzględnia spełnienie określonych warunków, takich jak dostępność sieci i stan zasilania. Załóżmy np., że masz podklasę Worker o nazwie DownloadHeadlinesWorker, która pobiera najnowsze nagłówki wiadomości. Można go zaplanować tak, aby działał co godzinę, pod warunkiem że urządzenie jest połączone z nielimitowaną siecią, a bateria nie jest prawie wyczerpana. W razie problemów z pobieraniem danych można użyć niestandardowej strategii ponownego próby, 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 też kilka innych narzędzi, które ułatwiają tworzenie wydajnego harmonogramu wykonywania zadań sieciowych, takich jak odpytywanie. Więcej informacji o korzystaniu z tych narzędzi znajdziesz w przewodniku po przetwarzaniu w tle.

Optymalizacja żądań inicjowanych przez serwer

Żądania inicjowane przez serwer występują zwykle w odpowiedzi na powiadomienie z serwera. Na przykład aplikacja służąca do czytania najnowszych wiadomości może otrzymać powiadomienie o nowej partii artykułów, które pasują do preferencji personalizacji użytkownika, i może je pobrać.

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

Komunikacja w chmurze Firebase (FCM) to prosty mechanizm służący do przesyłania danych z serwera do konkretnej instancji aplikacji. Za pomocą FCM serwer może powiadomić aplikację działającą na konkretnym urządzeniu, że są dla niej dostępne nowe dane.

W odróżnieniu od pollingu, w którym aplikacja musi regularnie wysyłać pingi do serwera, aby zapytać o nowe dane, ten model oparty na zdarzeniach pozwala aplikacji nawiązywać nowe połączenia tylko wtedy, gdy wie, że są dostępne dane do pobrania. Model minimalizuje niepotrzebne połączenia i zwiększa szybkość aktualizacji informacji w aplikacji.

Usługa FCM jest implementowana przy użyciu trwałego połączenia TCP/IP. Pozwala to zminimalizować liczbę trwałych połączeń i umożliwić platformie optymalizację przepustowości oraz zminimalizowanie powiązanego wpływu na czas pracy na baterii.