Réduire l'impact des mises à jour régulières

Les requêtes que votre application envoie au réseau sont l'une des principales causes de décharge de la batterie, car elles activent les radios cellulaires ou Wi-Fi qui consomment de l'énergie. Au-delà de la puissance nécessaire pour envoyer et recevoir des paquets, ces radios consomment de l'énergie supplémentaire juste pour s'allumer et rester éveillées. Une simple requête réseau toutes les 15 secondes peut maintenir la radio mobile allumée en permanence et épuiser rapidement la batterie.

Il existe trois types généraux de mises à jour régulières :

  • Déclenchement par l'utilisateur Effectuer une mise à jour en fonction du comportement de l'utilisateur, par exemple un geste de tirer pour actualiser.
  • Initié par l'application Effectuer une mise à jour de façon récurrente.
  • Initié par le serveur : Effectuer une mise à jour en réponse à une notification d'un serveur.

Cet article examine chacun de ces éléments et présente d'autres façons de les optimiser pour réduire la décharge de la batterie.

Optimiser les requêtes déclenchées par l'utilisateur

Les requêtes initiées par l'utilisateur se produisent généralement en réponse à un comportement de l'utilisateur. Par exemple, une application utilisée pour lire les derniers articles d'actualités peut permettre à l'utilisateur d'effectuer un geste de tirage vers le bas pour rechercher de nouveaux articles. Vous pouvez utiliser les techniques suivantes pour répondre aux requêtes initiées par l'utilisateur tout en optimisant l'utilisation du réseau.

Limiter les requêtes utilisateur

Vous pouvez ignorer certaines requêtes déclenchées par l'utilisateur si elles ne sont pas nécessaires, par exemple plusieurs gestes d'actualisation par balayage sur une courte période pour vérifier si de nouvelles données sont disponibles alors que les données actuelles sont encore récentes. Répondre à chaque requête pourrait gaspiller une quantité importante d'énergie en maintenant la radio éveillée. Une approche plus efficace consiste à limiter les requêtes initiées par l'utilisateur afin qu'une seule requête puisse être effectuée sur une période donnée, ce qui réduit la fréquence d'utilisation de la radio.

Utiliser un cache

En mettant en cache les données de votre application, vous créez une copie locale des informations auxquelles votre application doit faire référence. Votre application peut ensuite accéder plusieurs fois à la même copie locale des informations sans avoir à ouvrir de connexion réseau pour effectuer de nouvelles requêtes.

Vous devez mettre en cache les données de la manière la plus agressive possible, y compris les ressources statiques et les téléchargements à la demande tels que les images en taille réelle. Vous pouvez utiliser des en-têtes de cache HTTP pour vous assurer que votre stratégie de mise en cache n'entraîne pas l'affichage de données obsolètes dans votre application. Pour en savoir plus sur la mise en cache des réponses réseau, consultez Éviter les téléchargements redondants.

Sous Android 11 ou version ultérieure, votre application peut utiliser les mêmes grands ensembles de données que d'autres applications pour des cas d'utilisation tels que le machine learning et la lecture de contenus multimédias. Lorsque votre application a besoin d'accéder à un ensemble de données partagé, elle peut d'abord rechercher une version mise en cache avant de tenter de télécharger une nouvelle copie. Pour en savoir plus sur les ensembles de données partagés, consultez Accéder aux ensembles de données partagés.

Utiliser une bande passante plus importante pour télécharger plus de données moins souvent

Lorsqu'une connexion sans fil est utilisée, une bande passante plus élevée s'accompagne généralement d'une consommation de batterie plus importante. Cela signifie que la 5G consomme généralement plus d'énergie que la LTE, qui est à son tour plus gourmande que la 3G.

Cela signifie que, bien que l'état radio sous-jacent varie en fonction de la technologie radio, en règle générale, l'impact relatif sur la batterie du temps de latence du changement d'état est plus important pour les radios à bande passante plus élevée. Pour en savoir plus sur le temps de queue, consultez La machine à états de la radio.

En même temps, la bande passante plus élevée signifie que vous pouvez précharger plus agressivement, en téléchargeant plus de données sur la même période. De manière peut-être moins intuitive, comme le coût de la batterie en fin de transfert est relativement plus élevé, il est également plus efficace de maintenir la radio active pendant de plus longues périodes lors de chaque session de transfert pour réduire la fréquence des mises à jour.

Par exemple, si une radio LTE a une bande passante et un coût énergétique deux fois plus élevés que la 3G, vous devez télécharger quatre fois plus de données à chaque session, soit potentiellement jusqu'à 10 Mo. Lorsque vous téléchargez autant de données, il est important de tenir compte de l'effet de la prélecture sur l'espace de stockage local disponible et de vider régulièrement le cache de prélecture.

Vous pouvez utiliser ConnectivityManager pour enregistrer un écouteur pour le réseau par défaut et TelephonyManager pour enregistrer un PhoneStateListener afin de déterminer le type de connexion actuel de l'appareil. Une fois le type de connexion connu, vous pouvez modifier vos routines de préchargement en conséquence :

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

Optimiser les requêtes déclenchées par l'application

Les requêtes initiées par l'application se produisent généralement selon une programmation, par exemple lorsqu'une application envoie des journaux ou des données analytiques à un service de backend. Lorsque vous traitez des requêtes initiées par l'application, tenez compte de leur priorité, de la possibilité de les regrouper et de la possibilité de les différer jusqu'à ce que l'appareil soit en charge ou connecté à un réseau non facturé à l'usage. Ces requêtes peuvent être optimisées grâce à une planification minutieuse et à l'aide de bibliothèques telles que WorkManager.

Regrouper les requêtes réseau par lot

Sur un appareil mobile, le processus d'activation de la radio, d'établissement d'une connexion et de maintien de la radio en veille consomme beaucoup d'énergie. C'est pourquoi le traitement de requêtes individuelles à des moments aléatoires peut consommer beaucoup d'énergie et réduire l'autonomie de la batterie. Une approche plus efficace consiste à mettre en file d'attente un ensemble de requêtes réseau et à les traiter ensemble. Cela permet au système de payer le coût énergétique de l'activation de la radio une seule fois, tout en obtenant toutes les données demandées par une application.

Utiliser WorkManager

Vous pouvez utiliser la bibliothèque WorkManager pour effectuer des tâches selon un calendrier efficace qui tient compte de conditions spécifiques, telles que la disponibilité du réseau et l'état de l'alimentation. Par exemple, supposons que vous disposiez d'une sous-classe Worker appelée DownloadHeadlinesWorker qui récupère les derniers titres de l'actualité. Ce worker peut être planifié pour s'exécuter toutes les heures, à condition que l'appareil soit connecté à un réseau non limité et que la batterie de l'appareil ne soit pas faible, avec une stratégie de réessai personnalisée en cas de problème de récupération des données, comme indiqué ci-dessous :

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

En plus de WorkManager, la plate-forme Android fournit plusieurs autres outils pour vous aider à créer une planification efficace pour l'exécution des tâches réseau, telles que l'interrogation. Pour en savoir plus sur l'utilisation de ces outils, consultez le guide sur le traitement en arrière-plan.

Optimiser les requêtes déclenchées par le serveur

Les requêtes initiées par le serveur se produisent généralement en réponse à une notification d'un serveur. Par exemple, une application utilisée pour lire les derniers articles d'actualités peut recevoir une notification concernant un nouveau lot d'articles correspondant aux préférences de personnalisation de l'utilisateur, qu'elle télécharge ensuite.

Envoyer des mises à jour de serveur avec Firebase Cloud Messaging

Firebase Cloud Messaging (FCM) est un mécanisme simple utilisé pour transmettre des données d'un serveur à une instance d'application spécifique. Avec FCM, votre serveur peut informer votre application exécutée sur un appareil spécifique que de nouvelles données sont disponibles.

Contrairement au polling, où votre application doit interroger régulièrement le serveur pour obtenir de nouvelles données, ce modèle basé sur les événements permet à votre application de créer une nouvelle connexion uniquement lorsqu'elle sait qu'il y a des données à télécharger. Le modèle minimise les connexions inutiles et réduit la latence lors de la mise à jour des informations dans votre application.

FCM est implémenté à l'aide d'une connexion TCP/IP persistante. Cela réduit le nombre de connexions persistantes et permet à la plate-forme d'optimiser la bande passante et de minimiser l'impact associé sur l'autonomie de la batterie.