Минимизируйте эффект от регулярных обновлений

Запросы, которые ваше приложение отправляет в сеть, являются основной причиной разрядки батареи, поскольку они включают энергопотребляющие сотовые или Wi-Fi-модули. Помимо энергии, необходимой для отправки и приема пакетов, эти модули расходуют дополнительную энергию просто на включение и поддержание работы. Даже такой простой запрос к сети, как запрос каждые 15 секунд, может поддерживать работу мобильного модуля непрерывно и быстро расходовать заряд батареи.

Существует три основных типа регулярных обновлений:

  • Инициируется пользователем. Обновление выполняется на основе действий пользователя, например, жеста «потянуть для обновления».
  • Инициировано приложением. Выполняется периодическое обновление.
  • Инициировано сервером. Выполняется обновление в ответ на уведомление от сервера.

В этой статье рассматривается каждый из этих аспектов и обсуждаются дополнительные способы их оптимизации для снижения расхода заряда батареи.

Оптимизация запросов, инициированных пользователем.

Запросы, инициированные пользователем, обычно возникают в ответ на какое-либо действие пользователя. Например, приложение для чтения последних новостных статей может позволять пользователю выполнять жест обновления страницы, чтобы проверить наличие новых статей. Для оптимизации использования сети и реагирования на запросы, инициированные пользователем, можно использовать следующие методы.

Ограничение запросов пользователей

Возможно, вам стоит игнорировать некоторые запросы, инициированные пользователем, если в них нет необходимости, например, многократные жесты обновления страницы в течение короткого промежутка времени для проверки наличия новых данных, пока текущие данные еще актуальны. Обработка каждого запроса может привести к значительным энергозатратам из-за поддержания работы радиомодуля. Более эффективным подходом является ограничение количества запросов, инициированных пользователем, таким образом, чтобы за определенный период времени выполнялся только один запрос, что уменьшит частоту использования радиомодуля.

Используйте кэш

Кэширование данных вашего приложения создает локальную копию информации, к которой приложению необходимо обращаться. Затем приложение может многократно обращаться к одной и той же локальной копии информации, не открывая сетевое соединение для выполнения новых запросов.

Следует максимально активно кэшировать данные, включая статические ресурсы и загрузки по запросу, такие как изображения в полном размере. Для обеспечения того, чтобы ваша стратегия кэширования не приводила к отображению устаревших данных в приложении, можно использовать заголовки HTTP-кэширования. Дополнительную информацию о кэшировании сетевых ответов см. в разделе «Избегайте избыточных загрузок» .

На Android 11 и выше ваше приложение может использовать те же большие наборы данных, что и другие приложения, для таких задач, как машинное обучение и воспроизведение мультимедиа. Когда вашему приложению потребуется доступ к общему набору данных, оно может сначала проверить наличие кэшированной версии, прежде чем пытаться загрузить новую копию. Чтобы узнать больше об общих наборах данных, см. раздел «Доступ к общим наборам данных» .

Используйте большую пропускную способность, чтобы загружать больше данных реже.

При использовании беспроводной связи более высокая пропускная способность обычно достигается за счет более высокого расхода заряда батареи, а это значит, что 5G, как правило, потребляет больше энергии, чем LTE, который, в свою очередь, дороже, чем 3G.

Это означает, что хотя базовое состояние радиомодуля варьируется в зависимости от используемой радиотехнологии, в целом, относительное влияние времени задержки изменения состояния на расход заряда батареи больше для радиомодулей с более высокой пропускной способностью. Для получения дополнительной информации о времени задержки см. раздел «Конечный автомат радиомодуля» .

В то же время, более высокая пропускная способность позволяет более активно осуществлять предварительную загрузку, загружая больше данных за то же время. Возможно, менее очевидно, что из-за относительно более высокой стоимости батареи в режиме ожидания, также эффективнее поддерживать радиоактивность в течение более длительных периодов во время каждой сессии передачи, чтобы уменьшить частоту обновлений.

Например, если LTE-модуль имеет вдвое большую пропускную способность и вдвое большее энергопотребление, чем 3G, вам следует загружать в четыре раза больше данных за каждый сеанс — или потенциально до 10 МБ. При загрузке такого объема данных важно учитывать влияние предварительной загрузки на доступное локальное хранилище и регулярно очищать кэш предварительной загрузки.

Вы можете использовать ConnectivityManager для регистрации прослушивателя для сети по умолчанию, а TelephonyManager для регистрации PhoneStateListener , определяющего текущий тип подключения устройства. После того, как тип подключения станет известен, вы можете соответствующим образом изменить свои процедуры предварительной выборки:

Котлин

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

Оптимизация запросов, инициированных приложением.

Запросы, инициированные приложением, обычно выполняются по расписанию, например, приложение отправляет журналы или аналитические данные в бэкэнд-сервис. При работе с такими запросами следует учитывать их приоритет, возможность объединения в пакеты и отсрочку до момента зарядки устройства или подключения к сети с неограниченным трафиком. Эти запросы можно оптимизировать с помощью тщательного планирования и использования таких библиотек, как WorkManager .

Пакетные сетевые запросы

На мобильном устройстве процесс включения радиосвязи, установления соединения и поддержания её в активном состоянии потребляет большое количество энергии. По этой причине обработка отдельных запросов в случайное время может потреблять значительное количество энергии и сокращать время работы батареи. Более эффективным подходом является постановка в очередь набора сетевых запросов и их одновременная обработка. Это позволяет системе оплачивать затраты энергии на включение радиосвязи только один раз и при этом получать все данные, запрошенные приложением.

Используйте WorkManager

Библиотека WorkManager позволяет эффективно планировать выполнение задач, учитывая выполнение определенных условий, таких как доступность сети и состояние электропитания. Например, предположим, у вас есть подкласс Worker под названием DownloadHeadlinesWorker , который получает последние заголовки новостей. Этот Worker можно запланировать на запуск каждый час при условии, что устройство подключено к сети с неограниченным трафиком и заряд батареи устройства не низкий, с настраиваемой стратегией повторных попыток в случае возникновения проблем с получением данных, как показано ниже:

Котлин

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

Помимо WorkManager, платформа Android предоставляет ряд других инструментов, которые помогут вам создать эффективное расписание для выполнения сетевых задач, таких как опрос. Чтобы узнать больше об использовании этих инструментов, см. Руководство по фоновой обработке .

Оптимизация запросов, инициированных сервером.

Запросы, инициированные сервером, обычно происходят в ответ на уведомление от сервера. Например, приложение для чтения последних новостных статей может получить уведомление о новой партии статей, соответствующих персонализированным настройкам пользователя, после чего оно их загружает.

Отправляйте обновления сервера с помощью Firebase Cloud Messaging.

Firebase Cloud Messaging (FCM) — это легковесный механизм, используемый для передачи данных с сервера на конкретный экземпляр приложения. С помощью FCM ваш сервер может уведомлять ваше приложение, работающее на конкретном устройстве, о наличии новых доступных данных.

В отличие от метода опроса, при котором вашему приложению необходимо регулярно отправлять запросы на сервер для получения новых данных, эта событийно-ориентированная модель позволяет вашему приложению создавать новое соединение только тогда, когда оно знает, что есть данные для загрузки. Модель минимизирует ненужные соединения и уменьшает задержку при обновлении информации внутри вашего приложения.

В FCM используется постоянное TCP/IP-соединение. Это минимизирует количество постоянных соединений и позволяет платформе оптимизировать пропускную способность, а также минимизировать связанное с этим влияние на время работы от батареи.