Connect with us

Программирование

Загадочная история сбоя WorkManager

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

Опубликовано

/

     
     

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

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

Как сломать WorkManager

Читая документацию, становится ясно, что WorkManager — это простое решение для фоновой работы:

WorkManager — рекомендуемое решение для непрерывной работы. Его функционирование является постоянным даже после перезапуска приложения или перезагрузки системы.

Это очень здорово!

Итак, если мы запланируем какую-то работу, например, загрузку данных креша:

val workerClass = CrashUploadWorker::class.java
WorkManager.getInstance(application)
    .enqueue(OneTimeWorkRequest.Builder(workerClass).build())

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

Однако WorkManager предполагает, что класс Worker всегда будет существовать в нашем приложении. Итак, если мы выпустим новую версию нашего приложения, которая либо:

  • удаляет CrashUploadWorker
  • переименовывает CrashUploadWorker в CrashReportWorker
  • перемещает CrashUploadWorker в новый пакет

мы можем получить сбой ClassNotFoundException после установки обновления!

java.lang.Error: java.lang.ClassNotFoundException: com.example.CrashUploadWorker
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1119)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
    at java.lang.Thread.run(Thread.java:818)

Происходит это потому, что WorkManager живет в отдельном процессе (Google Play Services) и всегда будет пытаться завершить свою работу. В данном случае он попытается создать экземпляр CrashUploadWorker, но его больше нет в нашем приложении.

К сожалению, мне пришлось понять этот на практике.

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

Как не сломать WorkManager

Первое, что вы можете попробовать, это отменить всю незавершенную работу для рабочего, которого вы удалили/переименовали:

workManager.cancelAllWorkByTag("crash_upload")

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

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

Альтернативный подход состоит в том, чтобы сохранить исходный класс CrashUploadWorker и изменить его для обработки изменившихся требований:

  • отменить работу (пустая реализация)
  • миграция и планирование нового класса воркера
internal class CrashUploadWorker(
  appContext: Context,
  workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {

  override suspend fun doWork(): Result {
    // Schedule new worker class
    val workerClass = CrashReportWorker::class.java
    WorkManager.getInstance(appContext)
      .enqueue(OneTimeWorkRequest.Builder(workerClass).build())
    return Result.success()
  }
}

Как только вы перестанете планировать работу с помощью старого Worker, вы можете пометить его как удаленный после того, как все ваши клиенты обновятся и мигрируют.

Вот как может выглядеть план миграции:

  • Версия 1: добавление нового воркера и перенос всей работы.
  • Версия 5: отмена всей оставшейся работы с использованием старого Worker (приводит к потере данных!)
  • Релиз 10: удаление старого Worker (вызывает сбои!)

Использование WorkerFactory

Альтернативный подход — предоставить пользовательский WorkerFactory для обработки миграции в новый класс.

Для этого сначала отключите автоматическую инициализацию WorkManager:

<provider
   android:name="androidx.startup.InitializationProvider"
   android:authorities="${applicationId}.androidx-startup"
   android:exported="false"
   tools:node="merge">
   <!-- If you are using androidx.startup to initialize other components -->
   <meta-data
       android:name="androidx.work.WorkManagerInitializer"
       android:value="androidx.startup"
       tools:node="remove" />
</provider>

Затем инициализируйте WorkManager в вашем Application#onCreate или ContentProvider:

val configuration = Configuration.Builder()
    .setWorkerFactory(MigrateWorkerFactory())
    .build()
WorkManager.initialize(appContext, configuration)

И создайте свой собственный WorkerFactory, который запланирует новый воркер:

class MigrateWorkerFactory() : WorkerFactory() {

  override fun createWorker(
    appContext: Context,
    workerClassName: String,
    workerParameters: WorkerParameters
  ): ListenableWorker? {
    if (workerClassName = "com.example.CrashUploadWorker") {
      return CrashReportWorker(appContext, workerParameters)
    }
    ...
  }
}

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

Итого

WorkManager — очень удобный инструмент для фоновой работы, но будьте осторожны при удалении или переименовании воркеров.

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Advertisement

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: