Главная » Хабрахабр » Эволюция планировщиков задач

Эволюция планировщиков задач

За это время мобильной команде пришлось пережить множество разных подходов и миграций между инструментами, а год назад появилось время перейти с самописного решения и посмотреть в сторону чего-то более «модного» и распространённого. Приложение iFunny, над которым мы работаем, доступно в сторах уже более пяти лет. Эта статья — небольшая выжимка о том, что было изучено, на какие решения смотрели и к чему в итоге пришли.

Зачем нам это всё?

Давайте сразу определимся, в честь чего эта статья и почему эта тема оказалась важной для команды Android-разработки:

  1. есть множество сценариев, когда необходимо запускать задачи вне рамок активного пользовательского интерфейса;
  2. система накладывает большое количество ограничений на запуск подобных задач;
  3. выбрать между существующими решениями оказалось довольно сложно, так как каждый инструмент имеет свои плюсы и минусы.

Хронология развития событий

Android 0

AlarmManager, Handler, Service

Изначально были реализованы свои решения для запуска бэкграунд-задач на основе сервисов. Также был механизм, который привязывал задачи к жизненному циклу и умел отменять и восстанавливать их. Команду это долгое время устраивало, так как никаких ограничений платформа к таким задачам не предъявляла.
В Google же советовали это делать, опираясь на следующую диаграмму:

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

Плюсы:
доступно везде;
доступно для всех.

Lollipop
Минусы:
система всячески ограничивает работу;
нет запусков по условию;
API минимальное и нужно писать много кода.
Android 5.

JobScheduler

Спустя 5(!) лет, ближе к 2015 году в Google заметили, что задачи запускаются неэффективно. Пользователи стали регулярно жаловаться, что их телефоны разряжаются, просто лёжа на столе или в кармане.

Это механизм, с чьей помощью можно в фоне выполнять различную работу, начало выполнения которой оптимизировалось и упрощалось за счёт централизованной системы запуска этих задач и возможности задавать условия для этого самого запуска. С выходом Android 5 появился такой инструмент, как JobScheduler.

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

public class JobSchedulerService extends JobService @Override public boolean onStopJob(JobParameters params) { return false; }
}

Из любого места в приложении вы можете инициировать выполнение этой работы. Задачи выполняются в нашем процессе, но инициируются на уровне IPC. Есть централизованный механизм, который управляет их выполнением и будит приложение только в необходимые для этого моменты. Также можно задавать различные условия запуска и передавать данные через Bundle.

JobInfo task = new JobInfo.Builder(JOB_ID, serviceName) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) .setRequiresDeviceIdle(true) .setRequiresCharging(true) .build();
JobScheduler scheduler = (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE);
scheduler.schedule(task);

В общем, по сравнению с ничем это было уже кое-что. Но этот механизм доступен только с API 21, и на момент выхода Android 5.0 было бы странно перестать поддерживать все старые девайсы (прошло 3 года, а мы до сих пор поддерживаем четвёрки).

Плюсы:
API простое;
условия для запуска.

Минусы:
доступно начиная с API 21;
фактически только с API 23;
легко ошибиться.

Lollipop
Android 5.

GCM Network Manager

Также был представлен аналог JobScheduler — GCM Network Manager. Это библиотека, которая предоставляла схожий функционал, но работала уже с API 9. Правда, взамен требовала наличие Google Play Services. Видимо, функционал, необходимый для работы JobScheduler, стали поставлять не только через версию Android, но и на уровне GPS. Надо отметить, что разработчики фреймворка очень быстро одумались и решили не связывать своё будущее с GPS. Спасибо им за это.

Такой же сервис: Выглядит всё абсолютно идентично.

public class GcmNetworkManagerService extends GcmTaskService { @Override public int onRunTask(TaskParams taskParams) { doWork(taskParams); return 0; }
}

Такой же запуск задач:

OneoffTask task = new OneoffTask.Builder() .setService(GcmNetworkManagerService.class) .setTag(TAG) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) .setRequiresCharging(true) .build(); GcmNetworkManager mGcmNetworkManager = GcmNetworkManager.getInstance(this);
mGcmNetworkManager.schedule(task);

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

Плюсы:
API, аналогичное JobScheduler;
доступно начиная с API 9.

Минусы:
необходимо иметь Google Play Services;
легко ошибиться.

Lollipop
Android 5.

WakefulBroadcastReceiver

Далее напишу пару слов об одном из базовых механизмов, который используется в JobScheduler и доступен разработчикам напрямую. Это WakeLock и основанный на нём WakefulBroadcastReceiver.

Это необходимо, если мы хотим выполнить какую-то важную работу.
При создании WakeLock можно указать его настройки: держать CPU, экран или клавиатуру. С помощью WakeLock можно запретить системе уходить в suspend, то есть держать девайс в активном состоянии.

PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE)
PowerManager.WakeLock wl = pm.newWakeLock(PARTIAL_WAKE_LOCK, "name")
wl.acquire(timeout);

На основе этого механизма работает WakefulBroadcastReceiver. Мы запускаем сервис и удерживаем WakeLock.


public class SimpleWakefulReceiver extends WakefulBroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Intent service = new Intent(context, SimpleWakefulService.class); startWakefulService(context, service); }
}

После того как сервис выполнил необходимую работу, мы отпускаем его через аналогичные методы.

Через 4 версии этот BroadcastReceiver станет deprecated, и на developer.android.com будут описаны следующие альтернативы:

  • JobScheduler;
  • SyncAdapter;
  • DownloadManager;
  • FLAG_KEEP_SCREEN_ON для Window.

Android 6. Marshmallow

DozeMode: сон на ходу

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

В первых версиях это длилось час, в последующих длительность сна уменьшили до 30 минут. Первым делом появился DozeMode, который переводит устройство в спящий режим, если оно лежало без действий определённое время. Окно DozeMode увеличивается экспоненциально. Периодически телефон просыпается, выполняет все отложенные задачи и снова засыпает. Все переходы между режимами можно отследить через adb.

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

  • система игнорирует все WakeLock;
  • откладывается AlarmManager;
  • JobScheduler не работает;
  • SyncAdapter не работает;
  • доступ в сеть ограничен.

Также вы можете добавить ваше приложение в whitelist, чтобы оно не попадало под ограничения DozeMode, но как минимум Samsung полностью игнорировал этот список.

Marshmallow
Android 6.

AppStandby: неактивные приложения

Система определяет приложения, которые являются неактивными, и накладывает на них все те же ограничения, что и в рамках DozeMode.
Приложение отправляется в изоляцию, если:

  • не имеет процесса на переднем плане;
  • не имеет активной нотификации;
  • не добавлено в список исключений.

Android 7. Nougat

Background Optimizations. Svelte

Svelte — это проект, в рамках которого Google пытается оптимизировать потребление оперативной памяти приложениями и самой системой.
В Android 7 в рамках этого проекта было решено, что неявные бродкасты не очень эффективны, так как их слушает огромное количество приложений и система тратит большое количество ресурсов при наступлении этих событий. Поэтому следующие типы событий были запрещены для объявления в манифесте:

  • CONNECTIVITY_ACTION;
  • ACTION_NEW_PICTURE;
  • ACTION_NEW_VIDEO.

Android 7. Nougat

FirebaseJobDispatcher

В это же время была опубликована новая версия фреймворка для запуска задач — FirebaseJobDispatcher. На самом деле это был дописанный GCM NetworkManager, который немного привели в порядок и сделали чуть более гибким.

Такой же сервис: Визуально всё выглядело точно так же.

public class JobSchedulerService extends JobService { @Override public boolean onStartJob(JobParameters params) { doWork(params); return false; } @Override public boolean onStopJob(JobParameters params) { return false; }
}

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

Сам же запуск задач с течением времени не изменился.

FirebaseJobDispatcher dispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context)); Job task = dispatcher.newJobBuilder() .setService(FirebaseJobDispatcherService.class) .setTag(TAG) .setConstraints(Constraint.ON_UNMETERED_NETWORK, Constraint.DEVICE_IDLE) .build(); dispatcher.mustSchedule(task);

Плюсы:
API, аналогичное JobScheduler;
доступно начиная с API 9.

Минусы:
необходимо иметь Google Play Services;
легко ошибиться.


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

Google знает об этом, но эти задачи несколько лет остаются открытыми.

Nougat
Android 7.

Android Job by Evernote

В итоге сообщество не выдержало, и появилось самописное решение в виде библиотеки от Evernote. Оно было не единственное, но именно решение от Evernote смогло зарекомендовать себя и «выбилось в люди».

В случае с JobScheduler они создавались через reflection. В архитектурном плане эта библиотека была удобнее своих предшественников.
Появилась сущность, отвечающая за создание задач.

class SendLogsJobCreator : JobCreator { override fun create(tag: String): Job? { when (tag) { SendLogsJob.TAG -> return SendLogsJob() } return null }
}

Имеется отдельный класс, который является самой задачей. В JobScheduler это всё было свалено в switch внутри onStartJob.

class SendLogsJob : Job() { override fun onRunJob(params: Params): Result { return doWork(params) }
}

Запуск задач идентичен, но кроме унаследованных событий Evernote ещё добавил и свои, такие как запуск ежедневных задач, уникальные задачи, запуск в рамках окна.

new JobRequest.Builder(JOB_ID) .setRequiresDeviceIdle(true) .setRequiresCharging(true) .setRequiredNetworkType(JobRequest.NetworkType.UNMETERED) .build() .scheduleAsync();

Плюсы:
удобное API;
поддерживается на всех версиях;
не нужны Google Play Services.

Минусы:
стороннее решение.


Ребята активно поддерживали свою библиотеку. Хотя было довольно много критичных проблем, она работала на всех версиях и на всех девайсах. В итоге в прошлом году наша Android-команда выбрала решение именно от Evernote, так как библиотеки от Google срезают большой пласт девайсов, которые они не могут поддержать.
Внутри себя же она работала на решениях от Google, в крайних случаях — с AlarmManager.

Oreo
Android 8.

Background Execution Limits

Вернёмся к нашим ограничениям. С приходом нового Android пришли и новые оптимизации. Ребята из Google нашли другую проблему. В этот раз всё дело оказалось в сервисах и бродкастах (да, ничего нового).

  • startService если приложения в фоне
  • implicit broadcast в манифесте

Во-первых, было запрещено запускать сервисы из фона. В «рамках закона» остались только foreground services. Сервисы теперь, можно сказать, deprecated.
Второе ограничение — всё те же бродкасты. В этот раз стала запрещена регистрация ВСЕХ неявных бродкастов в манифесте. Неявный бродкаст — это бродкаст, который предназначается не только нашему приложению. Например, есть Action ACTION_PACKAGE_REPLACED, а есть ACTION_MY_PACKAGE_REPLACED. Так вот, первый — это неявный.

Но любой бродкаст по-прежнему можно зарегистрировать через Context.registerBroadcast.

Pie
Android 9.

WorkManager

На этом оптимизации пока прекратились. Возможно, устройства стали работать быстро и бережно в плане энергопотребления; возможно, пользователи стали меньше жаловаться на это.
В Android 9 разработчики фреймворка основательно подошли к инструменту для запуска задач. В попытке решить все насущные проблемы, на Google I/O была представлена библиотека для запуска бэкграунд-задач WorkManager.

Так появились архитектурные компоненты с LiveData, ViewModel и Room. Google последнее время пытается сформировать своё видение архитектуры Android-приложения и даёт разработчикам инструменты, необходимые для этого. WorkManager выглядит как разумное дополнение их подхода и парадигмы.

По сути это обёртка уже существующих решений: JobScheduler, FirebaseJobDispatcher и AlarmManager. Если же говорить про то, как устроен WorkManager внутри, то никакого технологического прорыва в нём нет.

createBestAvailableBackgroundScheduler

static Scheduler createBestAvailableBackgroundScheduler(Context, WorkManager) { if (Build.VERSION.SDK_INT >= MIN_JOB_SCHEDULER_API_LEVEL) { return new SystemJobScheduler(context, workManager); } try { return tryCreateFirebaseJobScheduler(context); } catch (Exception e) { return new SystemAlarmScheduler(context); }
}

Код выбора довольно прост. Но надо заметить, что JobScheduler доступен начиная с API 21, но используют его только с API 23, так как первые версии были довольно нестабильные.

Если версия ниже 23, то через reflection пробуем найти FirebaseJobDispatcher, в противном случае используем AlarmManager.

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

  • Worker — логика работы;
  • WorkRequest — логика запуска задачи;
  • WorkRequest.Builder — параметры;
  • Constrains — условия;
  • WorkManager — менеджер, который управляет задачами;
  • WorkStatus — статус задачи.

К тому же можно подписаться на изменение не только определённого URI, но и всех вложенных в него с помощью флага в методе. Условия для запуска наследовались от JobScheduler.
Можно отметить, что триггер на изменение URI появился только с API 23.

В Evernote есть пара критичных багов, которые разработчики библиотеки обещают поправить с переходом на версию с интегрированным WorkManager. Если говорить о нас, то ещё на этапе альфы было решено перейти на WorkManager.
Причин для этого несколько. К тому же это решение хорошо вписывается в нашу архитектуру, так как мы используем Architecture Components. Да и сами они соглашаются, что решение от Google сводит на нет плюсы Evernote.

При этом не сильно критично, WorkManager у вас или JobScheduler. Далее хотелось бы на простом примере показать, в каком виде мы стараемся использовать этот подход.

Посмотрим на пример с очень простым кейсом: клик по republish или like.

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

Затем в фоне идёт запрос на сервер, при неудаче которого данные сбрасываются в начальное состояние. В таких случаях сначала изменяются локальные данные — пользователь сразу видит результат своего действия.

Далее покажу пример того, как это выглядит у нас.

В его методах описывается конфигурация задач и передаются параметры. JobRunner содержит логику запуска задач.

JobRunner.java

fun likePost(content: IFunnyContent) { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val input = Data.Builder() .putString(LikeContentJob.ID, content.id) .build() val request = OneTimeWorkRequest.Builder(LikeContentJob::class.java) .setInputData(input) .setConstraints(constraints) .build() WorkManager.getInstance().enqueue(request)
}

Сама задача в рамках WorkManager выглядит следующим образом: берём id из параметров и вызываем метод на сервере, чтобы поставить лайк на этот контент.

У нас есть базовый класс, который содержит следующую логику:

abstract class BaseJob : Worker() { final override fun doWork(): Result { val workerInjector = WorkerInjectorProvider.injector() workerInjector.inject(this) return performJob(inputData) } abstract fun performJob(params: Data): Result
}

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

WorkerInjectorImpl.java

@Singleton
public class WorkerInjectorImpl implements WorkerInjector { @Inject public WorkerInjectorImpl() {} @Ovierride public void inject(Worker job) { if (worker instanceof AppCrashedEventSendJob) { Injector.getAppComponent().inject((AppCrashedEventSendJob) job); } else if (worker instanceof CheckNativeCrashesJob) { Injector.getAppComponent().inject((CheckNativeCrashesJob) job); } }
}

Он просто проксирует вызовы в Dagger, но это нам помогает при тестировании: мы подменяем реализации инжектора и внедряем в задачи необходимое окружение.

fun void testRegisterPushProvider() { WorkManagerTestInitHelper.initializeTestWorkManager(context) val testDriver = WorkManagerTestInitHelper.getTestDriver() WorkerInjectorProvider.setInjector(TestInjector()) // mock dependencies val id = jobRunner.runPushRegisterJob() testDriver.setAllConstraintsMet(id) Assert.assertTrue(…)
}

class LikePostInteractor @Inject constructor( val iFunnyContentDao: IFunnyContentDao, val jobRunner: JobRunner) : Interactor { fun execute() { iFunnyContentDao.like(getContent().id) jobRunner.likePost(getContent()) }
}

Interactor — это сущность, которую дёргает ViewController, чтобы инициировать прохождение сценария (в данном случае —поставить лайк). Мы отмечаем локально контент как «залайканный» и отправляем задачу на выполнение. Если задача происходит неуспешно, то лайк снимается.

class IFunnyContentViewModel(val iFunnyContentDao: IFunnyContentDao) : ViewModel() { val likeState = MediatorLiveData<Boolean>() var iFunnyContentId = MutableLiveData<String>() private var iFunnyContentState: LiveData<IFunnyContent> = attachLiveDataToContentId(); init { likeState.addSource(iFunnyContentState) { likeState.postValue(it!!.hasLike) } }
}

Мы используем Architecture Components от Google: ViewModel и LiveData. Так выглядит наша ViewModel. Здесь мы связываем обновление объекта в DAO со статусом лайка.

IFunnyContentViewController.java

class IFunnyContentViewController @Inject constructor( private val likePostInteractor: LikePostInteractor, val viewModel: IFunnyContentViewModel) : ViewController { override fun attach(view: View) { viewModel.likeState.observe(lifecycleOwner, { updateLikeView(it!!) }) } fun onLikePost() { likePostInteractor.setContent(getContent()) likePostInteractor.execute() }
}

ViewController, с одной стороны, подписывается на изменение статуса лайка, с другой — инициирует прохождение нужного нам сценария.

Осталось дописать поведение самой View с лайком и реализацию вашего DAO; если вы используете Room, то просто прописать поля в объекте. И это практически весь код, необходимый нам. Выглядит довольно просто и эффективно.

Если подводить итоги

JobScheduler, GCM Network Manager, FirebaseJobDispatcher:

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

Android Job by Evernote:

  • внутри будут использовать WorkManager;
  • критичные баги размываются между решениями.

WorkManager:

  • API LEVEL 9+;
  • не зависит от Google Play Services;
  • Chaining/InputMergers;
  • реактивный подход;
  • поддержка от Google (хочется в это верить).

Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

LockerGoga: что именно произошло с Norsk Hydro

Фото, источник — www.msspalert.com 03. В ночь с понедельника на вторник (примерно в 23:00 UTC 18. Вскоре стало понятно, что сбои вызваны массовым заражением систем шифровальщиком, которое очень быстро распространялось по объектам инфраструктуры. 2019) специалисты Norsk Hydro заметили сбои в ...

[Перевод] Каким был первый айфон?

Фото: Tom Warren Многие видели его презентацию, на которой Стив Джобс, в частности, заявил, что новый смартфон разрабатывали два с половиной года. Странный вопрос. Вскоре после этого новинка появилась на прилавках салонов связи. Любопытно посмотреть, какие прототипы были у современных ...