Site 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