Освой программирование играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Фоновая работа

Doze Mode
Режим Standby

Сложное современное приложение должно уметь использовать возможности фоновой работы в Android. Почему Android последовательно от версии к версии ограничивает работу в фоне? На это есть несколько причин:

  • Защита от недобросовестных разработчиков, которые могут бесконечно собирать данные о пользователе в фоне, чтобы показывать ему рекламу
  • Экономия аккумулятора. Чтобы новые телефоны могли работать дольше, нужно избавляться от лишних вычислений
  • Нужно улучшать производительность и увеличивать длительность работы от аккумулятора, чтобы пользователи оставались довольны
  • Необходимость. На данный момент только сервисы Google (а также сервисы вендоров) могут работать в фоне без остановки

Всё это привело к тому, что работу в фоне стали ограничивать.

С чего всё начиналось: Service, AlarmManager и другие

Перенесёмся в 2008 год, когда вышла первая версия Android. Какие инструменты для работы в фоне у нас были?

Service — компонент приложения, работающий в том потоке, в котором его запустили (в том числе в главном). Изначально было два режима работы:

  • Background — выполняет операцию, невидимую пользователю
  • Bound — позволяет другим компонентам подключаться к нему с помощью метода bindService(). Реализует взаимодействие клиент-сервер. Работает до тех пор, пока он привязан к другому компоненту приложения

AlarmManager — позволяет планировать выполнение задачи в будущем. Работает за счёт отправки отложенного Broadcast Intent.

Broadcast Intent — оповещение, которое может отправлять система или приложения при наступлении какого-то события. Приложения могут подписываться на конкретные оповещения и выполнять работу при их получении. Система отвечает за доставку этих оповещений.

Loader — класс, связанный с жизненным циклом Activity и Fragment через LoaderManager. Позволяет загружать данные в отдельном потоке.

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

Android 1.5

В версии Android 1.5 к ним добавились:

  • AsyncTask — задумывался как инструмент для упрощения написания многопоточного кода. Однако плохая реализация приводила к утечкам памяти и некорректной работе кода. В конечном итоге Google полностью отказалась от использования этого класса.
  • IntentService — наследник Service для выполнения одноразовых задач, таких как скачивание файлов или сложные расчёты в фоновом потоке. Выполняет работу и завершается. По мере введения ограничений на фоновую работу был признан устаревшим

Android 2.0

В версии Android 2.0 появились ещё:

  • Foreground Service — выполняет операцию, видимую пользователю в виде уведомления (например, таймер, аудиоплеер, прогресс длительной операции и т. д.).
  • Sync Adapter — компонент, интегрированный с Account Manager, предназначенный для синхронизации данных между устройством и сервером. Умеет работать автоматически, можно запускать вручную.

Android 2.3

Для того чтобы упростить скачивание файлов, в Android 2.3 добавили DownloadManager — системный сервис, выполняющий длительные загрузки файлов по протоколу HTTP.

Android 4.4 KitKat

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

Появились следующие изменения в AlarmManager:

  • метод set() не гарантирует точного времени запуска оповещений;
  • метод setExact() гарантирует точный запуск оповещений

Android 5 Lollipop

Новая версия системы принесла новые оптимизации под названием Project Volta:

Battery Saver – режим низкой производительности для увеличения времени работы аккумулятора.

JobScheduler – новый системный сервис для запуска фоновых задач, позволяющий настраивать условия запуска (например, при подключении к зарядке или наличии интернета).

Также в AlarmManager добавили новый метод setAlarmClock() — он запускает оповещение, которое помимо срабатывания ещё будет предупреждать пользователя о том, что у него стоит будильник.

Android 6 Marshmallow

Doze Mode

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

В спящем режиме не выполняются сетевые запросы, кроме GCM с высоким приоритетом. Также могут блокироваться операции синхронизации, задачи по сигнализации событий, сканирование сетей Wi-Fi, работа GPS.

Итак, в режиме Doze Mode:

  • Приложениям запрещается доступ к сети
  • Игнорируются WakeLock
  • Не может работать JobScheduler
  • Откладывает стандартные оповещения AlarmManager до следующего maintenance window, включая методы setExact() и setWindow(). Теперь единственный способ разбудить устройство в нужное время — это setAlarmClock()

Новые алгоритмы сна под названием Doze помогут вашему устройству работать на одной зарядке дольше: в моменты, когда аппарат долго лежит неподвижно, не подключён к зарядке и его дисплей не включается для отображения уведомлений, все приложения ставятся на паузу (App Standby), передача данных минимизируется, процессор переходит в энергосберегающий режим, все синхронизации и прочие любители что-нибудь сделать, пока телефон «бездельничает» отправляются в сон.

Для особо важных задач можно запустить метод setAndAllowWhileIdle() от AlarmManager, но не чаше одного раза в 15 минут.

Когда устройство на Android Marshmallow лежит без движения и без зарядки, спустя час оно переходит в Doze Mode. Режим отключки, когда почти все приложения перестают жрать батарею. Это происходит не сразу, а по шагам:

  • ACTIVE — Устройство используется или на зарядке
  • INACTIVE — Устройство недавно вышло из активного режима (пользователь выключил экран, выдернул зарядку и т.п.)
  • ...30 минут
  • IDLE_PENDING — Устройство готовится перейти в режим ожидания
  • ...30 минут
  • IDLE — Устройство в режиме бездействия
  • IDLE_MAINTENANCE — Открыто короткое окно, чтобы приложения выполнили свою работу

Если ваше приложение использует различные задачи, которые могут выполняться в разное время, то нужно обязательно протестировать поведение программы при включении спящего режима. Для этого можно воспользоваться специальными командами, чтобы не дожидаться наступления режима Doze.

Например, можно ввести команду отключения питания. Если ваше устройство подключено к компьютеру, то после этой команды вы увидите, что значок зарядки сменится на значок работы от батареи.


adb shell dumpsys battery unplug

Далее вы можете увидеть состояние устройства.


adb shell dumpsys deviceidle step

Команда будет возвращать следующие строки в разное время.


Stepped to: ACTIVE
Stepped to: IDLE_PENDING
Stepped to: SENSING
Stepped to: IDLE
Stepped to: IDLE_MAINTENANCE

Вернуть батарею обратно в обычное состояние зарядки от сети.


adb shell dumpsys battery reset

Увидеть все доступные команды.


adb shell dumpsys deviceidle -h

В момент, когда устройство переходит в состояние IDLE:

  • Доступ приложению к сети отключен, пока приложение не получит high-priority GCM-push
  • Система игнорирует Wake Lock. Приложения могут сколько угодно пытаться запросить пробуждение процессора — они их не получат
  • Запланированные Alarm в AlarmManager не будут вызываться, кроме тех, которые будут обновлены с помощью setAndAllowWhileIdle()
  • Система не производит поиска сетей Wi-Fi
  • NetworkPolicyManagerService: пропускает только приложения из белого списка
  • JobSchedulerService: все текущие задачи отменяются. Новые откладываются до пробуждения
  • SyncManager: все текущие отменяются, новые откладываются до пробуждения
  • PowerManagerService: только задачи приложений из белого списка

Соответственно, если наше приложение является чатом, то мы можем отправить с сервера push с полем priority = high. А если у нас приложение будильник, то мы должны обязательно вызвать setAndAllowWhileIdle() или setExactAndAllowWhileIdle().

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

Режим Standby

Также появился режим App Standby, ограничивающий сетевые запросы и фоновую работу приложений, которыми пользователь редко пользуется.

Если у приложения есть фоновые задачи, но приложение простаивает, то включается режим Standby.

Если приложение простаивает долгое время, то система разрешит делать ему запрос в сеть один раз в сутки.

Простаивающие приложения определяют по следующим критериям:

  • Пользователь не открывал приложение в течение определённого времени
  • Приложение не находится на переднем плане (foreground)
  • Приложение не отправляет уведомлений, которые показываются пользователю

Режим App Standby отправляет в изоляцию приложения, которые не подходят под условия:

  • Пользователь явно запустил приложение
  • Приложение имеет процесс, работающий в данный момент на переднем плане (Activity или foreground service, или используется другая активность или foreground service)
  • Приложение создало уведомление, которое висит в списке уведомлений
  • Пользователь принудительно добавил приложение в список исключений оптимизации в настройках системы

Этот режим тоже можно тестировать с помощью команд.


adb shell am broadcast -a android.os.action.DISCHARGING
shell am set-inactive <App_Package_Name> true

Разбудить приложение можно командой.


adb shell am set-inactive <App_Package_Name> false

Проверить статус приложения:


adb shell am get-inactive <App_Package_Name>

Например, может вернуться строка.


Idle=false

Есть специальный белый список Whitelist, в который пользователь может добавить исключения. Приложениям из белого списка не страшны ни Doze Mode ни App Standby.

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

Можно программно узнать через метод isIgnoringBatteryOptimizations(), находится ли приложение в "белом списке" приложений, которым разрешено не экономить заряд. Нужно указать имя пакета приложения (не обязательно указывать своё приложение).

Пример есть в PowerManager

Если находится, то вернёт true, иначе - false.

Сами вы не сможете добавить своё приложение в белый список, это может сделать только пользователь вручную. Но вы можете ему показать нужный экран настройки, чтобы он его не искал.


startActivity(new
        Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS));

Более агрессивный способ, когда вы явно вызывает диалоговое окно для добавления приложения в белый список.


Intent intent = new
        Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
        Uri.parse("package:" + getPackageName()));
startActivity(intent);

В манифесте следует прописать разрешение.


<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>

Не экономить

Если приложение уже находится в белом списке, то диалоговое окно не появится.

Когда устройство подключают к зарядке, система выводит приложения из этого режима и даёт им выполнить требуемые задачи. Если устройство долго не используется, система позволяет простаивающим приложениям делать сетевые запросы примерно раз в день.

Чтобы избежать попадания в режимы Doze и App Standby, приложение может попросить пользователя добавить его в список исключений при оптимизации заряда аккумулятора. Для этого можно открыть экран настроек со списком исключений, используя ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS.

Также можно отправить интент ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, чтобы напрямую запросить добавление в исключения (документация).

Проверить, добавлено ли приложение в исключения, можно с помощью метода PowerManager.isIgnoringBatteryOptimizations().

С появлением этих систем появился тип push-уведомлений FCM High Priority, который может выводить приложения из режима Doze и App Standby. Основной сценарий использования — мессенджеры. Получение уведомления с высоким приоритетом должно приводить к показу уведомления (changelog, документация).

Firebase отслеживает поведение каждого установленного приложения. Если получение пуша с высоким приоритетом не приводит к показу уведомления, эти пуши могут быть понижены до обычного приоритета.

Android 7 Nougat

В этой версии Android режим Doze Mode становится двухэтапным. Второй этап активируется через определённое время после первого. Новую версию Doze назвали Doze 2.0 или Doze On-the-go.

Также добавили оптимизацию Project Svelte, убирающую возможность подписки на системный broadcast-интент CONNECTIVITY_ACTION через AndroidManifest. Теперь его можно получать только через метод Context.registerReceiver(), пока приложение активно.

Также удалили broadcast-интенты ACTION_NEW_PICTURE и ACTION_NEW_VIDEO — система их больше не отправляет.

Android 8 Oreo

С точки зрения фоновых ограничений эта версия ОС стала вехой в истории, убившей Background Service (в том числе IntentService). В этой версии системы полностью запретили их работу в фоне.

Когда приложение больше не находится на переднем плане, его фоновые сервисы будут останавливаться, а запуск новых фоновых сервисов будет приводить к ошибке. Для работы в фоне приложению необходимо запускать foreground service или использовать JobScheduler.

Также ограничили подписку на практически все системные Broadcast Intent через AndroidManifest . Система больше не будет запускать приложения для доставки интентов. Но есть некоторые исключения. Когда приложение активно, можно использовать метод Context.registerReceiver() для получения любых интентов, пока корректен контекст получателя.

Когда приложение переходит в кэшированное состояние (без активных компонентов), система освобождает все WakeLock, которые это приложение удерживало.

Android 9 Pie

Для запуска Foreground Service нужно указывать новое разрешение FOREGROUND_SERVICE в AndroidManifest, иначе при запуске сервиса получим ошибку SecurityException.

В режиме App Standby появилось деление приложений на группы по частоте использования:

  • Active: приложение активно в данный момент (например: запущено Activity, работает foreground service или sync adapter, или пользователь нажал на уведомление приложения)
  • Working Set: приложение используют часто, но в данный момент оно не активно (например, приложение соцсети)
  • Frequent: приложение используют регулярно, но не обязательно каждый день (например, приложения для записи тренировок, которое пользователь запускает, когда приходит в спортзал)
  • Rare: приложение используют редко (например, приложение отеля, которое пользователь запускает только в отпуске)
  • Never: приложение, которое пользователь установил, но никогда не запускал. На такое приложение накладываются самые строгие ограничения

Узнать, в какой группе находится ваше приложение, можно с помощью метода UsageStatsManager.getAppStandbyBucket().

Обновили режим Battery Saver. Изменения:

  • Система более агрессивно переводит приложения в режим App Standby.
  • Ограничения работы в фоне применяются ко всем приложениям, независимо от targetSdkVersion
  • Сервисы локации могут отключаться, когда выключен экран
  • Фоновые приложения не имеют доступа к сети

Также для фоновых приложений ограничили доступ к камере, микрофону и датчикам.

Android 10

Добавили тип foreground-сервисов — специальный атрибут foregroundServiceType в AndroidManifest. Он может принимать следующие значения:

  • connectedDevice: получение данных с фитнес-трекера
  • dataSync: скачивание файлов из сети
  • location: обработка локации пользователя
  • mediaPlayback: проигрывание аудио
  • mediaProjection: запись экрана устройства за короткий промежуток времени
  • phoneCall: отображение текущего телефонного звонка

Также запретили запускать Activity из фона.

Добавили ограничение на доступ к локации из фона. Теперь необходимо указывать в AndroidManifest и запрашивать в runtime разрешение ACCESS_BACKGROUND_LOCATION.

Android 11

Нужно указывать типы camera и microphone для foreground-сервисов, которые обращаются к ним. Foreground-сервисы, запущенные из фона, не смогут получать доступ к камере, микрофону и локации.

Также WorkManager окончательно закрепили как универсальное средство для работы с фоновыми задачами. Все другие инструменты, такие как IntentService, AsyncTask, FirebaseJobDispatcher и GCMNetworkManager, перестали работать после перевода targetSdkVersion на Android 11.

Android 12

Новая группа App Standby Bucket стала Restricted, это ниже, чем Rare. Приложение попадает в неё, если:

  • им не пользовались 45 дней
  • приложение вызывает чрезмерное количество broadcast-интентов и binding'ов (возможно, это относится к вредоносным приложениям)

Запретили запускать foreground-сервис, когда приложение в фоне, за некоторыми исключениями (например, получение высокоприоритетного уведомления). Вместо этого предложили новый тип работ в WorkManager — Expedited work. Он имеет обратную совместимость: на старых версиях Android работает как foreground-сервис.

Получение локации в foreground (в том числе в foreground service) теперь работает при включённом режиме Battery Saver. Это единственный случай, когда Google откатили ограничение.

Добавили разрешение SCHEDULE_EXACT_ALARM для запуска будильников.

Android 13

Появилось специальное окно Task Manager, в котором собраны все работающие foreground-сервисы. Уведомления от них теперь можно смахивать.

Обновили правила попадания в Restricted bucket: если пользователь не пользовался приложением в течение 8 дней (было 45 дней).

Android 14

Новое условие попадания в Restricted bucket: получение ANR-ошибок во время выполнения методов onStartJob() или onStopJob() при использовании JobScheduler. Раньше если эти методы не успевали отработать, то задача тихо завершалась с ошибкой. А теперь вместо этого появляется ANR-ошибка: «No response to onStartJob» или «No response to onStopJob».

Обязательное требование: указывать хотя бы один тип foreground-сервиса. Также нужно указывать соответствующее для этого типа разрешение в AndroidManifest.

Дополнительные запреты на запуск Activity из фона.

Запрещено убивать фоновые процессы других приложений с помощью метода killBackgroundProcesses().

Android 15

Foreground-сервисы типа dataSync и mediaProcessing могут работать не больше 6 часов в день в сумме.

Нельзя запускать foreground-сервисы из broadcast receiver'а BOOT_COMPLETED с типами camera, dataSync, mediaPlayback, phoneCall, mediaProjection и microphone.

Сетевые запросы вне корректного жизненного цикла будут получать ошибку UnknownHostException или другую схожую IOException. Как правило, это касается приложений, которые продолжают выполнять сетевые запросы, даже когда больше не активны. Если важно, чтобы сетевой запрос выполнялся даже тогда, когда пользователь покидает приложение, то нужно использовать WorkManager или продолжить выполнение запроса в виде foreground-сервиса.

Android 16

Обновили квоты на запуски фоновых задач через WorkManager, JobScheduler и DownloadManager для разных бакетов App Standby.

Итог

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

Стандартные инструменты, такие как Service, AlarmManager и Broadcast Intent, которые разработчики использовали в начале развития системы, столкнулись со значительными ограничениями. Сторонние инструменты, такие как IntentService, AsyncTask, FirebaseJobDispatcher и GCMNetworkManager, полностью устарели.

Теперь вся фоновая работа регулируется и модерируется Google Play и RuStore. С каждой новой версией системы разработчикам приходится адаптироваться к новым правилам.

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



Использовались материалы из статьи Хроника изменений API фоновой работы в Android

Дополнительное чтение

Реклама