Как в Android запрещали фоновую работу и улучшали расход батареи
Когда-то работа в фоне была простой. Теперь в Android есть множество ограничений: работа в фоне, доступ к файловой системе, разрешения, на которые надо получить одобрение модераторов Google Play, и другие. Со всеми ими разработчикам приходится работать. Недавно я выступил с докладом на эту тему на конференции DUMP, а теперь написал текст на его основе для Хабра. Давайте вспомним, с чего все начиналось и как менялись требования с каждой новой версией Android.
Если вам интересно следить за самыми последними новостями Android разработки и получать подборку интересных статей по этой тематике, тогда вам стоит подписаться на Телеграм-канал Android Broadcast и мой YouTube канал «Android Broadcast»
Что было вначале
До Android 5.0 у нас был следующий набор инструментов, чтобы выполнять задачи в фоне:
Alarm Manager. Инструмент, который позволяет поставить будильник в системе и получать уведомления.
Broadcast Intent. Уведомления о событиях, которые происходят в системе. Например, о новых сообщениях.
Service — Background, Foreground, Bound.
Sync Adapter. Специальная древняя штука, которая связана с менеджментом аккаунтов для синхронизации данных. Яркий пример: при настройке Google-аккаунта в Android можно настроить синхронизацию календаря, контактов и других сервисов. Под капотом она работает через Sync Adapter. До наших дней инструмент не дожил.
Download Manager. Утилита, которая позволяет загружать файлы. Она не очень умная, но простая и удобная. Google Play, например, именно ею файлы и загружает.
Проблемы появились, потому что разработчики не соблюдали правило Человека-паука: «С большими возможностями для разработчиков приходит ответственность за скорость работы системы». Google с первых же версий Android дал им огромные возможности, но не предупредил, что они работают в системе, которая несет ответственность за качество.
Когда у пользователей что-то ломалось, они винили не сторонних разработчиков, а говорили: «Android тормозит, вот на IPhone все быстро». Получалось, что разработчики хорошие, а Google плохой.
В Google решили с этим бороться, но для этого нужно было найти «врагов», влияющих на качество работы приложений. Ими оказались:
Дилемма «размер батареи против габаритов устройства». Все хотят тонкие устройства, которые мало весят и приятно ложатся в руку. При этом всем нужна большая и мощная батарея. С этим врагом бороться трудно.
Экран. Все хотят высокую яркость и цветопередачу, но экран — основное место, куда тратится энергия смартфона.
Оптимизация прошивки под железо устройства. То, чем занимаются вендоры.
Сторонние разработчики. То есть мы с вами.
Вопрос, как продать крутой смартфон. У крутого смартфона мощное железо, а оно ест много энергии. Продать крутой смартфон с бюджетным процессором невозможно. Нужно поставить такой, что греется, как печка, и сумасшедше тратит энергию, но зато все делает быстро.
Маркетинг, который старается обмануть наши ожидания. Выпуская новое поколение смартфонов, производители говорят: мы перешли на четыре нанометра, на три, на два, поэтому у нас повысилась энергоемкость. Она повысилась, но все, что сэкономили на расходе электроэнергии, мы отдали новым транзисторам. И в итоге только увеличили энергопотребление.
Android 5.0
Google все это надоело, и там решили, что на сторонних разработчиков полагаться нельзя, ведь это самые большие пираты в экосистеме Android. И тогда появился Project Volta.
Суть проекта заключалась в том, чтобы дать новые инструменты для выполнения работы и операций в ключе энергоэффективности. То есть не давать разработчикам делать лишнего, чтобы не есть слишком много энергии. Так началась большая оптимизация, о которой мы и поговорим.
JobScheduler API. Новый системный сервис, единая точка входа для выполнения большинства фоновых операций. Также новый сервис позволил задавать комфортные условия для выполнения операций. Например, операция совершается, когда устройство подключено к зарядке, не используется владельцем или есть доступ к сети с заданными требованиями.
Самое интересное: JobScheduler не использовался в Android 5.0, потому что был жутко забагованный. Заработал он только в Android 6.0.
Battery Saver, вторая важная новинка. До этого похожие механизмы были у вендоров как собственные фичи. Теперь инструмент стал стандартизированной частью системы. Вот что он привнес:
Уменьшение частоты процессора.
Уменьшение частоты обновления дисплея.
Ограничение потребления данных в фоне.
Другие оптимизации от вендора. А если вендор может добавить что угодно, фича практически уничтожена.
Android 6.0
Doze Mode. В этой версии Android начинаются серьезные подвижки, и режим Doze Mode — первая из них. Идея в том, чтобы отключать все, что не нужно, когда устройством не пользуются.
Как система понимает, что нужно включить Doze Mode? Сначала было так: если пользователь кладет телефон и не использует его, можно отключить сетевой доступ и другие ненужные функции. Например, режим включался, пока владелец смартфона спит. Google заявляла, что Nexus 5 в состоянии Doze Mode поедал всего 1% зарядки за ночь. Правда, я такого никогда не видел.
Все это дало свои плоды. Кроме того, что Doze Mode отключает максимум функций, появляется такая штука, как maintenance window. Это время, когда устройство выходит из Doze Mode и позволяет разработчику что-то сделать или проверить. И тут снова важную роль начинает играть JobScheduler, потому что все контролируется через него.
Примерная схема «включения» maintenance windows
App Standby. Появился второй режим экономии электроэнергии — приостановка активности приложений, которые человек не использует. Если мы сворачиваем приложение и не пользуемся им, на него накладываются ограничения. Они не такие жесткие, как в Doze Mode, но мешают ходить в сеть и снижают частоту срабатываний. В чем конкретно заключаются ограничения, Google, как всегда, не уточняет, потому что вендор может сделать что угодно.
FCM High Priority — тип пушей, способных разбудить устройство в Doze Mode и App Standby. И это важная шутка, но нужно помнить, что, если сделать все пуши высокоприоритетными, они не обязательно дойдут. Часть из них рано или поздно начнет игнорироваться. Поэтому использовать их нужно только на действительно важные события. Например, когда пользователю приходит сообщение в чате, в котором он включил уведомления.
Android 7.0
Doze on the Go, вторая версия Doze Mode. Если раньше устройство приходило в Doze Mode, только когда было неподвижным какое-то время, то теперь достаточно выключить экран и положить смартфон в карман.
В Android 7.0 мы получили немного другую картину с maintenance window. Окна стали появляться чаще, ограничения стали мягче. Но приложения по-прежнему не могли нормально работать в фоне.
Project Svelte. Второй проект по сокращению расхода оперативной памяти и оптимизации способов работы приложений в фоне.
No Broadcasts. Первое, что сделали в рамках проекта, — убрали все бродкасты. Начали с CONNECTIVITY_ACTION. Он стал первым и самым важным, потому что этот бродкаст рассылается на любое изменение сети. Например, когда пользователь переходит с мобильной сети на Wi-Fi. А в Android есть особенность: даже если приложение сейчас убито, но бродкаст-ресивер подписан на какой-то бродкаст, система поднимет приложение, чтобы доставить его.
Представьте, что такое приложение у нас одно. Поднять процесс — недешевая операция, но в целом не страшная. А если таких приложений 20? Каждая смена будет приводить к тому, что процессы будут подниматься и убиваться 20 раз. Хотя и это не так страшно. Страшно, что CONNECTIVITY_ACTION вызывается не только при уходе с сети на Wi-Fi, но и, например, с 3G на 4G. А ведь тип соединения скачет постоянно при прогулках по городу — представьте, сколько событий рассылается системе. Поэтому его отключили первым.
ACTION_NEW_PICTURE. Затем отключили рассылку новых картинок и видео. Не критичная штука, потому что это можно делать и другим путем, например через JobScheduler. Появилась возможность реакции на изменения в контент-провайдер. Можно передать какой-то Uri с ContentProvider и, когда произойдет оповещение об изменении, вызвать джобу.
Android 8.0
NoBroadcasts+, запрет на все системные бродкасты. Либо разработчику это не нужно, либо он может подписаться на них, когда приложение работает. Делать это неявно нельзя, только через JobScheduler. Остались исключения, но их очень мало:
ACTION_BOOT_COMPLETED;
ACTION_LOCAL_CHANGED;
ACTION_PACKAGE_DATA_CLEARED и ACTION_PACKAGE_ FULLY_REMOVED;
ACTION_NEW_OUTGOING_CALL;
ACTION_MEDIA_***;
SMS_RECEIVED_ACTION и WAP_PUSH_RECEIVED_ACTION.
Этот список актуален на лето 2022 года.
Нет фоновым Service. Запретили запускать Service в фоне. Если разработчик запустил такой Service и свернул приложение, он проживет совсем недолго и будет принудительно убит. Если попытаться запустить обычный Service, когда приложения в фоне, тоже будет крэш.
Обновление JobScheduler. Первое — не мало свободной памяти (именно так звучит условие). Сколько памяти достаточно, в Google не говорят. Не низкий заряд батареи — что именно под этим подразумевается, опять же не поясняют. Видимо, это про критичные проценты, когда батарея становится красной.
Нетарифицируемая сеть. Новая классная штука, привязанная к стоимости. Когда мы подключаемся к сети в настройках, мобильная сеть по умолчанию идет как тарифицируемая, то есть мы платим за трафик. Wi-Fi по умолчанию считается нетарифицируемым, но в настройках можно это изменить.
Foreground Service. Бэкграунд-сервисы теперь нужно явно вызывать. Раньше нужно было просто вызвать такой сервис, а потом сказать, что он Foreground. Теперь из-за того, что бэкграунд-сервисы убиты, нужно явно сказать: я сейчас буду запускать Foreground Service. И код становится примерно таким.
context.startForegroundService(Intent(context, MediaService::class))
class MediaService : Service() {
override fun onCreate() {
super.onCreate()
startForeground(NOTIFICATION_ID, newOngoingNotification())
}
fun newOngoingNotification() : Notification
companion object {
const val NOTIFICATION_ID
}
}
Важная особенность: между вызовом startForegroundService и startForeground должно пройти не больше пяти секунд. Иначе у приложения будет крэш, потому что мы не выполнили контракт.
Важно помнить, что в любом приложении, когда запускается сервис, активити или что-то еще, первым начинает грузиться апликейшн. Как много разных нитов вы делаете в апликейшене? Обычно мы запихиваем туда все библиотеки. И его смысл в том, чтобы минимизировать объем.
Грубо говоря, если у вас есть инициализация, необходимая для UI, ее не нужно делать в апликейшне, потому что она опциональна и может запустить какой-то сервис. Нужно либо все уводить в фон, либо делать лэзи — это новый подход.
Может показаться, что пять секунд — это много, но поверьте, для бюджетных устройств с Android 8.0 это целая вечность. Код может просто не выполниться, и будет крэш. Особенно если есть какая-то сетевая операция в апликейшне. Поэтому не следует забывать его оптимизировать.
Ограничение на доступ к местоположению. Еще одно ограничение, связанное с безопасностью и сохранением заряда батареи. Если приложение в фоне, оно будет получать обновления не чаще, чем несколько раз в час. Сколько именно раз — мы не знаем.
Снятие WakeLock системой. При переходе приложения в состояние cached (отсутствуют активные Android-компоненты) все WakeLock, захваченные системой, освободятся. WakeLock — это спец-API, которое позволяет системе блокировать ненужное. Например, если система видит, что сейчас ничего не используется и девайс уходит в сон, можно выключить процессор. WakeLock как бы говорит системе: не выключай процессор, я собираюсь его использовать. Соответственно, идет нагрузка на железо. И если приложение переходит в состояние cached, значит, в нем нет активных Service и Activity, никто не вызывает у него контент-провайдер или бродкаст-ресиверы. Все WakeLock для него автоматически снимут.
Android 9.0
App StandBy Buckets. Так называемые корзинки — новый подход к тому, какие приложения нужно останавливать и как. Если раньше приложение уходило в бэкграунд, система полагала, что его нужно остановить. Теперь система анализирует, как приложения используют, и распределяет их по категориям:
Active — с приложением работает пользователь или другое приложение в состоянии Active;
Working Set — приложение используют часто, но сейчас пользователь с ним не работает;
Frequent — приложение используют регулярно, но не обязательно каждый день;
Rare — приложение используют редко;
Never — приложение установили, но пользователь никогда не запускал его. Выключено все.
Каждому бакету соответствовал определенный набор ограничений
Улучшение режима экономии энергии. Его сделали сильнее, введя больше ограничений:
система агрессивнее переводит приложения в режим App Standby;
ограничения на работу применяются ко всем приложениям, независимо от targetSdk;
доступ к приложению может пропасть при отключении экрана;
у фоновых приложений нет доступа к сети;
дополнительный пункт — вендоры могут что-то добавить или изменить.
Разрешение на запуск Foreground Service. Оно не критично, потому что попадает в категорию normal. Его нужно только декларировать в манифесте, а запрашивать в рантайме не нужно. Фактически оно помогает системе понять, что вы запросили пермишн — за вами можно следить. Если попытаться запустить Foreground Service без разрешения, система все крэшнет и скажет, что у вас SecurityException.
Ограничения на доступ к сенсорам. Приложения в фоне не могут получать доступ к микрофону и камере, сенсоры не передают данные вовсе или делают это с пониженной частотой. Насколько пониженной, мы не знаем.
Обновление JobScheduler. Появилась поддержка информации о размере загружаемого файла. На основе этой информации система могла понимать: если вы грузите маленький файл, вас можно пропустить быстро. А если файл большой, лучше дождаться, когда будет нетарифицируемая сеть и высокий уровень заряда.
Еще появилась важная штука для предзагрузки контента. Если приложению нужно предзагрузить данные для отображения, например, главного экрана, можно пометить джобу с помощью этого нового флага, и JobScheduler поднимет ее приоритет.
Android 10
Тип Foreground Service. Новая штука, которая позволяет системе понять, зачем нужен Foreground Service, и приоритизировать его. Всего типов восемь, других быть не должно:
Camera — использует камеру, делает фото и записывает видео;
ConnectedDevice — работает с bluetooth-устройствами, авто и прочим;
DataSync — передача данных по сети, бэкап/восстановление, загрузка файлов;
Location — GPS, карта или навигация;
MediaPlayback — проигрывание звука и видео;
MediaProjection — управление проекцией медиа, например запись видео с экрана, снятие скриншота и так далее;
Microphone — использует микрофон или записывает аудио;
PhoneCall — операции, связанные с телефонными и видеозвонками и похожими коммуникациями.
Важно, что у одного Foreground Service может быть объявлено несколько типов. Теперь их можно явно указывать при запуске. Например, один из типов, объявленных в манифесте, вообще не будет работать в операциях этого типа либо будет запускать все сразу.
class MediaService : Service() {
override fun onCreate() {
super.onCreate()
// Сделать Service Foreground с одним из типов, указанных в AndroidManifest
startForeground(NOTIF_ID, newNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
// Сделать Service Foreground со всеми типами из AndroidManifest
startForeground(NOTIF_ID, newNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST)
// Сделать Service Foreground без использования типов
startForeground(NOTIF_ID, newNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE)
}
}
Запрет на запуск Activity из фона. Раньше, если приходил пуш, можно было запустить Activity из бродкаст-ресивера. Теперь так нельзя: любой показ UI должен инициировать пользователь. Например, можно показать уведомление. Если пользователь нажимает на него, мы получаем разрешение на запуск активити. Если это не так, будет крэш.
Доступ к местоположению из фона. Появился третий пермишн на локейшн, и теперь нужно сообщать, что мы собираемся работать еще и в фоне. Кроме того, нужно получить отдельное разрешение на работу в фоне от пользователя.
Android 11
Самое интересное и самое разочаровывающее для меня — что в Android 11 ничего не ограничили. Поэтому мы переходим к…
Android 12
Restricted App Standby Bucket. Самый жесткий бакет: если приложение потребляет много системных ресурсов или работает нежелательным образом, система поместит его туда. Формулировки расплывчатые, но, скорее всего, речь идет о зловредах.
Ограничения запуска Foreground Service. Приложения в фоне не могут запускать Foreground Service за исключением нескольких случаев. Список исключений очень маленький.
Разрешение на Exact Alarm. Раньше это можно было делать как угодно, а теперь появился пермишн. И это не какой-нибудь runtimepermission, а такая интересная штука, как Picture-in-Picture. То есть мы должны сказать пользователю: сходи в настройки системы и разреши моему приложению это делать. С одной стороны, это неудобно, с другой — полезно. Многие разработчики сталкивались с тем, что Exact Alarm срабатывали непредсказуемо или не срабатывали вовсе. А пермишн — это стандартизация и явный сигнал систем. Главное — помнить, что Exact Alarm не обязательно вызовется в нужное нам время. Это может случиться позже на несколько минут или больше.
Expedited Job. Новый тип Job для важных задач, которые нужно выполнить мгновенно. Злоупотреблять нельзя. Вот признаки такой джобы:
выполняется как можно скорее;
имеет меньше ограничений в режимах Doze и «Экономия батареи»;
на него не влияют ограничения доступа в сеть, накладываемые Doze, App Standby и режимом «Экономия батареи»;
имеет меньше шансов быть убитой, чем обычная Job;
ограничения на доступ к местоположению в фоне сохраняются;
для запуска работы доступны лишь требования по наличию сети, объему свободного места и сохранению Job между перезагрузками устройства.
У этого типа есть особенности, о которых полезно знать:
система выделяет квоту на количество одновременно запущенных Expedited Job для приложения;
на активное приложение квоты не распространяются;
может не запуститься сразу, если устройство слишком загружено или заданные для выполнения Job нельзя выполнить;
имеет ограниченное время на выполнение — не меньше минуты;
если у приложения осталась свободная квота, возможно, Expedited Job сможет работать дольше.
Android 13
Foreground Service Task Manager. Теперь пользователь может в специальном окошке в нотификейшн-панели увидеть, какие приложения сейчас что-то делают в Foreground. То есть свернуты, но пытаются что-то выполнять. Пользователь может вручную их остановить.
Foreground Service Task Manager в Android 13
Специальная кнопка принудительно останавливает приложение. Не так, как Force Stop в настройках, но все же полностью его останавливает. Даже если оно просто свернуто.
Настройка использования батареи для каждого приложения. По умолчанию все приложения оптимизированы, то есть соблюдается баланс между временем работы в фоне и расходом батареи. Пользователь может выбрать одну из двух настроек:
Unrestricted — «делай что угодно, мне все равно»;
Restricted — когда приложение практически ничего не может делать в фоне.
В Restricted режиме будут применены следующие ограничения:
нельзя запускать Foreground Service;
запущенные Foreground Service убираются из Foreground;
не будет срабатывать Alarm;
не будет запускаться Job;
не будут доставляться бродкасты BOOT_COMPLETED и LOCKED_BOOT_COMPLETED.
Уведомление о слишком долгом Foreground Service. Система отслеживает, если приложение слишком долго работает в таком режиме, и рекомендует его остановить. Почему-то Google считает, что долго — это 20 часов в окне в 24 часа. Это странно, мне сложно представить приложение, которое так работает. Особенности работы в таком режиме:
Уведомление можно показывать не чаще чем раз в 30 дней;
Предупреждение не покажут, если уведомление, ассоциированное с Foreground Service, видно;
Исключения — сервисы с типами MediaPlayback и Location;
Системные приложения, приложения с определенными целями и устройства в деморежиме — тоже исключения.
Обновление JobScheduler. Появилась возможность поддержки дозагрузки файлов. Если сервер, с которого вы скачиваете файлы, поддерживает дозагрузку, можно эти данные указывать в Job. И если JobScheduler решит, что Job нужно остановить, потом он запустит ее, и вы будете знать, с какой части нужно продолжить выполнение.
Кроме этого, добавили приоритет Job. Это спецконстанта, которая помогает отсортировать все Job в рамках одного приложения и приоритизировать их выполнение.
Android сейчас
Download Manager по-прежнему с нами. JobScheduler мы больше не используем — на смену ему пришел Jetpack WorkManager.
Если говорить о сервисах, то background официально есть, но по факту его скорее нет. Foreground тоже есть, но там похожая ситуация. Bound есть, но их мало кто использует. Ну и AlarmManager никто не отменял.
Есть WorkManager — это официальная рекомендация Google для всех, кто хочет что-то делать в фоне. Вот его возможности:
корректная работа на разных версиях Android с учетом требований;
задание условий для выполнения задачи;
гибкие возможности времени выполнения задачи;
возможность выполнять работу незамедлительно;
гибкая политика по повтору задачи, выполненной неуспешно;
организация нескольких задач в цепочки, чтобы выполнять их последовательно и/или параллельно;
поддержка Coroutines и RxJava;
поддержка работы в нескольких процессах.
Если вам нужно запустить что-то в точное время и гарантировать выполнение, лучше по старинке использовать AlarmManager. А для простой загрузки файлов с сервера — DownloadManager. Нужно указать url, параметры и место, куда нужно сохранить.
Если у задания есть конечный результат, можно действовать по-разному. Конечный результат — это, например, когда файл загружен на сервер. Если вы проигрываете музыку, тут результата нет, это бесконечный процесс.
Если конечного результата нет, это Foreground Service, причем он должен попадать под типы. Если нет совпадения с Foreground Service или нужно запустить Job немедленно и с очень высоким приоритетом, используйте Expedited Job в WorkManager. В остальных случаях — WorkManager.
Что и когда запускать
Но тут начинается веселье, потому что в игру вступают вендоры. Есть такой рейтинг — Don«t kill my app. Там ребята пишут подробные гайды с объяснениями, как различные вендоры убивают приложения. Довольно долго рейтинг возглавлял Xiaomi, потом в лидеры выбился One Plus, а потом Samsung обновил свои устройства до Android 11 и с сильным отрывом ушел вперед.
Вы можете найти проблемы, с которыми сталкиваются разработчикиКакие оптимизации были на разных версиях устройствГайды, помогающие пользователю отключить это в обход стандартных настроекСхемы, помогающие найти разные опции
Единственный вариант, как попросить пользователя отключить все оптимизации для приложения — запустить стандартный Intent, который появился в Android 6.0, и молиться, чтобы пользователь это сделал. Тогда приложение может начать работать в фоне лучше, но результат никто не гарантирует.
// Android 6.0 API 23
val powerManager: PowerManager = getSystemService()
if (!powerManager.isIgnoringBatteryOptimizations(context.packageName)) {
try {
context.startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))
} catch (e: ActivityNotFoundException) {
// Обрабатываем если экрана нет
}
}
Кстати, вендоры любят по умолчанию глушить все приложения, которые не добавлены в white-list. Такой лист есть у каждого популярного вендора. Если ваше приложение довольно крупное, с несколькими миллионами пользователей, возможно, у вас получится договориться с вендором и попасть туда.
Заключение
Последние восемь лет Google пытается ограничить разработчиков и заставить их использовать специальные API, так как свобода выбора инструментов привела к хаосу на устройствах.
Второй линией обороны служит Google Play, который не дает публиковать приложения с отдельными разрешениями без прохождения специальной проверки со стороны магазина. Дальше будет становиться только жестче: все новые API по умолчанию направлены на ограничения, экономию батарейки и четкий контроль работы приложения со стороны системы.
Это закономерно, ведь на старте у Google были доставшиеся в наследство исходники. Нужно было привлечь разработчиков и вендоров для популяризации Android. К 2014 году компания решила эту задачу и сменила вектор на обеспечение надежности, безопасности и приятного пользовательского опыта от работы с Android-устройствами.