新架構元件: WorkManager

snwr611發表於2018-05-10

5月8號, I/O大會上又推出了兩個新的Architeture Component庫: Navigation與WorkManager. 這裡就先介紹一下WorkManager.

一. WorkManager的一句話介紹

其實就是"管理一些要在後臺工作的任務, -- 即使你的應用沒啟動也能保證任務能被執行".

1. 為何不用JobScheduler, AlarmManger來做?

: 其實這個想法很對. WorkManager在底層也是看你是什麼版本來選到底是JobScheduler, AlamarManager來做. JobScheduler是Android 5.x才有的. 而AlarmManager一直存在. 所以WorkManager在底層, 會根據你的裝置情況, 選用JobScheduler, Firebase的JobDispatcher, 或是AlarmManager

2. 為啥不用AsyncTask, ThreadPool, RxJava?

: 這一點就要特別說明一下了. 這三個和WorkManager並不是替代的關係. 這三個工具, 能幫助你在應用中開後臺執行緒幹活, 但是應用一被殺或被關閉, 這些工具就幹不了活了. 而WorkManager不是, 它在應用被殺, 甚至裝置重啟後仍能保證你安排給他的任務能得到執行.

其實Google自己也說了:"WorkManager並不是為了那種在應用內的後臺執行緒而設計出來的. 這種需求你應該使用ThreadPool"

二. 例子例子

還是show me the code吧.

1. 匯入WorkManager

app/build.gradle中加入

[kotlin]

implementation "android.arch.work:work-runtime-ktx:1.0.0-alpha01"

[java]

implementation "android.arch.work:work-runtime:1.0.0-alpha01"

2. 一個定期Pull的例子

以我在2012年做過的一個專案為例, 當時我在做一個電商專案. 我們有一個需求是要定時推給使用者一些我們推薦的單品, 但是當時集團還沒有push service元件呢, 所以我們當時為了及時上線, 選用的策略是"pull策略". 客戶端定時去後臺拉取, 看有沒有新的推薦.

這時我們要分兩步走. 第一步是確定要幹什麼活(去後臺pull推薦資訊); 第二步是讓這個活入佇列.

程式碼上我們也分兩步

  1. Worker是幹活的主體. 它只管輪到了它時要做的工作. 不管其它的東西(如何時輪到它, 它的ID, ...).

這裡要新建個Worker的子類, 重寫它的doWork()方法.


class PullWorker : Worker() {
    override fun doWork(): WorkerResult {
        // 模擬設定頁面中的"是否接受推送"是否被勾選
        val isOkay = this.inputData.getBoolean("key_accept_bg_work", false)
        if(isOkay) {
            Thread.sleep(5000) //模擬長時間工作

            val pulledResult = startPull()
            val output = Data.Builder().putString("key_pulled_result", pulledResult).build()
            outputData = output
            return WorkerResult.SUCCESS
        } else {
            return WorkerResult.FAILURE
        }
    }

    fun startPull() : String{
        return "szw [worker] pull messages from backend"
    }
}
複製程式碼
  1. 把Worker包裝成一個WorkRequest, 併入列
    • WorkRequest就多了一些新屬性: 如:
      • ID(一般是一個UUID, 以保證唯一性),
      • 何時執行,
      • 有沒有限制(如只有在充電並連網時才執行此任務),
      • 執行鏈 (當某任務執行完了, 才能輪到我執行)
    • WorkManager就負責把WorkRequest入列
class PullEngine {
    fun schedulePull(){
        //java就請用PeriodicWorkRequest.Builder類
        val pullRequest = PeriodicWorkRequestBuilder<PullWorker>(24, TimeUnit.HOURS)
                .setInputData(
                    Data.Builder()
                        .putBoolean("key_accept_bg_work", true)
                        .build()
                )
                .build()
        
        WorkManager.getInstance().enqueue(pullRequest)
    }
}
複製程式碼

3. 講解

1.幹活的是Worker類. 我們一般是新建個Worker的子類, 並重寫doWork()方法. 但是, doWork()方法是沒有引數的. 我們有時有引數的需求,怎麼辦? 這時就要用上Worker.getInputData()方法了.

2.同理, doWork()方法是返回void的. 你要是有結果想傳出去, 就可以用Worker.setOutputData()

3.上面的兩個方法所得到/設定的資料型別都是Data. 這個Data很類似我們Android中的Bundle, 也有putInt(key, value), getString(key, defaultValue)這樣的方法.

一般Data的生成, 是用Data.Builder類. 如:

val output = Data.Builder().putInt(key, 23).build()
複製程式碼

4.上面講了WorkRequest其實就是入列的一個實體, 它包裝了Worker在內. 但我們一般不直接使用WorkReqeust類, 多是用它的子類: OneTimeWorkRequest, 或是PeriodWorkReqeust.

因為我們的pull需求是每天都要去拉一次, 所以這裡我們沒有用OneTimeWorkRequest, 而是構建了一個24小時就重複幹活的PeriodicWorkReqeust.

三. 進階

1. 想拿到結果

WorkManager提供了一個介面讓我們拿到結果, 這個東東就是WorkStatus. 你可以由id得到你想要的那個任務的WorkStatus. 這個WorkStatus其實就是知道這任務沒有完成, 有什麼返回值.

因為前後臺要解耦合的原因, 所以這個工作其實是由LiveData來完成的. 既然有LiveData, 那我們肯定要有一個LifecycleOwner了(一般是我們的AppcompatActivity).

來看個例子. 以上面的pull例子為例, 若我們拉到了結果, 就顯示一個notification (這裡為簡便, 是收到結果後就列印一下日誌).

[PullEngine.kt]
class PullEngine {
    fun schedulePull(){
        val pullRequest = PeriodicWorkRequestBuilder<PullWorker>(24, TimeUnit.HOURS).build()
        WorkManager.getInstance().enqueue(pullRequest)

        // 下面兩行是新加的, 用來存任務的ID
        val pullRequestID = pullRequest.id
        MockedSp.pullId = pullRequestID.toString() // 模擬存在SharedPreference中
    }
}

複製程式碼
[PullActivity.kt]
class PullActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // UUID實現了Serializable介面. 也能由toString(), fromString()與String互轉
        val uuid = UUID.fromString(MockedSp.pullId)
        WorkManager.getInstance().getStatusById(uuid)
                .observe(this, Observer<WorkStatus> { status ->
                    if (status != null){
                        val pulledResult = status.outputData.getString("key_pulled_result", "")
                        println("szw Activity getResultFromBackend : $pulledResult")
                    }
                })
    }
}

複製程式碼

注意, observe()方法是用來監聽嘛. 它的引數分別是: observer(LifecycleOwner, Observer<WorkStatus>)

2. 總結入參/返回值

入參: WorkRequest.Builder.setInputData()

Worker類: 可以getIntpuData(), 以及setOutputData()

返回值: 由LiveData監聽, 可以得到WorkStatus. 而WorkStatus就有getOutputDat()方法

只是注意,這裡說的inputData, outputDat, 都不是普通的int, string. 而是Data類.

3. 如果任務執行完了, 應用卻沒被啟動怎麼辦? 會強行啟動應用來顯示UI變化嗎?

: 好問題. 但嚴格來說, 這個其實不是WorkManager的問題, 而是LiveData的問題. LiveData自己本身就是和Activity的生命週期繫結的. 你不用說應用被殺了, 就是你退出了這個註冊的Activity, 你都收不到LiveData的通知. 所以說你的應用被殺, 任務又執行完了時, 是沒有UI通知的, 更不會強行啟動你的啟動. (這有點流氓~)

4. 任務鏈

WorkManager.getInstance()
    .beginWith(workA)
    .then(workB)
    .then(workC)
    .enqueue()
複製程式碼

這樣就會按workA, workB, workC的順序來執行. workA執行完了, 才會接著執行workB.

WorkManager甚至還能執行:

A --> B
        --> E
C --> D
複製程式碼

這樣的形式, 即A執行完了才執行了B, C執行完才執行D. B,D都執行完了才執行E.

5. 插入任務時, 已經有相同的任務時, 怎麼辦?

WorkManager可以用beginUniqueWork()來執行唯一工作佇列("unique work sequence"). 若有任務有重複時, 怎麼辦?

這個主要是一個ExistingWorkPolicy類. 這個類也是WorkManager包中的類. 它其實是一個Enum. 其值有:

  • REPLACE: 用新任務來取代已經存在的任務
  • KEEP: 保留已經存在的任務. 忽視新任務
  • APPEND: 新任務入列. 新舊任務都存在於佇列中.

四. 總結

總體來說, WorkManager並不是要取代執行緒池/AsyncTask/RxJava. 反而是有點AlarmManager來做定時任務的意思. 即保證你給它的任務能完成, 即使你的應用都沒有被開啟, 或是裝置重啟後也能讓你的任務被執行.

WorkManager在設計上設計得比較好. 沒有把worker, 任務混為一談, 而是把它們解耦成Worker, WorkRequest. 這樣分層就清晰多了, 也好擴充套件. (如以後再有一個什麼WorkRequest的子類出來)

最後, WorkManager的入參出參設計得不錯. WorkReqeust負責放入引數, Worker處理並放置返回值, 最後WorkStaus中取出返回值, 並由LiveData來通知監聽者.

至於鏈式執行, 唯一工作佇列這些特性在你有類似的需求時, 也能幫助到你.

相關文章