使用WorkManager調度任務
WorkManager是一個庫, 用以將工作入隊, 當該工作的約束條件得到滿足之后, WorkManager保證它的執行. WorkManager允許觀測工作的狀態, 並且擁有能力創建復雜的工作鏈.
WorkManager使用基礎的作業分發服務, 僅當如下條件可用時:
- 當API 23+時, 使用JobScheduler;
- 當API 14~22時, 如果應用使用了Firebase JobDispatcher和其它的Firebase依賴時, 使用Firebase JobDispatcher; 否則的話, 則使用自定義的AlarmManager + BroadcastReceiver實現.
所有的工作必須有相應的Worker來實現計算. 工作在后台線程執行. WorkManager支持兩種類型的工作: OneTimeWorkRequest和PeriodicWorkRequest.
默認情況下, WorkManager在后台線程執行它的操作. 如果你已經在后台線程運行且需要同步調用WorkManager, 使用synchronous()方法訪問這些方法.
WorkManager API可以輕易的指定可暫緩的異步任務, 以及這些任務何時運行. 這些API能夠讓你創建一個任務並把該任務推給WorkManager即刻或者在恰當的時刻去運行. 比如, 某個應用可能時不時地需要從網絡下載最新的資源. 使用這些類, 你可以設置一個任務, 選擇該任務運行時的恰當條件(例如"只有在設備充電且聯網的時候"), 然后把任務推給WorkManager, 當指定的條件滿足時, WorkManager便會運行該任務. 即使你的應用被強制退出或者設備進行了重啟, 這些任務依然會被保障執行.
備注: 例如, 向服務器上傳應用數據, 即使應用退出, 系統依然保障運行這些任務. WorkManager即是為這些任務而設計的. 而對於進程內的后台工作, 在應用的進程結束的時候, 則會被安全地終止, 這些任務並不是WorkManager設計的目的. 而這些這種情況, 推薦使用ThreadPools.
WorkManager基於設備的API和應用的狀態來選擇恰當的方式來執行任務. 如果WorkManager在應用正在運行的時候執行了你的某條任務, 那么WorkManager會在你的應用進程中開啟新的線程來執行這條任務. 而如果你的應用沒有在運行, WorkManager則會選擇恰當的方式來調度后台任務-根據設備API水平和依賴庫, WorkManager可能會選擇JobScheduler, Firebase JobDispatcher或者AlarmManager. 你不必自己寫邏輯找出設備有哪些能力以找出恰當的API; 正相反, 你只需把任務遞交給WorkManager讓它選擇最佳選項.
此外, WorkManager還提供了些許高級特性. 比如, 你可以設置任務鏈; 在一些任務完成之后, WorkManager將下一個任務添加進隊列以形成鏈. 你也可以通過觀測任務的LiveData來查看它的狀態和返回值. 如果你想通過展示UI來顯示任務狀態, 這也許會有用.
同時, WorkManager已經存在於androidx.arch包中, 但它依然依賴於Android Support Library 27.1並與Arch構件的版本有關聯. 擁有AndroidX依賴的WorkManager版本將會在未來發布.
WorkManager的依賴添加方式如下:
1 dependencies { 2 def work_version = "1.0.0-alpha04" 3 4 implementation "android.arch.work:work-runtime:$work_version" // use -ktx for Kotlin 5 6 // optional - Firebase JobDispatcher support 7 implementation "android.arch.work:work-firebase:$work_version" 8 9 // optional - Test helpers 10 androidTestImplementation "android.arch.work:work-testing:$work_version" 11 }
相關類和概念
WorkManager API使用了幾個不同的類. 某些情況下, 你需要繼承某些類.
重要的一些類有:
- Worker: 指定了你需要執行什么樣的任務. WorkManager包含了抽象類Worker. 你需要繼承它並在里面執行工作.
- WorkRequest: 表示一個單獨的任務. 最少, WorkRequest對象指定了哪個Worker執行該任務. 然后, 你也可以向WorkRequest添加一些細節, 指定諸如任務在什么條件下執行等這些事情. 每一個WorkRequest都擁有一個自動生成的獨一無二的ID. 你可以使用該ID來做一些事情, 例如, 取消已經入隊的任務, 或者, 獲取任務的狀態. WorkRequest是個抽象類, 使用時, 應該使用它的直接子類, OneTimeWorkReqeust或者PeriodicWorkRequest.
- WorkRequest.Builder: 幫助類, 用來創建WorkRequest對象. 再說一遍, 你需要使用它的子類, OneTimeWorkRequest.Builder或者PeriodicWorkRequest.Builder.
- Constraints: 指定任務運行時機的約束條件(例如"只有在連接到網絡的時候"). 你可以使用Constraints.Builder來創建Constraints對象, 並且在創建WorkRequest對象之前, 把Constraints傳遞給WorkerRequest.Builder.
- WorkerManager: 入隊並管理工作請求. 你把WorkRequest對象傳遞給WorkManager以入隊任務. WorkManager以這種方式調度任務來分散系統資源的加載, 同時尊重了你指定的約束條件.
- WorkStatus: 包含了特定任務的信息. WorkManager為每一個WorkRequest對象提供了LiveData. LiveData持有WorkStatus對象; 通過觀測LiveData, 你能夠判定任務的當前狀態, 以及在任務結束后, 獲取返回值.
經典工作流
假設你正在寫一個圖片庫應用, 該應用需要定期壓縮存儲的圖片. 你想要使用WorkManager API來執行圖片壓縮. 在這種情況下, 你不必特別關注壓縮在什么時候執行; 你想要設置好任務, 然后不再關注它.
首先, 你需要定義自己的Worker類, 然后覆蓋doWork方法. 你的Worker類詳述了操作該怎么執行, 卻沒有任何信息表明任務何時運行.
1 class CompressWorker : Worker() { 2 3 override fun doWork(): Result { 4 5 // Do the work here--in this case, compress the stored images. 6 // In this example no parameters are passed; the task is 7 // assumed to be "compress the whole library." 8 myCompress() 9 10 // Indicate success or failure with your return value: 11 return Result.SUCCESS 12 13 // (Returning RETRY tells WorkManager to try this task again 14 // later; FAILURE says not to try again.) 15 16 } 17 18 }
然后, 基於上述Worker, 你創建了OneTimeWorkRequest對象, 然后通過WorkManager將該任務入隊.
1 val compressionWork = OneTimeWorkRequestBuilder<CompressWorker>().build() 2 WorkManager.getInstance().enqueue(compressionWork)
WorkManager選擇恰當的時刻運行該任務, 在平衡了諸如系統加載, 設備插拔等等, 這些考慮之后. 在多數情況下, 你不需要指定任務約束, WorkManager即刻運行你的任務. 如果你需要檢測任務狀態, 可能通過對正確的LiveData<WorkStatus>的處理, 獲取WorkStatus對象. 比如, 你想知道任務是否已經完成, 你可以使用這樣的代碼:
1 WorkManager.getInstance().getStatusById(compressionWork.id) 2 .observe(lifecycleOwner, Observer { workStatus -> 3 // Do something with the status 4 if (workStatus != null && workStatus.state.isFinished) { 5 // ... 6 } 7 })
任務約束
如果你想, 你可以指定任務何時應該運行的約束條件. 比如, 你可以指定任務只有在設備空閑且連接充電時才可以運行. 在這種情況下, 你需要創建一個OneTimeWorkReqeust.Builder對象, 然后可以使用這個Builder創建一個真正的OneTimeWorkRequest.
1 // Create a Constraints that defines when the task should run 2 val myConstraints = Constraints.Builder() 3 .setRequiresDeviceIdle(true) 4 .setRequiresCharging(true) 5 // Many other constraints are available, see the 6 // Constraints.Builder reference 7 .build() 8 9 val compressionWork = OneTimeWorkRequestBuilder<CompressWorker>() 10 .setConstraints(myConstraints) 11 .build()
然后, 像之前一樣, 將這個OneTimeWorkRequest對象傳遞給WorkManager.enqueue(). WorkManager會考慮你的約束條件, 直到一個時刻可以運行你的任務.
取消任務
在一個任務入隊后, 你依然可以取消它. 要取消一個任務, 你需要它的工作ID, 這個ID可以從WorkRequest對象中獲取到. 比如, 下面的代碼取消了之前的compressionWork請求:
1 val compressionWorkId:UUID = compressionWork.getId() 2 WorkManager.getInstance().cancelWorkById(compressionWorkId)
WorkManager會盡最大努力來取消該任務, 但這是天性不確定的--因為在你嘗試取消該任務時, 它可能已經在運行或者已經結束了. WorkManager也提供了方法取消在一個單獨工作序列中的所有任務, 或者擁有特定tag的所有任務, 當然, 也是盡力而為.
高級功能
WorkManager API的核心功能合使你能夠創建簡單且"即發即棄"的任務. 除此之外, API提供了高級功能讓你能夠設置更多復雜的任務.
任務重現
你也許擁有一個需要反復執行的任務. 比如, 照片管理應用不會只執行一次壓縮圖片. 更可能的情況是, 它會想非常頻繁地檢查共享的照片, 來看是否有新的或者修改過的照片需要壓縮. 這個再現任務能夠壓縮它找到的照片, 或者, 它可以接二連三地新建"壓縮該圖片"任務.
要創建一個重現任務, 使用PeriodicWorkRequest.Builder類創建一個PeriodicWorkRequest對象, 然后用入隊OneTimeWorkRequest對象一樣的方式, 入隊PeriodicWorkRequest對象. 比如, 假設我們定義了PhotoCheckWorker類來識別需要壓縮的圖片. 如果我們想每12小時運行一次這個清單任務, 我們需要像如下一樣創建一個PeriodicWorkRequest對象:
1 val photoCheckBuilder = 2 PeriodicWorkRequestBuilder<PhotoCheckWorker>(12, TimeUnit.HOURS) 3 // ...if you want, you can apply constraints to the builder here... 4 5 // Create the actual work object: 6 val photoCheckWork = photoCheckBuilder.build() 7 // Then enqueue the recurring task: 8 WorkManager.getInstance().enqueue(photoCheckWork)
WorkManager嘗試運行你的任務, 在你請求的間隔, 並且服從你設置的約束條件和其它需求.
任務入鏈
你的應用可能需要以特定的順序執行幾個任務. WorkManager允許你創建並入隊一個工作序列, 這個工作序列指定了多個任務, 以及它們運行的順序.
比如, 假設你的應用有三個OneTimeWorkRequest對象: workA, workB和workC. 這些任務必須以A->B->C的順序執行. 要反三個任務入隊, 使用WorkManager.beginWith(), 以第一個OneTimeWorkRequest對象workA為參數, 創建工作序列. 這個方法返回了WorkContinuation對象, 該對象定義了一個任務序列. 然后, 按順序, 用WorkContinuation.then方法添加剩下的OneTimeWorkRequest對象. 最后用WorkContinuation.enqueue方法將該工作序列入隊.
1 WorkManager.getInstance() 2 .beginWith(workA) 3 // Note: WorkManager.beginWith() returns a 4 // WorkContinuation object; the following calls are 5 // to WorkContinuation methods 6 .then(workB) // FYI, then() returns a new WorkContinuation instance 7 .then(workC) 8 .enqueue()
WorkManager用請求的順序, 根據每一個任務特定的約束, 運行任務. 如果任何一個任務返回了Worker.Result.FAILURE, 則整個序列終止.
你也可以傳遞多個OneTimeWorkRequest對象給beginWith()/then()方法, 如果你對單個方法調用傳遞了多個OneTimeWorkRequest對象, WorkManager將會並行運行該方法里面的所有任務, 在該方法里面的所有任務都完成之后, 才會執行方法序列的余下部分. 比如:
1 WorkManager.getInstance() 2 // First, run all the A tasks (in parallel): 3 .beginWith(workA1, workA2, workA3) 4 // ...when all A tasks are finished, run the single B task: 5 .then(workB) 6 // ...then run the C tasks (in any order): 7 .then(workC1, workC2) 8 .enqueue()
你可以通過WorkContinuation.combine()方法將多個鏈聯合在一起, 來創建更加復雜的任務鏈. 比如你想創建如下一個任務鏈:
要創建這個序列, 需要創建兩個單獨的序列, 然后把它們結合在一起生成第三個:
1 val chain1 = WorkManager.getInstance() 2 .beginWith(workA) 3 .then(workB) 4 val chain2 = WorkManager.getInstance() 5 .beginWith(workC) 6 .then(workD) 7 val chain3 = WorkContinuation 8 .combine(chain1, chain2) 9 .then(workE) 10 chain3.enqueue()
在這個實例下, WorkManager先運行workA, 然后運行workB; 也是先運行workC, 然后運行workD. 在workB和workD都完成之后, 再運行workE.
備注: 盡管在每一個子鏈中WorkManager有序運行任務, 但卻無法保證chains1中的任務如何與chains2中的任務重疊. 比如上面例子中的workB可以在workD之前或者之后運行, 也可以同時運行. 唯一的承諾是每一個子鏈中的多個任務有序運行; 也就是, workB直到workA運行結束都會開始執行.
有大量的WorkContinuation方法的變體提供了特定場景下的速記. 比如WorkContinuation.combine(OneTimeWorkRequest, WorkContinuation...)命令WorkManager先完成所有指定的WorkContinuation鏈, 然后以執行完OneTimeWorkRequest結束.
獨立工作序列(Unique work sequences)
通過調用beginUniqueWork()而非beginWith()來開始一個序列, 就能夠創建一個獨立工作序列. 每一個獨立工作序列都有一個名字; 一個名字, WorkManager一次只允許開啟一個工作序列. 當你創建一個新的獨立工作序列時, 如果已經存在了一個未完成的同名工作序列時, 你應該詳述WorkManager此時應該做什么:
- 取消已存在序列並用新的序列替代它;
- 保留已存在序列並忽略新序列;
- 將新的序列附加到已存在序列末尾, 當已存在序列最后一個任務完成后, 執行新序列中的第一個任務.
當你擁有一個任務不該入隊多次時, 獨立工作序列會非常有用. 比如, 你的應用需要同步數據到網絡時, 你可能會入隊一個名叫"sync"的獨立工作序列, 並且指明如果已經存在一個"sync"序列時, 新的任務應該被忽略.
當你需要逐步構建一個長的任務鏈時, 獨立工作序列也會非常有用. 比如照片編輯應用可能會讓用戶撤銷一長鏈的操作, 每一個操作的撤銷可能花費很短的時間, 但是它們必須以正確的順序執行. 在這種情形下, 應用可以創建一個"撤銷"鏈並將每個撤銷操作按需添加到鏈中.
工作打標
對於每一個WorkRequest對象, 你都可以給它賦值一個tag字符串, 然后從邏輯上將任務分組. 要設置tag的話, 調用WorkRequest.Builder.addTag(), 如下:
1 val cacheCleanupTask = 2 OneTimeWorkRequestBuilder<MyCacheCleanupWorker>() 3 .setConstraints(myConstraints) 4 .addTag("cleanup") 5 .build()
類WorkManager提供了幾個工具方法, 允許你用特定的tag操作所有的任務. 比如, WorkManager.cancelAllWorkByTag()使用特定的tag取消所有的任務; WorkManager.getStatusesByTag()返回特定tag標記任務的WorkStatus列表.
輸入參數和返回值
為了獲取更大的靈活性, 你可以給任務傳遞參數, 也可以使任務返回結果. 被傳遞和返回的值是鍵-值對. 要給任務傳參數, 在創建WorkRequest對象之前調用WorkRequest.Builder.setInputData(). 該方法的參數是Data.Builder創建的Data對象. Worker類可以通過Worker.getInputData()方法訪問傳入的參數. 要輸出返回值, 任務調用Worker.setOutputData(). 該方法的參數也是個Data對象. 你可能通過觀測任務的LiveData<WorkStatus>獲取輸出.
比如, 假設你有一個Worker類執行耗時的計算. 下面是這個Worker的示例:
1 // Define the parameter keys: 2 const val KEY_X_ARG = "X" 3 const val KEY_Y_ARG = "Y" 4 const val KEY_Z_ARG = "Z" 5 6 // ...and the result key: 7 const val KEY_RESULT = "result" 8 9 // Define the Worker class: 10 class MathWorker : Worker() { 11 12 override fun doWork(): Result { 13 val x = inputData.getInt(KEY_X_ARG, 0) 14 val y = inputData.getInt(KEY_Y_ARG, 0) 15 val z = inputData.getInt(KEY_Z_ARG, 0) 16 17 // ...do the math... 18 val result = myCrazyMathFunction(x, y, z); 19 20 //...set the output, and we're done! 21 val output: Data = mapOf(KEY_RESULT to result).toWorkData() 22 setOutputData(output) 23 24 return Result.SUCCESS 25 } 26 }
要創建這個工作且傳參, 代碼應該如下:
1 val myData: Data = mapOf("KEY_X_ARG" to 42, 2 "KEY_Y_ARG" to 421, 3 "KEY_Z_ARG" to 8675309) 4 .toWorkData() 5 6 // ...then create and enqueue a OneTimeWorkRequest that uses those arguments 7 val mathWork = OneTimeWorkRequestBuilder<MathWorker>() 8 .setInputData(myData) 9 .build() 10 WorkManager.getInstance().enqueue(mathWork)
返回值將會在任務的WorkStatus中可用:
1 WorkManager.getInstance().getStatusById(mathWork.id) 2 .observe(this, Observer { status -> 3 if (status != null && status.state.isFinished) { 4 val myResult = status.outputData.getInt(KEY_RESULT, 5 myDefaultValue) 6 // ... do something with the result ... 7 } 8 })
如果你將任務入鏈, 一個任務的輸出是可以作為鏈中下一個任務的輸入的. 如果是個很簡單的鏈, 單OneTimeWorkRequest緊接着另一個單OneTimeWorkRequest, 第一個任務通過調用setOutputData返回的值, 在第二個任務中通過調用getInputData可以獲取得到. 如果工作鏈比較復雜, 比如多個任務都發送輸出到單個任務--你可以在OneTimeWorkRequest.Builder中定義一個InputMerger, 以指定當不同任務返回的輸出有相同的鍵時, 應該怎么處理.