Rouse
讀完需要
15
分鐘
速讀僅需8分鐘
1
前言
上一次我們對Paging的應用進行了一次全面的分析,這一次我們來聊聊WorkManager。
如果你對Paging還未了解,推薦閱讀這篇文章:
Paging在RecyclerView中的應用,有這一篇就夠了
本來這一篇文章上周就能夠釋出出來,但我寫文章有一個特點,都會結合具體的Demo來進行闡述,而WorkManager的Demo早就完成了,隻是要結合文章一起闡述實在需要時間,上周自身原因也就延期了,想想還是寫代碼容易啊...??
哎呀不多說了,進入正題!
2
WorkManager
WorkManager是什麼?官方給的解釋是:它對可延期任務操作非常簡單,同時穩定性非常強,對于異步任務,即使App退出運作或者裝置重新開機,它都能夠很好的保證任務的順利執行。
是以關鍵點是簡單與穩定性。
對于平常的使用,如果一個背景任務在執行的過程中,app突然退出或者手機斷網,這時背景任務将直接終止。
典型的場景是:App的關注功能。如果使用者在弱網的情況下點選關注按鈕,此時使用者由于某種原因馬上退出了App,但關注的請求并沒有成功發送給服務端,那麼下次使用者再進入時,拿到的還是之前未關注的狀态資訊。這就産生了操作上的bug,降低了使用者的體驗,增加了使用者不必要的操作。
那麼該如何解決呢?很簡單,看WorkManager的定義,使用WorkManager就可以輕松解決。這裡就不再拓展實作代碼了,隻要你繼續看完這篇文章,你就能輕松實作。
當然你不使用WorkManager也能實作,這就涉及到它的另一個好處:簡單。如果你不使用WorkManager,你就要對不同API版本進行區分。
2.1
JobScheduler
複制
val service = ComponentName(this, MyJobService::class.java)
val mJobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val builder = JobInfo.Builder(jobId, serviceComponent)
.setRequiredNetworkType(jobInfoNetworkType)
.setRequiresCharging(false)
.setRequiresDeviceIdle(false)
.setExtras(extras).build()
mJobScheduler.schedule(jobInfo)
複制
通過JobScheduler來建立一個Job,一旦所設的條件達到,就會執行該Job。但JobScheduler是在API21加入的,同時在API21&22有一個系統Bug

這就意味着它隻能用在API23及以上的版本
複制
if (Build.VERSION.SDK_INT >= 23) {
// use JobScheduler
}
複制
既然隻能API23及以上才能使用JobScheduler,那麼在API23以下又該如何呢?
2.2
AlarmManager & BroadcastReceiver
這時對于API23以下,可以使用AlarmManager來進行任務的執行,同時結合BoradcastReceiver來進行任務的條件監聽,例如網絡的連接配接狀态、裝置的啟動等。
看到這裡是不是開始頭大了呢,我們開始的目的隻是想做一個穩定性的背景任務,最後發現居然還要進行版本相容。相容性與實作性進一步加大。
那麼有沒有統一的實作方式呢?當然有,它就是WorkManager,它的核心原理使用的就是上面所分析的結合體。
他會結合版本自動使用最佳的實作方式,同時還會提供額外的便利操作,例如狀态監聽、鍊式請求等等。
WorkManager的使用,我将其分為以下幾步:
- 建構Work
- 配置WorkRequest
- 添加到WorkContinuation中
- 擷取響應結果
下面我們來通過Demo逐漸了解。
3
建構Work
WorkManager每一個任務都是由Work構成,是以Work是任務具體執行的核心所在。既然是核心所在,你可能會認為它會非常難實作,但恰恰相反,它的實作非常簡單,你隻需實作它的doWork方法即可。例如我們來實作一個清除相關目錄下的.png圖檔的Work
複制
class CleanUpWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
override fun doWork(): Result {
val outputDir = File(applicationContext.filesDir, Constants.OUTPUT_PATH)
if (outputDir.exists()) {
val fileLists = outputDir.listFiles()
for (file in fileLists) {
val fileName = file.name
if (!TextUtils.isEmpty(fileName) && fileName.endsWith(".png")) {
file.delete()
}
}
}
return Result.success()
}
}
複制
所有代碼都在doWork中,實作邏輯也非常簡單:找到相關目錄,然後逐一判斷目錄中的檔案是否為.png圖檔,如果是就删除。
以上是邏輯代碼,關鍵點是傳回值Result.success(),它是一個Result類型,可用值有三個
- Result.success(): 成功
- Result.failure(): 失敗
- Result.retry(): 重試
對于success與failure,它還支援傳遞Data類型的值,Data内部是一個Map來管理的,是以對于kotlin可以直接使用workDataOf
複制
return Result.success(workDataOf(Constants.KEY_IMAGE_URI to outputFileUri.toString()))
複制
它傳遞的值将放入OutputData中,可以在鍊式請求中傳遞,與最終的響應結果擷取。其實本質是WorkManager結合了Room,将資料儲存在資料庫中。
這一步要點就是這麼多,下面進入下一步。
4
配置WorkRequest
WorkManager主要是通過WorkRequest來配置任務的,而它的WorkRequest種類包括:
- OneTimeWorkRequest
- PeriodicWorkRequest
4.1
OneTimeWorkRequest
首先OneTimeWorkRequest是作用于一次性任務,即任務隻執行一次,一旦執行完就自動結束。它的建構也非常簡單:
複制
val cleanUpRequest = OneTimeWorkRequestBuilder<CleanUpWorker>().build()
複制
這樣就配置了與CleanUpWorker相關的WorkRequest,而且是一次性的。
在配置WorkRequest的過程中我們還可以對其添加别的配置,例如添加tag、傳入inputData與添加constraint限制條件等等。
複制
val constraint = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val blurRequest = OneTimeWorkRequestBuilder<BlurImageWorker>()
.setInputData(workDataOf(Constants.KEY_IMAGE_RES_ID to R.drawable.yaodaoji))
.addTag(Constants.TAG_BLUR_IMAGE)
.setConstraints(constraint)
.build()
複制
添加tag是為了打上标簽,以便後續擷取結果;傳入的inputData可以在BlurImageWork中擷取傳入的值;添加網絡連接配接constraint限制條件,代表隻有在網絡連接配接的狀态下才會觸發該WorkRequest。
而BlurImageWork的核心代碼如下:
複制
override suspend fun doWork(): Result {
val resId = inputData.getInt(Constants.KEY_IMAGE_RES_ID, -1)
if (resId != -1) {
val bitmap = BitmapFactory.decodeResource(applicationContext.resources, resId)
val outputBitmap = apply(bitmap)
val outputFileUri = writeToFile(outputBitmap)
return Result.success(workDataOf(Constants.KEY_IMAGE_URI to outputFileUri.toString()))
}
return Result.failure()
}
複制
在doWork中,通過InputData來擷取上述blurRequest中傳入的InputData資料。然後通過apply來處理圖檔,最後使用writeToFile寫入到本地檔案中,并傳回路徑。
由于篇幅有限,這裡就不一一展開,感興趣的可以檢視源碼
4.2
PeriodicWorkRequest
PeriodicWorkRequest是可以周期性的執行任務,它的使用方式與配置和OneTimeWorkRequest一緻。
複制
val constraint = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
// at least 15 minutes
mPeriodicRequest = PeriodicWorkRequestBuilder<DataSourceWorker>(15, TimeUnit.MINUTES)
.setConstraints(constraint)
.addTag(Constants.TAG_DATA_SOURCE)
.build()
複制
不過需要注意的是:它的周期間隔最少為15分鐘。
5
添加到WorkContinuation中
上面我們已經将WorkRequest配置好了,剩下要做的是将其加入到work工作鍊中進行執行。
對于單個的WorkRequest,可以直接通過WorkManager的enqueue方法
複制
private val mWorkManager: WorkManager = WorkManager.getInstance(application)
mWorkManager.enqueue(cleanUpRequest)
複制
如果你想使用鍊式工作,隻需調用beginWith或者beginUniqueWork方法即可。其實它們本質都是執行個體化了一個WorkContinuationImpl,隻是調用了不同的構造方法。而最終的構造方法為:
複制
WorkContinuationImpl(@NonNull WorkManagerImpl workManagerImpl,
String name,
ExistingWorkPolicy existingWorkPolicy,
@NonNull List<? extends WorkRequest> work,
@Nullable List<WorkContinuationImpl> parents) { }
複制
其中beginWith方法隻需傳入WorkRequest
複制
val workContinuation = mWorkManager.beginWith(cleanUpWork)
複制
beginUniqueWork允許我們建立一個獨一無二的鍊式請求。使用也很簡單:
複制
val workContinuation = mWorkManager.beginUniqueWork(Constants.IMAGE_UNIQUE_WORK, ExistingWorkPolicy.REPLACE, cleanUpWork)
複制
其中第一個參數是設定該鍊式請求的name;第二個參數ExistingWorkPolicy是設定name相同時的表現,它三個值,分别為:
- REPLACE: 當有相同name且未完成的鍊式請求時,将原來的進度取消并删除,重新加入新的鍊式請求
- KEEP: 當有相同name且未完成的鍊式請求時,鍊式請求保持不變
- APPEND: 當有相同name且未完成的鍊式請求時,将新的鍊式請求追加到原來的子隊列中,即當原來的鍊式請求全部執行後才開始執行。
而不管是beginWith還是beginUniqueWork,它都會傳回WorkContinuation對象,通過該對象我們可以将後續任務加入到鍊式請求中。例如将上面的cleanUpRequest(清除)、blurRequest(圖檔模糊處理)與saveRequest(儲存)串行起來執行,實作如下:
複制
val cleanUpRequest = OneTimeWorkRequestBuilder<CleanUpWorker>().build()
val workContinuation = mWorkManager.beginUniqueWork(Constants.IMAGE_UNIQUE_WORK, ExistingWorkPolicy.REPLACE, cleanUpRequest)
val blurRequest = OneTimeWorkRequestBuilder<BlurImageWorker>()
.setInputData(workDataOf(Constants.KEY_IMAGE_RES_ID to R.drawable.yaodaoji))
.addTag(Constants.TAG_BLUR_IMAGE)
.build()
val saveRequest = OneTimeWorkRequestBuilder<SaveImageToMediaWorker>()
.addTag(Constants.TAG_SAVE_IMAGE)
.build()
workContinuation.then(blurRequest)
.then(saveRequest)
.enqueue()
複制
除了串行執行,還支援并行。例如将cleanUpRequest與blurRequest并行處理,完成之後再與saveRequest串行
複制
val left = mWorkManager.beginWith(cleanUpRequest)
val right = mWorkManager.beginWith(blurRequest)
WorkContinuation.combine(arrayListOf(left, right))
.then(saveRequest)
.enqueue()
複制
需要注意的是:如果你的WorkRequest是PeriodicWorkRequest類型,那麼它不支援建立鍊式請求,這一點需要注意了。簡單的了解,周期性的任務原則上是沒有終止的,是個閉環,也就不存在所謂的鍊了。
6
擷取響應結果
這就到最後一步了,擷取響應結果WorkInfo。WorkManager支援兩種方式來擷取響應結果
- Request.id: WorkRequest的id
- Tag.name: WorkRequest中設定的tag
同時傳回的WorkInfo還支援LiveData資料格式。
例如,現在我們要監聽上述blurRequest與saveRequest的狀态,使用tag來擷取:
複制
// ViewModel
internal val blurWorkInfo: LiveData<List<WorkInfo>>
get() = mWorkManager.getWorkInfosByTagLiveData(Constants.TAG_BLUR_IMAGE)
internal val saveWorkInfo: LiveData<List<WorkInfo>>
get() = mWorkManager.getWorkInfosByTagLiveData(Constants.TAG_SAVE_IMAGE)
// Activity
private fun addObserver() {
vm.blurWorkInfo.observe(this, Observer {
if (it == null || it.isEmpty()) return@Observer
with(it[0]) {
if (!state.isFinished) {
vm.processEnable.value = false
} else {
vm.processEnable.value = true
val uri = outputData.getString(Constants.KEY_IMAGE_URI)
if (!TextUtils.isEmpty(uri)) {
vm.blurUri.value = Uri.parse(uri)
}
}
}
})
vm.saveWorkInfo.observe(this, Observer {
saveImageUri = ""
if (it == null || it.isEmpty()) return@Observer
with(it[0]) {
saveImageUri = outputData.getString(Constants.KEY_SHOW_IMAGE_URI)
vm.showImageEnable.value = state.isFinished && !TextUtils.isEmpty(saveImageUri)
}
})
......
......
}
複制
再來看一個通過id擷取的:
複制
// ViewModel
internal val dataSourceInfo: MediatorLiveData<WorkInfo> = MediatorLiveData()
private fun addSource() {
val periodicWorkInfo = mWorkManager.getWorkInfoByIdLiveData(mPeriodicRequest.id)
dataSourceInfo.addSource(periodicWorkInfo) {
dataSourceInfo.value = it
}
}
// Activity
private fun addObserver() {
vm.dataSourceInfo.observe(this, Observer {
if (it == null) return@Observer
with(it) {
if (state == WorkInfo.State.ENQUEUED) {
val result = outputData.getString(Constants.KEY_DATA_SOURCE)
if (!TextUtils.isEmpty(result)) {
Toast.makeText(this@OtherWorkerActivity, result, Toast.LENGTH_LONG).show()
}
}
}
})
}
複制
結合LiveData使用是不是很簡單呢?WorkInfo擷取的本質是通過操作Room資料庫來擷取。在文章的Work部分已經提到,在執行完Work任務之後傳遞的資料将會儲存到Room資料庫中。
是以WorkManager與AAC的結合度非常高,目的也是緻力于為我們開發者提供一套完整的架構,同時也說明Google對AAC架構的重視。
如果你還未了解AAC,推薦你閱讀我之前的文章
Room
LiveData
Lifecycle
ViewModel
最後我們将上面的幾個WorkRequest結合起來執行,看下它們的最終效果:
通過這篇文章,希望你能夠熟悉運用WorkManager。如果這篇文章對你有所幫助,你可以順手點贊、關注一波,這是對我最大的鼓勵!
7
項目位址
7.1
Android精華錄
該庫的目的是結合詳細的Demo來全面解析Android相關的知識點, 幫助讀者能夠更快的掌握與了解所闡述的要點。
連結位址(或者點選閱讀原文):
https://github.com/idisfkj/android-api-analysis