Minimizar o efeito de atualizações regulares

As solicitações que seu app faz para a rede são uma das principais causas do consumo da bateria, porque ativam os rádios celulares ou Wi-Fi que mais consomem energia. Além da energia necessária para enviar e receber pacotes, esses rádios consomem mais energia apenas para ligar e manter o estado ativo. Algo tão simples quanto uma solicitação de rede a cada 15 segundos pode manter o rádio móvel sempre ligado e esgotar rapidamente a bateria.

Há três tipos gerais de atualizações regulares:

  • Iniciado pelo usuário. Fazer uma atualização com base em algum comportamento do usuário, como um gesto de puxar para atualizar.
  • Iniciada pelo app. Executar uma atualização de forma recorrente.
  • Iniciada pelo servidor. Fazer uma atualização em resposta a uma notificação de um servidor.

Este tópico analisa cada um deles e discute outras maneiras de otimizá-los para reduzir o consumo de bateria.

Otimizar solicitações iniciadas pelo usuário

As solicitações iniciadas pelo usuário geralmente ocorrem em resposta a algum comportamento do usuário. Por exemplo, um app usado para ler os artigos de notícias mais recentes pode permitir que o usuário execute um gesto de puxar para atualizar para verificar se há novos artigos. É possível usar as técnicas a seguir para responder a solicitações iniciadas pelo usuário enquanto otimiza o uso da rede.

Limitar solicitações de usuários

Talvez seja necessário ignorar algumas solicitações iniciadas pelo usuário se não houver necessidade delas, como vários gestos de puxar para atualizar em um curto período de tempo para verificar novos dados enquanto os dados atuais ainda estão atualizados. A ação em cada solicitação pode desperdiçar uma quantidade significativa de energia, mantendo o rádio ativo. Uma abordagem mais eficiente é limitar as solicitações iniciadas pelo usuário para que apenas uma solicitação possa ser feita em um período de tempo, reduzindo a frequência de uso do rádio.

Usar um cache

Ao armazenar em cache os dados do app, você cria uma cópia local das informações que o app precisa referenciar. O app pode acessar a mesma cópia local das informações várias vezes sem precisar abrir uma conexão de rede para fazer novas solicitações.

Armazene dados em cache o mais agressivamente possível, incluindo recursos estáticos e downloads sob demanda, como imagens em tamanho original. É possível usar cabeçalhos de cache HTTP para garantir que a estratégia de armazenamento em cache não resulte em dados desatualizados no app. Para mais informações sobre o armazenamento em cache de respostas de rede, consulte Evitar downloads redundantes.

No Android 11 e versões mais recentes, seu app pode usar os mesmos grandes conjuntos de dados que outros apps usam para casos de uso como aprendizado de máquina e reprodução de mídia. Quando o app precisa acessar um conjunto de dados compartilhado, ele pode verificar primeiro se há uma versão em cache antes de tentar fazer o download de uma nova cópia. Para saber mais sobre conjuntos de dados compartilhados, consulte Acessar conjuntos de dados compartilhados.

Usar uma maior largura de banda para fazer o download de mais dados com menos frequência

Quando o dispositivo está conectado por um rádio sem fio, uma largura de banda maior geralmente tem um maior custo de bateria. Isso significa que o 5G normalmente consome mais energia do que o LTE, que tem um custo maior que o 3G.

Isso significa que, embora o estado de rádio subjacente varie de acordo com a tecnologia de rádio, de maneira geral, o impacto relativo na bateria causado pelo tempo de cauda da mudança de estado é maior para rádios com largura de banda mais alta. Para mais informações sobre o tempo de espera, consulte A máquina de estado de rádio.

Ao mesmo tempo, a maior largura de banda permite uma pré-busca mais agressiva, fazendo o download de mais dados de uma só vez. Talvez menos intuitivamente, porque o custo da bateria no tempo de cauda é relativamente maior, e também é mais eficiente manter o rádio ativo por períodos mais longos durante cada sessão de transferência para reduzir a frequência das atualizações.

Por exemplo, se um rádio LTE tiver o dobro da largura de banda e o dobro do custo de energia do 3G, você precisará fazer o download de quatro vezes mais dados durante cada sessão, ou até 10 MB. Ao fazer o download de tantos dados, é importante considerar o efeito da pré-busca no armazenamento local disponível e limpar o cache de pré-busca regularmente.

É possível usar o ConnectivityManager para registrar um listener para a rede padrão e o TelephonyManager para registrar um PhoneStateListener para determinar o tipo de conexão do dispositivo atual. Depois que o tipo de conexão for conhecido, modifique suas rotinas de pré-busca conforme necessário:

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

Otimizar solicitações iniciadas pelo app

As solicitações iniciadas pelo app geralmente ocorrem em uma programação, como um app que envia registros ou análises para um serviço de back-end. Ao lidar com solicitações iniciadas pelo app, considere a prioridade delas, se podem ser agrupadas e se podem ser adiadas até que o dispositivo esteja carregando ou conectado a uma rede ilimitada. Essas solicitações podem ser otimizadas com uma programação cuidadosa e usando bibliotecas como o WorkManager.

Solicitações de rede em lote

Em um dispositivo móvel, o processo de ligar o rádio, estabelecer uma conexão e manter o rádio ativo consome uma grande quantidade de energia. Por isso, o processamento de solicitações individuais em momentos aleatórios pode consumir energia de maneira significativa e reduzir a duração da bateria. Uma abordagem mais eficiente é enfileirar um conjunto de solicitações de rede e processá-las juntas. Isso permite que o sistema pague o custo de energia para ligar o rádio apenas uma vez e ainda assim receba todos os dados solicitados por um app.

Usar o WorkManager

É possível usar a biblioteca WorkManager para executar o trabalho em uma programação eficiente que considera se condições específicas são atendidas, como disponibilidade de rede e status de energia. Por exemplo, suponha que você tenha uma subclasse Worker chamada DownloadHeadlinesWorker que extrai as manchetes de notícias mais recentes. Esse worker pode ser programado para ser executado a cada hora, desde que o dispositivo esteja conectado a uma rede sem medição e a bateria do dispositivo não esteja fraca, com uma estratégia de nova tentativa personalizada se houver algum problema ao recuperar os dados, conforme mostrado abaixo:

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

Além do WorkManager, a plataforma Android oferece várias outras ferramentas para ajudar você a criar uma programação eficiente para concluir tarefas de rede, como pesquisas. Para saber mais sobre o uso dessas ferramentas, consulte o Guia para processamento em segundo plano.

Otimizar solicitações iniciadas pelo servidor

As solicitações iniciadas pelo servidor geralmente ocorrem em resposta a uma notificação de um servidor. Por exemplo, um app usado para ler os artigos de notícias mais recentes pode receber uma notificação sobre um novo lote de artigos que se encaixam nas preferências de personalização do usuário, que são baixadas.

Enviar atualizações de servidor com o Firebase Cloud Messaging

O Firebase Cloud Messaging (FCM) é um mecanismo leve usado para transmitir dados de um servidor para uma instância de app específica. Usando o FCM, o servidor pode notificar seu app em execução em um dispositivo específico de que há novos dados disponíveis para ele.

Comparado à pesquisa, em que seu app precisa dar um ping regularmente no servidor para consultar novos dados, esse modelo orientado por eventos permite que o app crie uma nova conexão apenas quando souber que há dados para transferir por download. O modelo minimiza conexões desnecessárias e reduz a latência ao atualizar informações no seu app.

O FCM é implementado usando uma conexão TCP/IP persistente. Isso minimiza o número de conexões persistentes e permite que a plataforma otimize a largura de banda e minimize o impacto associado na duração da bateria.