Привет! Меня зовут Алексей, я работаю Android-разработчиком в Облаке Mail. Наше приложение выполняет важную задачу — хранит воспоминания пользователей. Для этого необходимо уметь правильно работать с файлами в фоне, чтобы не только надёжно хранить те самые воспоминания, но и быстро их загружать, редактировать и делиться. В этой статье я расскажу о том, как мы пришли к нашим современным методам фоновой работы в Android.

Зачем ограничивать работу в фоне?

На это есть несколько причин:

  • Защита от недобросовестных разработчиков, которые могут бесконечно собирать данные о пользователе в фоне, чтобы показывать ему рекламу.

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

  • Поддержание имиджа. С выходом новых версий Android необходимо улучшать производительность и увеличивать длительность работы от аккумулятора, чтобы пользователи оставались довольны.

  • Влияние на выбор инструмента. На данный момент только сервисы Google (а также сервисы вендоров) могут работать в фоне без остановки. Отправка push-уведомлений в 99% случаев реализуется их сервисами.

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

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

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

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

    • Background — выполняет операцию, невидимую пользователю.

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

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

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

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

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

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

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

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

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

  • Foreground Service — выполняет операцию, видимую пользователю в виде уведомления (например, таймер, аудиоплеер, прогресс длительной операции и т. д.).

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

Для того чтобы упростить скачивание файлов, в Android 2.3 добавили:

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

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

Android 4.4 KitKat

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

Так появились следующие изменения в AlarmManager:

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

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

Android 5 Lollipop

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

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

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

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

Может показаться, что этот метод не несёт в себе какой-либо особой пользы по сравнению с setExact(), но уже буквально в следующей версии системы ситуация кардинально поменяется.

Android 6 Marshmallow

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

В режиме Doze Mode:

  • Приложениям запрещается доступ к сети.

  • Игнорируются WakeLock'и.

  • Не может работать JobScheduler.

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

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

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

  • Пользователь не открывал приложение в течение определённого времени.

  • Приложение не находится на переднем плане (foreground).

  • Приложение не отправляет уведомлений, которые показываются пользователю.

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

Чтобы избежать попадания в режимы 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 (changelog).

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

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

Android 8 Oreo

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

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

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

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

Android 9 Pie

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

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

  • Active: приложение активно в данный момент (например: запущено Activity, работает foreground service или sync adapter, или пользователь нажал на уведомление приложения).

  • Working Set: приложение используют часто, но в данный момент оно не активно (например, приложение соцсети).

  • Frequent: приложение используют регулярно, но не обязательно каждый день (например, приложения для записи тренировок, которое пользователь запускает, когда приходит в спортзал).

  • Rare: приложение используют редко (например, приложение отеля, которое пользователь запускает только в отпуске).

  • Never: приложение, которое пользователь установил, но никогда не запускал. На такое приложение накладываются самые строгие ограничения.

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

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

  • Система более агрессивно переводит приложения в режим App Standby.

  • Ограничения работы в фоне применяются ко всем приложениям, независимо от targetSdkVersion.

  • Сервисы локации могут отключаться, когда выключен экран.

  • Фоновые приложения не имеют доступа к сети.

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

Android 10

Больше нет дессертов! ?

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

  • connectedDevice: получение данных с фитнес-трекера;

  • dataSync: скачивание файлов из сети;

  • location: обработка локации пользователя;

  • mediaPlyback: проигрывание аудио;

  • mediaProjection: запись экрана устройства за короткий промежуток времени;

  • phoneCall: отображение текущего телефонного звонка.

Также запретили запускать Activity из фона (changelog, документация).

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

Android 11

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

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

Android 12

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

  • им не пользовались 45 дней;

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

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

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

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

Android 13

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

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

Android 14

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

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

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

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

Android 15

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

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

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

Android 16

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

Итог

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

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

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

А что там у вендоров?

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

Подробности: https://dontkillmyapp.com/

Как дальше жить?

Если после всего прочитанного вы задаетесь вопросом: «Как дальше жить?» «Как теперь работать в фоне?», то предлагаю вам прочитать следующую мою статью, которая скоро выйдет. В ней я собрал все актуальные методы с примерами кода.

Спасибо, что дочитали до конца! Делитесь своими историями о том, как вы сталкивались с ограничениями в фоновой работе и адаптировались к новым правилам.

Комментарии (0)