Żą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.