В данной статье, речь пойдет об одной проблеме с JobIntentService'ом, о которой существует множество вопросов на соответсвующих ресурсах и репортов в баг трэкере гугла. А так же о причине по которой, судя повсему, гугл не считает ее багом и закрывет данные репорты.
Введение
JobIntentService’ы были созданы для фоновой работы. Широкое применение они получили в Android 8 и выше, когда исчезла возможность использования сервисов в фоновом режиме.
По сути они заменяют сервисы в фоновом режиме, а так же находятся под управлением планировщика задач(JobScheduler).
Таким образом система имеет возможность контролировать ход выполнения задач в фоновом режиме а так же сама управляет wakelock’ами, что позволило оптимизировать расход батареи устройства и избежать не корректного использования wakelock’ов разработчиками. Данные шаги позволили минимизировать ситуации когда устройство не может погрузится в режим сна(Doze Mode), что опять же влияет на экономию заряда батареи.
Коротко о JobIntentService
По сути JobIntentService это тот же IntentService под управлением планировщика задач(JobScheduler).
Выполняется в фоновом потоке AsyncTask’а.
В версиях Андроида 4.4 и ниже используется обычный IntentService.
Подробное описание можно прочитать в документации
Жизненный цикл и подводные камни
Оба типа задач имеют одинаковый жизненный цикл. Задачи контролируются Handler’ом и имеют состояния.
Хотя эти состояния не доступны извне, но при определенных обстоятельствах система выбрасывает исключения, при которых приложение “падает”. Данные поведения являются проблемой и головной болью для многих разработчиков и к сожалению не имеют простого решения. Для начала изучим состояния и жизненный цикл задач, а потом рассмотрим возможные решения.
Последовательность состояний задач
BINDING — состояние создания задачи(байндинг сервиса) тайм-аут 18 секунд.
STARTING — состояние запуска задачи, тайм-аут 8 секунд.
EXECUTING — состояние выполнения задачи, тайм-аут 10 минут.
STOPPING — состояние остановки задачи(например после вызова cancel()), тайм-аут 8 секунд.
FINISHED — финальное состояние завершенной задачи, последнее состояние в жизненном цикле задачи.
Упрощенная схема жизненного цикла задач
У каждого состояния задачи есть свой тайм-аут. По истечении тайм-аута выполнение задачи прерывается вне зависимости от ее состояния. Собственно это механизм тайм-аутов и является подводным камнем т.к. по истечении тайм-аута система выбрасывает исключение типа java.lang.SecurityException
и приложение “падает” со следующим сообщением Caller no longer running, last stopped +1s600ms because: timed out while starting
где +1s600ms
это время которое прошло с момента тайм-аута до момента выброса исключения, а “причина” (because: timed out while starting
) — указывает в каком состоянии была задача, когда тайм-аут истек.
Выводы
Как показывает опыт, данные исключения встречаются в довольно таки загруженных приложениях.
В подтверждение данной проблемы можно наблюдать как и на слабых, так и на топовых устройствах. На данную проблему так же указывает и выбрасываемые исключения с сообщениями о тайм-аутах. Соответственно, само собой напрашивается решение о разгрузке приложения и оптимизации использования JobIntentService’ов, например, избегать ситуаций когда паралельно запускаются несколько JobIntentService’ов. Второе решение, в некоторых случаях более тривиальное, а иногда и сложнее первого варианта — это использовать JobService.
Так же если погуглить данную проблему то можно наткнуться и на другие "сомнительные" варианты решения данной проблемы, например можно посмотреть следующие ссылки:
P. S.
На данный момент гугл готовит хорошую замену JobService’ам и JobIntentService’ам — это Worker и WorkManger из пакета androidx.work.
К сожалению данные инструменты еще не готовы к продакшену и имеют ряд багов, но уже сейчас как показали тесты, решают проблему описаную выше.