Освой программирование играючи
/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
Сложное современное приложение должно уметь использовать возможности фоновой работы в Android. Почему Android последовательно от версии к версии ограничивает работу в фоне? На это есть несколько причин:
Всё это привело к тому, что работу в фоне стали ограничивать.
Перенесёмся в 2008 год, когда вышла первая версия Android. Какие инструменты для работы в фоне у нас были?
Service — компонент приложения, работающий в том потоке, в котором его запустили (в том числе в главном). Изначально было два режима работы:
AlarmManager — позволяет планировать выполнение задачи в будущем. Работает за счёт отправки отложенного Broadcast Intent.
Broadcast Intent — оповещение, которое может отправлять система или приложения при наступлении какого-то события. Приложения могут подписываться на конкретные оповещения и выполнять работу при их получении. Система отвечает за доставку этих оповещений.
Loader — класс, связанный с жизненным циклом Activity и Fragment через LoaderManager. Позволяет загружать данные в отдельном потоке.
WakeLock — механизм, предотвращающий переход процессора в режим ожидания. Когда пользователь перестаёт взаимодействовать с устройством, оно быстро переходит в режим ожидания, чтобы избежать разрядки аккумулятора. Но иногда приложению необходимо предотвратить такой переход. Именно для этой цели был разработан WakeLock.
В версии Android 1.5 к ним добавились:
В версии Android 2.0 появились ещё:
Для того чтобы упростить скачивание файлов, в Android 2.3 добавили DownloadManager — системный сервис, выполняющий длительные загрузки файлов по протоколу HTTP.
Первой оптимизацией Android стало изменение принципа запуска оповещений. Вместо того, чтобы пробуждать устройство для каждого оповещения, система объединяет запуск оповещений из всех приложений, запланированных примерно на одно время.
Появились следующие изменения в AlarmManager:
Новая версия системы принесла новые оптимизации под названием Project Volta:
Battery Saver – режим низкой производительности для увеличения времени работы аккумулятора.
JobScheduler – новый системный сервис для запуска фоновых задач, позволяющий настраивать условия запуска (например, при подключении к зарядке или наличии интернета).
Также в AlarmManager добавили новый метод setAlarmClock() — он запускает оповещение, которое помимо срабатывания ещё будет предупреждать пользователя о том, что у него стоит будильник.
В этой версии появился Doze Mode — режим, ограничивающий функции устройства, когда оно не используется. В следующих версиях требования ужесточаются всё больше и больше.
В спящем режиме не выполняются сетевые запросы, кроме GCM с высоким приоритетом. Также могут блокироваться операции синхронизации, задачи по сигнализации событий, сканирование сетей Wi-Fi, работа GPS.
Итак, в режиме Doze Mode:
Новые алгоритмы сна под названием Doze помогут вашему устройству работать на одной зарядке дольше: в моменты, когда аппарат долго лежит неподвижно, не подключён к зарядке и его дисплей не включается для отображения уведомлений, все приложения ставятся на паузу (App Standby), передача данных минимизируется, процессор переходит в энергосберегающий режим, все синхронизации и прочие любители что-нибудь сделать, пока телефон «бездельничает» отправляются в сон.
Для особо важных задач можно запустить метод setAndAllowWhileIdle() от AlarmManager, но не чаше одного раза в 15 минут.
Когда устройство на Android Marshmallow лежит без движения и без зарядки, спустя час оно переходит в Doze Mode. Режим отключки, когда почти все приложения перестают жрать батарею. Это происходит не сразу, а по шагам:
Если ваше приложение использует различные задачи, которые могут выполняться в разное время, то нужно обязательно протестировать поведение программы при включении спящего режима. Для этого можно воспользоваться специальными командами, чтобы не дожидаться наступления режима 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:
Соответственно, если наше приложение является чатом, то мы можем отправить с сервера push с полем priority = high. А если у нас приложение будильник, то мы должны обязательно вызвать setAndAllowWhileIdle() или setExactAndAllowWhileIdle().
Во многих других случаях мы вообще не должны об этом переживать, после того, как пользователь возьмёт устройство в руки, все заснувшие объекты проснутся и сделают свою работу.
Также появился режим App Standby, ограничивающий сетевые запросы и фоновую работу приложений, которыми пользователь редко пользуется.
Если у приложения есть фоновые задачи, но приложение простаивает, то включается режим Standby.
Если приложение простаивает долгое время, то система разрешит делать ему запрос в сеть один раз в сутки.
Простаивающие приложения определяют по следующим критериям:
Режим App Standby отправляет в изоляцию приложения, которые не подходят под условия:
Этот режим тоже можно тестировать с помощью команд.
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 режим 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 — система их больше не отправляет.
С точки зрения фоновых ограничений эта версия ОС стала вехой в истории, убившей Background Service (в том числе IntentService). В этой версии системы полностью запретили их работу в фоне.
Когда приложение больше не находится на переднем плане, его фоновые сервисы будут останавливаться, а запуск новых фоновых сервисов будет приводить к ошибке. Для работы в фоне приложению необходимо запускать foreground service или использовать JobScheduler.
Также ограничили подписку на практически все системные Broadcast Intent через AndroidManifest . Система больше не будет запускать приложения для доставки интентов. Но есть некоторые исключения. Когда приложение активно, можно использовать метод Context.registerReceiver() для получения любых интентов, пока корректен контекст получателя.
Когда приложение переходит в кэшированное состояние (без активных компонентов), система освобождает все WakeLock, которые это приложение удерживало.
Для запуска Foreground Service нужно указывать новое разрешение FOREGROUND_SERVICE в AndroidManifest, иначе при запуске сервиса получим ошибку SecurityException.
В режиме App Standby появилось деление приложений на группы по частоте использования:
Узнать, в какой группе находится ваше приложение, можно с помощью метода UsageStatsManager.getAppStandbyBucket().
Обновили режим Battery Saver. Изменения:
Также для фоновых приложений ограничили доступ к камере, микрофону и датчикам.
Добавили тип foreground-сервисов — специальный атрибут foregroundServiceType в AndroidManifest. Он может принимать следующие значения:
Также запретили запускать Activity из фона.
Добавили ограничение на доступ к локации из фона. Теперь необходимо указывать в AndroidManifest и запрашивать в runtime разрешение ACCESS_BACKGROUND_LOCATION.
Нужно указывать типы camera и microphone для foreground-сервисов, которые обращаются к ним. Foreground-сервисы, запущенные из фона, не смогут получать доступ к камере, микрофону и локации.
Также WorkManager окончательно закрепили как универсальное средство для работы с фоновыми задачами. Все другие инструменты, такие как IntentService, AsyncTask, FirebaseJobDispatcher и GCMNetworkManager, перестали работать после перевода targetSdkVersion на Android 11.
Новая группа App Standby Bucket стала Restricted, это ниже, чем Rare. Приложение попадает в неё, если:
Запретили запускать foreground-сервис, когда приложение в фоне, за некоторыми исключениями (например, получение высокоприоритетного уведомления). Вместо этого предложили новый тип работ в WorkManager — Expedited work. Он имеет обратную совместимость: на старых версиях Android работает как foreground-сервис.
Получение локации в foreground (в том числе в foreground service) теперь работает при включённом режиме Battery Saver. Это единственный случай, когда Google откатили ограничение.
Добавили разрешение SCHEDULE_EXACT_ALARM для запуска будильников.
Появилось специальное окно Task Manager, в котором собраны все работающие foreground-сервисы. Уведомления от них теперь можно смахивать.
Обновили правила попадания в Restricted bucket: если пользователь не пользовался приложением в течение 8 дней (было 45 дней).
Новое условие попадания в Restricted bucket: получение ANR-ошибок во время выполнения методов onStartJob() или onStopJob() при использовании JobScheduler. Раньше если эти методы не успевали отработать, то задача тихо завершалась с ошибкой. А теперь вместо этого появляется ANR-ошибка: «No response to onStartJob» или «No response to onStopJob».
Обязательное требование: указывать хотя бы один тип foreground-сервиса. Также нужно указывать соответствующее для этого типа разрешение в AndroidManifest.
Дополнительные запреты на запуск Activity из фона.
Запрещено убивать фоновые процессы других приложений с помощью метода killBackgroundProcesses().
Foreground-сервисы типа dataSync и mediaProcessing могут работать не больше 6 часов в день в сумме.
Нельзя запускать foreground-сервисы из broadcast receiver'а BOOT_COMPLETED с типами camera, dataSync, mediaPlayback, phoneCall, mediaProjection и microphone.
Сетевые запросы вне корректного жизненного цикла будут получать ошибку UnknownHostException или другую схожую IOException. Как правило, это касается приложений, которые продолжают выполнять сетевые запросы, даже когда больше не активны. Если важно, чтобы сетевой запрос выполнялся даже тогда, когда пользователь покидает приложение, то нужно использовать WorkManager или продолжить выполнение запроса в виде foreground-сервиса.
Обновили квоты на запуски фоновых задач через WorkManager, JobScheduler и DownloadManager для разных бакетов App Standby.
В последние годы Google активно работает над оптимизацией работы приложений в фоновом режиме, что необходимо для улучшения производительности устройств пользователей и увеличения времени работы от аккумулятора.
Стандартные инструменты, такие как Service, AlarmManager и Broadcast Intent, которые разработчики использовали в начале развития системы, столкнулись со значительными ограничениями. Сторонние инструменты, такие как IntentService, AsyncTask, FirebaseJobDispatcher и GCMNetworkManager, полностью устарели.
Теперь вся фоновая работа регулируется и модерируется Google Play и RuStore. С каждой новой версией системы разработчикам приходится адаптироваться к новым правилам.
Мы рассмотрели все изменения в системе Android, касающиеся фоновой работы. Однако на практике вендоры часто добавляют свои собственные ограничения. В результате приложение может быть завершено системой, несмотря на все попытки честно работать в фоне. Чтобы этого избежать, необходимо просить пользователя выполнить определённые действия в своей системе.
Использовались материалы из статьи Хроника изменений API фоновой работы в Android