Site iconSite icon AppTractor

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

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

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

Как сломать WorkManager

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

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

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

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

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

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

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

мы можем получить сбой 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, вы можете пометить его как удаленный после того, как все ваши клиенты обновятся и мигрируют.

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

Использование 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 — очень удобный инструмент для фоновой работы, но будьте осторожны при удалении или переименовании воркеров.

Exit mobile version