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