Запросы, которые ваше приложение отправляет в сеть, являются основной причиной разряда батареи, поскольку они активируют энергоёмкие сотовые или 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 } } }
Ява
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)
Ява
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. Это минимизирует количество постоянных соединений и позволяет платформе оптимизировать пропускную способность, минимизируя влияние на время работы батареи.