Minimalizuj wpływ regularnych aktualizacji

Żądania, które aplikacja wysyła do sieci, są główną przyczyną szybkiego zużycia baterii, ponieważ korzystają z radioodbiorników komórkowych i Wi-Fi, co pochłania dużo energii. Oprócz energii potrzebnej do wysyłania i odbierania pakietów te moduły radiowe zużywają dodatkową energię na samo włączenie i utrzymanie stanu aktywności. Nawet tak prosta czynność jak żądanie sieciowe co 15 sekund może sprawić, że moduł radiowy będzie stale włączony, co szybko rozładuje baterię.

Istnieją 3 główne typy regularnych aktualizacji:

  • Inicjowane przez użytkownika. wykonanie aktualizacji na podstawie zachowania użytkownika, np. gestu przeciągnięcia w dół w celu odświeżenia;
  • Inicjowane przez aplikację. Aktualizacja jest przeprowadzana cyklicznie.
  • Inicjowane przez serwer. wykonanie aktualizacji w odpowiedzi na powiadomienie z serwera;

W tym temacie omówimy każdy z tych elementów i przedstawimy dodatkowe sposoby ich optymalizacji pod kątem zmniejszenia zużycia baterii.

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

Żądania inicjowane przez użytkownika zwykle występują w odpowiedzi na jakieś jego zachowanie. Na przykład aplikacja służąca do czytania najnowszych artykułów może umożliwiać użytkownikowi wykonanie gestu przeciągnięcia w dół, aby sprawdzić, czy są nowe artykuły. Aby odpowiadać na żądania inicjowane przez użytkowników, a jednocześnie optymalizować wykorzystanie sieci, możesz używać poniższych technik.

Ograniczanie żądań użytkowników

Możesz zignorować niektóre żądania zainicjowane przez użytkownika, jeśli nie są one potrzebne, np. wielokrotne przeciąganie w dół w krótkim czasie, aby sprawdzić, czy są nowe dane, gdy bieżące dane są nadal aktualne. Reagowanie na każde żądanie może marnować znaczną ilość energii, ponieważ radio jest stale aktywne. Bardziej efektywnym podejściem jest ograniczanie żądań inicjowanych przez użytkownika, tak aby w danym okresie można było wysłać tylko jedno żądanie, co zmniejsza częstotliwość korzystania z modułu radiowego.

Używanie pamięci podręcznej

Przez buforowanie danych aplikacji tworzysz lokalną kopię 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 powinny być przechowywane w pamięci podręcznej tak agresywnie, 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 przez aplikację nieaktualnych danych. Więcej informacji o buforowaniu odpowiedzi sieciowych znajdziesz w artykule Unikanie zbędnego pobierania.

Na Androidzie 11 i nowszych aplikacja może używać tych samych dużych zbiorów danych, których używają inne aplikacje, np. do uczenia maszynowego i odtwarzania multimediów. Gdy aplikacja musi uzyskać dostęp 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.

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

W przypadku połączenia bezprzewodowego większa przepustowość zwykle oznacza większe zużycie baterii, co oznacza, że 5G zwykle zużywa więcej energii niż LTE, które z kolei zużywa więcej energii niż 3G.

Oznacza to, że chociaż podstawowy stan modułu radiowego różni się w zależności od technologii, ogólnie rzecz biorąc, względny wpływ czasu oczekiwania na zmianę stanu na baterię jest większy w przypadku modułów radiowych o większej przepustowości. Więcej informacji o czasie oczekiwania znajdziesz w sekcji Maszyna stanowa modułu radiowego.

Jednocześnie większa przepustowość oznacza, że możesz bardziej agresywnie pobierać dane z wyprzedzeniem, pobierając więcej danych w tym samym czasie. Może to być mniej intuicyjne, ale ponieważ koszt baterii w czasie oczekiwania jest stosunkowo wysoki, bardziej efektywne jest utrzymywanie aktywności radia przez dłuższy czas podczas każdej sesji przesyłania, aby zmniejszyć częstotliwość aktualizacji.

Jeśli na przykład radio LTE ma 2 razy większą przepustowość i 2 razy większe zużycie energii niż 3G, podczas każdej sesji należy pobrać 4 razy więcej danych – potencjalnie nawet 10 MB. Podczas pobierania tak dużej ilości danych należy wziąć pod uwagę wpływ pobierania z wyprzedzeniem na dostępne lokalne miejsce na dane i regularnie czyścić pamięć podręczną pobierania z wyprzedzeniem.

Możesz użyć ConnectivityManager, aby zarejestrować detektor sieci domyślnej, oraz TelephonyManager, aby zarejestrować PhoneStateListener i określić bieżący typ połączenia urządzenia. Gdy poznasz typ połączenia, możesz odpowiednio zmodyfikować procedury pobierania z wyprzedzeniem:

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ę są zwykle wysyłane zgodnie z harmonogramem, np. aplikacja wysyła dzienniki lub dane analityczne do usługi backendu. W przypadku żądań inicjowanych przez aplikację weź pod uwagę ich priorytet, możliwość ich grupowania oraz możliwość ich odroczenia do czasu, gdy urządzenie będzie się ładować lub będzie połączone z siecią bez taryfowego rozliczania danych. Te żądania można zoptymalizować, starannie je planując i korzystając z bibliotek takich jak WorkManager.

Żądania sieciowe zbiorcze

Na urządzeniu mobilnym proces włączania radia, nawiązywania połączenia i utrzymywania radia w stanie aktywnym zużywa dużo energii. Z tego powodu przetwarzanie pojedynczych żądań w losowych momentach może zużywać dużo energii i skracać czas pracy na baterii. Bardziej efektywnym podejściem jest umieszczenie zestawu żądań sieciowych w kolejce i przetworzenie ich razem. Dzięki temu system może ponieść koszt energii związany z włączeniem radia tylko raz, a mimo to uzyskać wszystkie dane, o które prosi aplikacja.

Używanie biblioteki WorkManager

Możesz użyć biblioteki WorkManager, aby 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 na przykład, że masz podklasę Worker o nazwie DownloadHeadlinesWorker, która pobiera najnowsze nagłówki wiadomości. Można zaplanować uruchamianie tego mechanizmu co godzinę, pod warunkiem że urządzenie jest połączone z siecią bez taryfowego rozliczania danych, a bateria nie jest rozładowana. Można też zastosować niestandardową strategię ponawiania prób w przypadku problemów z pobieraniem danych, 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 Androida udostępnia kilka innych narzędzi, które pomagają utworzyć efektywny harmonogram 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 zwykle występują w odpowiedzi na powiadomienie z serwera. Na przykład aplikacja służąca do czytania najnowszych artykułów może otrzymać powiadomienie o nowej partii artykułów, które odpowiadają preferencjom personalizacji 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. Dzięki FCM Twój serwer może powiadomić aplikację działającą na konkretnym urządzeniu, że są dostępne nowe dane.

W porównaniu z odpytywaniem, w którym aplikacja musi regularnie wysyłać pingi do serwera, aby sprawdzić, czy są nowe dane, ten model oparty na zdarzeniach pozwala aplikacji utworzyć nowe połączenie tylko wtedy, gdy wie, że są dane do pobrania. Model minimalizuje niepotrzebne połączenia i zmniejsza opóźnienia podczas aktualizowania informacji w aplikacji.

FCM jest wdrażany przy użyciu trwałego połączenia TCP/IP. Minimalizuje to liczbę stałych połączeń i umożliwia platformie optymalizację przepustowości oraz zminimalizowanie wpływu na czas pracy na baterii.