Irbisa · Шпаргалка

Фоновая работа и сервисы

WorkManager, foreground-сервисы, типы сервисов, AlarmManager, Doze, ограничения фона

Middle-разработчик · 22 вопроса

  1. 01

    Что такое Service, чем отличаются started-, bound- и foreground-сервисы и в каком потоке работает Service?

    Service представляет собой компонент приложения для работы без UI.

  2. 02

    Что такое WorkManager, что он гарантирует и чего не обещает?

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

    class UploadWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {  override suspend fun doWork(): Result {    val uri = inputData.getString("uri")?.toUri() ?: return Result.failure()    return try {      repo.upload(uri)      Result.success()…
  3. 03

    Чем OneTimeWorkRequest отличается от PeriodicWorkRequest и почему периодическую работу нельзя запускать каждые пять минут?

    OneTimeWorkRequest выполняется один раз и приходит в финальное состояние; PeriodicWorkRequest повторяется, пока его не отменят.

    // Raised to the 15-minute floor: no exception, only a logcat warningval tooFast = PeriodicWorkRequestBuilder<FeedWorker>(5, TimeUnit.MINUTES).build() // Once an hour, somewhere in the last 15 minutes of each hourval feed = PeriodicWorkRequestBuilder<FeedWorker>(  1, TimeUnit.HOURS,      // repeat interval…
  4. 04

    Как передать входные данные в Worker и получить результат обратно и почему нельзя передать картинку на 2 МБ через Data?

    Вход передаётся через setInputData(workDataOf(...)), а результат возвращается через Result.success(workDataOf(...)).

    // wrong: IllegalStateException, Data cannot occupy more than 10240 bytes// val bad = workDataOf("image" to jpegBytes) // right: the bytes go to disk, Data carries the pathval file = File(context.cacheDir, "upload-${UUID.randomUUID()}.jpg")file.writeBytes(jpegBytes)…
  5. 05

    Как выбрать между корутиной, WorkManager, foreground-сервисом и AlarmManager для фоновой задачи?

    Задайте два вопроса: должна ли работа пережить уход из приложения и нужен ли ей точный момент или присутствие пользователя?

  6. 06

    Этот Service выполняет ночную синхронизацию, а Activity привязывается к нему, чтобы читать прогресс. Что здесь не так и что должно выполнять синхронизацию?

    class SyncService : Service() {  private var percent = 0  inner class LocalBinder : Binder() { fun progress() = percent }   override fun onBind(intent: Intent?): IBinder = LocalBinder()   override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {    repo.syncBlocking { percent = it }    return START_STICKY  }} // Called when the app has no visible UI and no recent user interaction (e.g. from a sync callback):context.startService(Intent(context, SyncService::class.java))

    Почти каждая строка: syncBlocking выполняется в главном потоке, startService() из фона на Android 8+ бросает IllegalStateException, а START_STICKY не гарантирует надёжности.

  7. 07

    Как WorkManager устроен внутри: где хранится работа, что её реально запускает и как она переживает перезагрузку?

    Каждый запрос хранится строкой в собственной Room-базе WorkManager, а планировщики передают его системному JobScheduler или запускают прямо в процессе, пока приложение живо.

  8. 08

    Этот Worker копирует файлы после остановки WorkManager. Почему, и как Worker, CoroutineWorker и ListenableWorker реагируют на остановку?

    class CopyWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {  override fun doWork(): Result {    for (file in files) {      copy(file)    }    return Result.success()  }}

    Остановка кооперативная.

  9. 09

    Найдите баг: Worker загрузки всегда завершается как SUCCEEDED, но часть фотографий так и не доходит до сервера.

    class UploadWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {  override fun doWork(): Result {    val paths = inputData.getStringArray("paths") ?: return Result.failure()    CoroutineScope(Dispatchers.IO).launch {      for (path in paths) {        api.upload(File(path))        File(path).delete()      }    }    return Result.success()  }}

    doWork() запускает загрузки в собственном scope и сразу возвращает Result.success(), поэтому WorkManager помечает работу как SUCCEEDED и перестаёт за ней следить.

  10. 10

    Как работают ограничения (constraints) в WorkManager и что происходит с запущенным Worker, когда ограничение перестаёт выполняться?

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

    val request = OneTimeWorkRequestBuilder<BackupWorker>()  .setConstraints(    Constraints.Builder()      .setRequiredNetworkType(NetworkType.UNMETERED)  // watched during the whole run      .setRequiresBatteryNotLow(true)      .setRequiresStorageNotLow(true)…
  11. 11

    Этот код выполняется в Application.onCreate. Почему у пользователей дублируются синхронизации и почему новый двухчасовой интервал так и не вступает в силу?

    val wm = WorkManager.getInstance(this) wm.enqueue(OneTimeWorkRequestBuilder<SyncWorker>().build()) wm.enqueueUniquePeriodicWork(  "sync",  ExistingPeriodicWorkPolicy.KEEP,  PeriodicWorkRequestBuilder<SyncWorker>(2, TimeUnit.HOURS).build(),)

    Обычный enqueue() добавляет новый запрос при каждом старте, а ExistingPeriodicWorkPolicy.KEEP игнорирует новый запрос, пока старое расписание существует, поэтому побеждает интервал с первой установки.

  12. 12

    Как работают повторы и backoff в WorkManager и как не дать Worker повторяться бесконечно?

    Result.retry() откладывает работу на backoff-задержку, и лимита попыток нет: остановить цикл может только ваш код.

    class SyncWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {   override suspend fun doWork(): Result = try {    api.sync()    Result.success()  } catch (e: HttpException) {…
  13. 13

    Как связать Worker-ы в цепочку, чтобы три загрузки шли параллельно, а публикация запускалась после всех, и что будет, если одна загрузка упадёт?

    beginWith(listOf(a, b, c)).then(publish).enqueue(): список в beginWith выполняется параллельно, then ждёт их всех, и publish стартует, только когда все три успешны.

    val uploads = photos.map { photo ->  OneTimeWorkRequestBuilder<UploadWorker>()    .setInputData(workDataOf("path" to photo.path))    .build()}…
  14. 14

    Как показывать прогресс длительного Worker и когда ему нужны setForeground и тип foreground-сервиса?

    Внутри Worker вызывайте setProgress(workDataOf(...)), а WorkInfo.progress наблюдайте через Flow по уникальному имени работы.

    class UploadWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {   override suspend fun doWork(): Result {    setForeground(getForegroundInfo())  // long run: become a foreground service first    val files = dao.pending()    files.forEachIndexed { i, file ->…
  15. 15

    Что даёт setExpedited и что решает OutOfQuotaPolicy?

    setExpedited просит систему запустить one-time запрос как можно скорее за счёт ограниченной квоты приложения.

    val sync = OneTimeWorkRequestBuilder<SyncWorker>()  .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)  .setConstraints(    Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()  )  // .setInitialDelay(...) here would throw: expedited work cannot be delayed…
  16. 16

    Что изменилось для foreground-сервисов в Android 12, 14 и 15 и чего требует тип foreground-сервиса?

    Android 12 запретил запускать foreground-сервис из фона, кроме исключений.

    // AndroidManifest.xml// <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />// <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />// <service android:name=".UploadService"//     android:foregroundServiceType="dataSync" android:exported="false" />…
  17. 17

    Что такое Doze mode и App Standby buckets и какие ограничения фоновой работы Android добавил начиная с 8.0?

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

  18. 18

    Найдите баг: трекинг пробежки работает в офисном Wi-Fi, но на слабой мобильной сети приложение падает через несколько секунд после нажатия «Старт».

    // Started from the Run screen: ContextCompat.startForegroundService(context, intent)class RunTrackingService : Service() {  private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)  override fun onBind(intent: Intent?): IBinder? = null  override fun onDestroy() = scope.cancel()  override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {    scope.launch {      val route = routeRepo.loadActiveRoute() // HTTP call      ServiceCompat.startForeground(this@RunTrackingService, NOTIFICATION_ID,        buildNotification(route.name), ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION)      tracker.track(route)    }    return START_NOT_STICKY  }}

    startForeground() вызывается только после сетевого запроса, а сервис, запущенный через startForegroundService(), обязан перейти в foreground примерно за пять секунд.

  19. 19

    Как сделать будильник, который зазвонит в 6:00, даже если телефон всю ночь был в Doze, а в 3:00 перезагрузился?

    Поставить точный будильник через setAlarmClock, получить разрешение на точные будильники и в boot-ресивере заново построить все будильники из своей базы: AlarmManager забывает их при перезагрузке и обновлении приложения.

    class AlarmScheduler(private val ctx: Context, private val dao: AlarmDao) {  private val am = ctx.getSystemService(AlarmManager::class.java)   fun armNext(): Boolean {    val next = dao.nextEnabled() ?: return true    // false: the UI sends the user to Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM…
  20. 20

    Ночная синхронизация стабильно работает на Pixel, но не запускается на многих телефонах Xiaomi и Samsung. Почему и что с этим реально можно сделать?

    Поверх Doze и App Standby многие производители ставят свои батарейные менеджеры, которые замораживают или убивают приложения вне белого списка (вместе с задачами и будильниками), и никакой API или targetSdk от этого не избавляет.

  21. 21

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

    Push служит сигналом «проснись», а не самой работой: onMessageReceived ставит уникальную задачу WorkManager и возвращает управление, а с высоким приоритетом отправляются только сообщения, которые действительно не могут ждать.

    class AppMessagingService : FirebaseMessagingService() {   override fun onMessageReceived(message: RemoteMessage) {    // Did FCM honour our high priority, or downgrade it?    if (message.priority != message.originalPriority) {      analytics.log("fcm_priority_downgraded")…
  22. 22

    Один пользователь говорит, что воркер синхронизации никогда не запускается, а у вас не воспроизводится. Как узнать, что планировщик на самом деле сделал с задачей?

    Спросить планировщик напрямую: adb shell dumpsys jobscheduler показывает, какие ограничения не выполнены и когда задача может запуститься, а диагностический broadcast WorkManager логирует его взгляд на работу.

    # What did the scheduler decide about our job?adb shell dumpsys jobscheduler | grep -A 30 "com.example.app"#   Required constraints: CONNECTIVITY BATTERY_NOT_LOW#   Satisfied constraints: BATTERY_NOT_LOW     <- no network: this is the answer#   Standby bucket: RARE…