在Service中使用WorkManager

2,546 阅读1分钟

前言

  本文主要讲述在Service中使用WorkManager执行周期性任务遇到的问题和解决方法。

场景

  大屏物联网设备上两个应用A和B,A应用负责业务,随系统自启动(始终位于前台),B应用作为A应用的守护进程(始终处于后台运行状态),负责后台安装A应用下载后的安装包以及周期性的执行一些任务。

正文

声明依赖项

dependencies {
  def work_version = "2.3.1"

    // (Java only)
    implementation "androidx.work:work-runtime:$work_version"

    // Kotlin + coroutines
    implementation "androidx.work:work-runtime-ktx:$work_version"

    // optional - RxJava2 support
    implementation "androidx.work:work-rxjava2:$work_version"

    // optional - GCMNetworkManager support
    implementation "androidx.work:work-gcm:$work_version"

    // optional - Test helpers
    androidTestImplementation "androidx.work:work-testing:$work_version"
 }

  根据自己需求声明相应的依赖项

构建执行任务的Worker工作器


class UploadWorker(appContext: Context, workerParams: WorkerParameters)
    : Worker(appContext, workerParams) {

    override fun doWork(): Result {
        // Do the work here--in this case, upload the images.

        uploadImages()

        // Indicate whether the task finished successfully with the Result
        return Result.success()
    }
}

  官方给出的示例如上,达到执行条件后,doWork()将会被执行。我们可以根据任务的执行情况返回相应状态结果,Result有三种状态结果可以返回:当任务正常执行完毕时,我们返回Result.success(outputData),执行中出现异常时根据实际情况返回Result.failure(outputData)或Result.retry()。

问题一:doWork()通常用于处理异步耗时任务,如果需要准确地获取工作结果,如何在异步回调中返回结果呢?

例如:

...
override fun doWork(): Result {
    uploader.uploadImages(object: Callback{
        override fun onSuccess(data: String){
            ...
        }
        
        override fun onFailed(){
            ...
        }
    })
    return ??
}

解决方法

  我们可以使用CountDownLatch或CyclicBarrier同步锁机制将异步回调同步处理,在周期性任务情况下,考虑到复用性,以CyclicBarrier为例,如下:

private val cyclicBarrier = CyclicBarrier(2)
private var workResult: Result = Result.success()

uploader.uploadImages(object: Callback{
    override fun onSuccess(data: String){
        ...
        workResult = Result.success(outputData)
        cyclicBarrier.await()
    }
    
    override fun onFailed(){
        ...
        workResult = Result.failure(outputData)
        cyclicBarrier.await()
    }
})

cyclicBarrier.await(5,TimeUnit.MINUTES) //设置等待uploadImages返回结果最多5分钟

return workResult

定义WorkRequest工作请求


  如下,定义一个只有在有可用网络的情况下重复间隔为一小时的定期工作请求:

val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .build()

val saveRequest =
PeriodicWorkRequestBuilder<SaveImageToFileWorker>(1, TimeUnit.HOURS)
    .setConstraints(constraints)
    .build()

WorkManager.getInstance(myContext)
    .enqueue(saveRequest)

⚠️注意:可以定义的最短重复间隔是 15 分钟(与 JobScheduler API 相同),工作器的确切执行时间取决于在工作请求中使用的约束,也取决于系统进行的优化。实测,在Android 7.1原生系统,在满足约束的情况下,大多数时候实际执行的周期与设置的周期相差在2分钟以内,WorkManager并不适合用于对执行时间精确度要求高的情况。

  如上,调用WorkManager的enqueue(saveRequest)将工作请求放入队列,如果此时满足设置的约束,将会执行Worker中doWork(),否则会等到满足约束条件开始执行。

观察工作状态


  经过前边两步,我们设置约束和周期的后台任务已经开始执行,如果想要获取工作状态和工作进度、结果,还要通过id或tag获取到对应的WorkRequest并注册监听器,如下:

WorkManager.getInstance(myContext).getWorkInfoByIdLiveData(uploadWorkRequest.id)
    .observe(lifecycleOwner, Observer { workInfo ->
        if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
            displayMessage("Work finished!")
        }
    })

⚠️注意:如果工作器返回 Result.success(),则被视为处于 SUCCEEDED 状态,相反,如果工作器返回 Result.failure(),则被视为处于 FAILED 状态。SUCCEEDED和FAILED为工作终止状态,只有OneTimeWorkRequest可以进入终止状态,也就是只有OneTimeWorkRequest持有Worker时,才可以获取到Result.success(outputData)和Result.failure(outputData)中返回的outputData

问题二:PeriodicWorkRequest中如何获取工作状态和进度?

WorkManager 2.3.0-alpha01 为设置和观察工作器的中间进度添加了一流支持。如果应用在前台运行时,工作器保持运行状态,也可以使用返回 WorkInfo 的 LiveData 的 API 向用户显示此信息。

ListenableWorker 现在支持 setProgressAsync() API,此类 API 可以保留中间进度。借助这些 API,开发者能够设置可通过界面观察到的中间进度。进度由 Data 类型表示,这是一个可序列化的属性容器(类似于 input 和 output,并且受到相同的限制)。

解决方法:

  由此可知,在Worker中可以通过setProgressAsync()来设置进度和数据。setProgressAsync()是一个异步操作方法,会调用ProgressUpdater的updateProgress()操作数据库,所以当doWork()中的异步任务完成时,如果调用setProgressAsync()设置进度结果后直接return Result.success()或Result.failure()时将获取不到通过setProgressAsync()设置的数据,因为这个时候PeriodicWorkRequest的WorkInfo状态是ENQUEUED。

⚠️注意:只有WorkInfo的状态为RUNNING时才可以获取到setProgressAsync()设置的进度数据。

代码如下:


class UpgradeFirmwareWorker(appContext: Context,workerParams: WorkerParameters): Worker(appContext,workerParams) {

    private var resultMessage = ""
    private var running = AtomicBoolean(false)
    private val outData = Data.Builder()
    private val cyclicBarrier = CyclicBarrier(2)
    private var workResult: Result = Result.success()

    override fun doWork(): Result {
        setProgressAsync(outData.putString(KEY_RESULT_MESSAGE,"正在检查升级").build())
        if (running.compareAndSet(false,true)) {
            MTAUpdate.checkUpdate(object : UpdateListener {
                override fun onFailed(e: UpdateException, firmwareName: FirmwareName) {
                    running.set(false)
                    resultMessage = "result:${e.message},firmwareName:$firmwareName,running:${running.get()}"
                    setProgressAsync(outData.putString(KEY_RESULT_MESSAGE,e.message).build())
                    workResult = Result.failure()
                    Thread.sleep(1000)
                    cyclicBarrier.await()
                }

                override fun onSuccess(result: UpdateResult) {
                    running.set(false)
                    resultMessage = result.toString()+",running:${running.get()}"
                    setProgressAsync(outData.putString(KEY_RESULT_MESSAGE,result.toString()).build())
                    workResult = Result.success()
                    Thread.sleep(1000)
                    cyclicBarrier.await()
                }
            })
            try {
                cyclicBarrier.await(5,TimeUnit.MINUTES)
            } catch (e: InterruptedException){
                e.printStackTrace()
            } catch (e: TimeoutException){
                logi("等待超时,checkUpdate未返回结果")
            }

        } else {
            logd("UpgradeFirmwareWorker","正在检查升级")
        }
        return workResult
    }

    companion object{
        const val KEY_RESULT_MESSAGE = "result_message"
    }
}

  上边的代码,在调用setProgressAsync()后使用Thread.sleep(1000)暂停一秒,确保不会立即调用return Result.success()或Result.failure(),这样在监听器中通过判断WorkInfo.State == RUNNING时,调用WorkInfo的getProgress()可以获取到工作进度。

Service中代码如下:

WorkManager.getInstance(applicationContext).apply {
            enqueueUniquePeriodicWork(REQUEST_TAG,ExistingPeriodicWorkPolicy.KEEP,upgradeRequest)
            getWorkInfoByIdLiveData(upgradeRequest.id)
                    .observe(this@URMService, Observer {
                        if (it != null && it.state == WorkInfo.State.RUNNING){
                            val progress = it.progress
                            val value = progress.getString(UpgradeFirmwareWorker.KEY_RESULT_MESSAGE)
                            progressListener?.onProgress(value)
                        }
                    })
        }

  enqueueUniquePeriodicWork()作用是确保队列中工作请求的唯一性,第二个参数ExistingPeriodicWorkPolicy.KEEP指定当队列中有同样的工作请求时忽略新的请求,保持已有工作继续执行。

绑定Service生命周期


  前边提到观察工作状态可以通过getWorkInfoByIdLiveData()或getWorkInfosByTagLiveData()获取工作请求,此时返回持有WorkInfo的LiveData,LiveData的observe(lifecycleOwner,observer)需要传入lifecycleOwner(生命周期的持有者)和observer(观察者)。

问题三:如何在Service中提供lifecycleOwner?

解决方法:

  在androidx.appcompat:appcompat兼容库中已经提供了实现了LifecycleOwner接口的AppCompatActivity和Fragment,在Service中我们需要自行实现LifecycleOwner,绑定Service的生命周期,具体实现如下:

class URMService : Service(),LifecycleOwner {

    private lateinit var lifecycleRegistry: LifecycleRegistry
    private lateinit var upgradeRequest: PeriodicWorkRequest

    override fun onCreate() {
        super.onCreate()
        lifecycleRegistry = LifecycleRegistry(this)
        lifecycleRegistry.currentState = Lifecycle.State.CREATED
        
        val constraints = Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build()
        upgradeRequest = PeriodicWorkRequestBuilder<UpgradeFirmwareWorker>(20,TimeUnit.MINUTES)
                .addTag(REQUEST_TAG)
                .setConstraints(constraints)
                .build()
        WorkManager.getInstance(applicationContext).apply {
            enqueueUniquePeriodicWork(REQUEST_TAG,ExistingPeriodicWorkPolicy.KEEP,upgradeRequest)
            getWorkInfoByIdLiveData(upgradeRequest.id)
                    .observe(this@URMService, Observer {
                        if (it != null && it.state == WorkInfo.State.RUNNING){
                            val progress = it.progress
                            val value = progress.getString(UpgradeFirmwareWorker.KEY_RESULT_MESSAGE)
                            progressListener?.onProgress(value)
                        }
                    })
        }
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        lifecycleRegistry.currentState = Lifecycle.State.STARTED
        return START_STICKY
    }

    override fun onBind(intent: Intent): IBinder? {
        return binder
    }

    override fun onDestroy() {
        lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
        WorkManager.getInstance(applicationContext).cancelAllWork()
    }

    override fun getLifecycle(): Lifecycle {
        return lifecycleRegistry
    }
}

  LifecycleOwner接口只有一个getLifecycle(),该方法返回一个代表Android生命周期的Lifecycle对象。Lifecycle是一个抽象类,在androidx.lifecycle包中已经为我们提供了Lifecycle的实现类LifecycleRegistry,androidx.appcompat:appcompat兼容库中提供的Activity和Fragment也是使用了LifecycleRegistry。我们只需要实例化LifecycleRegistry,然后在Service的onCreate()、onStartCommand()、onDestroy()中调用LifecycleRegistry的setCurrentState(State state)传入对应的Lifecycle State即可。

小结


  • 使用 WorkManager API 可以轻松地调度即使在应用退出或设备重启时仍应运行的可延迟异步任务。
  • WorkManager 旨在用于可延迟运行(即不需要立即运行)并且在应用退出或设备重启时必须能够可靠运行的任务。
  • WorkManager 不适用于应用进程结束时能够安全终止的运行中后台工作,也不适用于需要立即执行的任务。

2020.05.13 更新

在 ListenableWorker 中进行线程处理

问题一:doWork()通常用于处理异步耗时任务,如果需要准确地获取工作结果,如何在异步回调中返回结果呢?

解决方法二:

在doWork()中处理基于回调的异步操作,除了使用Java中提供的CountDownLatch和CyclicBarrier API,还可以通过继承ListenableWorker并自定义线程处理策略的方式实现。

ListenableWorker 是最低层级的工作器 API;Worker、CoroutineWorker 和 RxWorker 都是从这个类衍生而来的。ListenableWorker 只会发出信号以表明应该开始和停止工作,而线程处理则完全交由您负责完成。开始工作信号在主线程上调用,因此请务必手动转到您选择的后台线程。

抽象方法 ListenableWorker.startWork() 会返回一个持有ListenableWorker.Result的ListenableFuture。ListenableFuture继承自java.util.concurrent.Future,用于提供附加监听器和传播异常的功能。
为了构造ListenableFuture,我们需要引入androidx.concurrent:concurrent-futures依赖,在concurrent-futures中提供了异步回调转Future的实用程序类 CallbackToFutureAdapter,通过 CallbackToFutureAdapter可以非常方便的构造ListenableFuture,示例代码如下:

class CallbackWorker(context: Context,params: WorkerParameters): ListenableWorker(context,params) {

    private val outputData = Data.Builder()
    private var running = AtomicBoolean(false)
    private val executor = Executors.newSingleThreadExecutor()
    private val runnable = Runnable {
        logd("Works is canceled by ListenableFuture.cancel()")
    }

    override fun startWork(): ListenableFuture<Result> {
        return CallbackToFutureAdapter.getFuture {completer ->
            if (running.compareAndSet(false,true)){
                MTAUpdate.checkUpdate(object : UpdateListener {
                    override fun onFailed(e: UpdateException, firmwareName: FirmwareName) {
                        loge("An error occurred at works. $e")
                        running.set(false)
                        completer.set(Result.failure(outputData.putString(KEY_RESULT_MESSAGE,e.message).build()))
                        //completer.setException(e)
                    }

                    override fun onSuccess(result: UpdateResult) {
                        running.set(false)
                        completer.set(Result.success(outputData.putString(KEY_RESULT_MESSAGE,result.msg).build()))
                    }
                })
            }

            completer.addCancellationListener(runnable,executor)
        }
    }

    companion object{
        const val KEY_RESULT_MESSAGE = "result_message"
    }
}

CallbackToFutureAdapter.getFuture(resolver)的参数是一个Resolver接口,Resolver只有一个方法Object attachCompleter(@NonNull Completer completer),我们实现Resolver并通过completer对象的set()或setException(e)方法将异步回调结果放入ListenableFuture。

在ListenableWorker内监听工作是否被停止

如果预计工作会停止,则始终会取消 ListenableWorker 的 ListenableFuture。通过使用 CallbackToFutureAdapter,您只需添加一个取消监听器即可。

调用CallbackToFutureAdapter.Completer.addCancellationListener()来观察ListenableFuture是否被取消,当调用ListenableFuture.cancel()或者CallbackToFutureAdapter.Completer对象在ListenableFuture完成之前被GC,将会触发CallbackToFutureAdapter.Completer.addCancellationListener()的回调。addCancellationListener(runnable,executor)第一个参数为Runable,当工作被停止时我们可以在Runnable中处理一些事情,executor是Runable的执行器。