Android使用 WorkManager 調度任務

一、前言

    WorkManager屬於Android Jetpack的一部分,通過WorkManager API可以輕鬆第調度可延遲的任務,即使是那些在應用退出或者設備重啓時仍需要運行的任務。

WorkManager的主要功能

  • 最高可向後兼容到 API 14
    • 在 API 23 及以上級別的設備上運行使用 JobScheduler實現
    • 在 API 14-22 的設備上運行結合使用 BroadcastReceiver 和 AlarmManager實現
  • 支持網絡可用性或充電狀態等工作約束
  • 支持調度一次性任務和週期行的任務
  • 支持監控和管理計劃任務
  • 將任務鏈接起來
  • 確保任務執行,即使應用退出或者設備重啓也會執行任務
  • 遵循低電耗模式等省電功能

WorkManager的主要用途
    WorkManager主要用於可延遲執行的任務,並且在應用退出或者設備重啓時必須能可靠運行的任務。例如:

  • 想後端發送日誌或者分析數據
  • 定期同步應用數據到服務器

WorkManager的侷限性
    WorkManager 不適用於應用進程結束時能夠安全終止的運行中後臺任務,也不適用於需要立即執行的任務。因此,也不要濫用WorkManager。

本文測試Demo源碼下載

二、WorkManager入門指南

    WorkManager 的使用主要有以下幾點操作:

  1. 將 WorkManager 添加到您的 Android 項目中(添加依賴)
  2. 創建後臺任務
  3. 配置運行任務的方式和時間
  4. 將任務提交給系統

2.1 添加依賴

    WorkManager 是屬於 Android Jetpack 的一部分,使用需要在應用模塊下的build.gradle引入相關的依賴。

dependencies {
      def work_version = "2.3.4"

        // (僅Java使用,如果你的項目是基於Java開發的,引入這個)
        implementation "androidx.work:work-runtime:$work_version"

        // (Kotlin + coroutines,如果你的項目基於Kotlin開發,引入這個)
        implementation "androidx.work:work-runtime-ktx:$work_version"

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

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

        // 可選 - 測試支持
        androidTestImplementation "androidx.work:work-testing:$work_version"
      }

說明:關於最新的WorkManager依賴,請參考: WorkManager 庫版本歷史

2.2 創建後臺任務

    任務使用 Worker 類定義,類內部的doWork() 方法在 WorkManager 提供的後臺線程上同步運行。要創建後臺任務,擴展 Worker 類並重寫 doWork() 方法即可。

示例:

class CheckSystemWorker(context: Context, workerParameters: WorkerParameters): Worker(context, workerParameters) {

    override fun doWork(): Result {

        Log.e("TEST", "Checking system。。。。。。。。")
        Thread.sleep(3000)
        Log.e("TEST", "Checking system done.")
        return Result.success()
    }

}

2.3 配置運行任務的方式和時間

使用 Worker 定義任務之後,使用 WorkRequest 定義任務的運行方式和時間。任務可以是一次性的,也可以是週期性的。對於一次性 WorkRequest,請使用 OneTimeWorkRequest,對於週期性工作,請使用 PeriodicWorkRequest

示例:

val checkSystem = OneTimeWorkRequestBuilder<CheckSystemWorker>().build()

2.4 將任務提交給系統

定義 WorkRequest 之後,現在可以通過 WorkManagerenqueue() 方法來調度它,將任務提交給系統進行管理和運行。

示例:

WorkManager.getInstance(applicationContext).enqueue(checkSystem)

三、WorkManager 進階

3.1 設定任務請求

    前面介紹瞭如何創建簡單的任務請求(WorkRequest)並將其放到系統隊列中。在這個章節中,將會詳細介紹任務和任務請求的各種設定。

3.1.1 任務約束

    可以通過向任務請求中添加 Constraints 指明任務運行的條件。Constraints通過 Constraints.Builder 創建,裏面定義了設定不同約束的成員函數。例如電池非低電量、需要網絡等等。

示例:

val constraints = Constraints.Builder()
    .setRequiresBatteryNotLow(true)
    .setRequiredNetworkType(NetworkType.CONNECTED) // 需要聯網
    .build()

val checkSystem = OneTimeWorkRequestBuilder<CheckSystemWorker>()
    .setConstraints(constraints)
    .build()
WorkManager.getInstance(applicationContext).enqueue(checkSystem)

上面的示例中,爲任務添加了需要網絡連接的約束,在聯網狀態下,任務放入隊列之後將會安排執行,但是如果當前設備斷開了網絡,並不會立刻執行,而是在隊列中等到網絡恢復,網絡恢復後,等待的任務會繼續執行。

    如果爲一個任務設定了多個約束,當所有約束都滿足時任務纔會執行。如果運行期間任務的約束不滿足,將會停止任務的執行,等到約束滿足時,系統將會嘗試恢復執行任務。

3.1.2 初始延遲

    如果任務沒有設定約束或者所有約束都滿足,任務有可能會立即執行。如果不想任務立即執行,可以設定一個最短的延遲時間。需要設定最短初始延遲時間,需要向任務請求中調用setInitialDelay()接口設定。

說明:爲什麼說最短延遲時間呢?因爲將任務提交給系統,本來就有可能出現等待延遲,同理,設定了初始延遲時間的任務即使到了延遲時間,任務也有可能需要等待資源。換句話說,任務開始執行的時間 >= 設定的初始延遲時間

val checkSystem = OneTimeWorkRequestBuilder<CheckSystemWorker>()
    .setInitialDelay(3000, TimeUnit.MILLISECONDS)
    .build()
WorkManager.getInstance(applicationContext).enqueue(checkSystem)

上面的例子可以發現,當任務請求提交到系統時,並不會立即執行,而是等待一段時間纔會開始執行。

3.1.3 重試和退避政策

    如果任務執行失敗,需要讓 WorkManager 重新嘗試執行任務,可以從工作器(Worker)中返回 Result.retry(),然後,系統會根據退避延遲時間和政策重現調整調度任務。

    設置任務的退避政策通過調用 WorkerRequest.BuildersetBackoffCriteria()) 接口實現,設置的值包括退避延遲時間的增長方式重試任務最短等待時間重試等待時間的單位。退避延遲時間的增長方式在 BackoffPolicy 中定義,默認是 BackoffPolicy.EXPONENTIAL (指數增長)。

提示:退避延遲時間增長方式有兩種

  • 線性增長(BackoffPolicy.LINEAR:線性函數增長,即f(n)=tnf(n)=tn,其中 t 爲重試任務最短時間, n 爲重試次數(n1n\geq1)。
  • 指數正在(BackoffPolicy.EXPONENTIAL:2的指數倍增長,即f(n)=t2nf(n)=t*2^n,其中 t 爲重試任務最短時間, n 爲重試次數(n1n\geq1)。
// 定義Worker
var times: Int = 0
var lastTime = 0L

class CheckDiskWorker(context: Context, workerParameters: WorkerParameters): Worker(context, workerParameters) {
    override fun doWork(): Result {
        Log.e("TEST", "Checking disk。。。。。。。。")
        if(times > 0) {
            Log.w("TEST", "Retry ${times}, Delay is ${(System.currentTimeMillis() - lastTime) / 1000} s")
        }
        Thread.sleep(3000)
        lastTime = System.currentTimeMillis()
        if(times < 5) {
            Log.e("TEST", "Checking disk failure.")
            times++
            // 需要重試,比如操作失敗了,返回Result.retry()
            return Result.retry()
        }
        Log.e("TEST", "Checking disk done.")
        times = 0
        // 返回成功時,將不會再重試
        return Result.success()
    }
}
// 設定任務請求的重試和迴避策略
val checkDisk = OneTimeWorkRequestBuilder<CheckDiskWorker>()
    .setBackoffCriteria(BackoffPolicy.LINEAR, OneTimeWorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS)
    .build()
WorkManager.getInstance(applicationContext).enqueue(checkDisk)

注意事項
1. OneTimeWorkRequest重試的最小時間間隔必須是OneTimeWorkRequest.MIN_BACKOFF_MILLISOneTimeWorkRequest.MAX_BACKOFF_MILLIS,超出這個範圍的將會取臨界值;
2. 因爲任務提交給系統到真是執行並不是實時的,所以,真實的延長時間不一定是預期的(有可能會出現後一次重試時間間隔比前一次的還要短),但是一定是大於等於預期值。

3.1.4 定義任務的輸入/輸出

    將任務提交給系統,但是需要和任務進行交互,就需要實現任務的輸入/輸出。將需要的數據以參數的形式傳入到任務(輸入),任務執行完畢後返回結果(輸出)。傳入參數通過 WorkerReuqestsetInputData() 接口傳入。輸入和輸出值以鍵值對的形式存儲在 Data 對象中。在 Worker 中通過getInputData()接口獲取輸入參數。類似地,從 Worker 中返回數據(輸出)也是通過 Data 對象,並通過Result 對應帶有返回數據參數的方法返回結果,例如:Result.success(Data)Result.failure(Data)

說明:Data 對象可以通過原始的 Data.Builder 進行構建,也可以使用 worker 庫提供的內聯函數 workDataOf(vararg pairs: Pair<String, Any?>) 進行構建,原理一樣

示例:

// 定義Worker
class CheckNetworkWorker(context: Context, workerParameters: WorkerParameters): Worker(context, workerParameters) {
    override fun doWork(): Result {
        Log.e("TEST", "Checking network。。。。。。。。")
        Log.d("TEST", "Checking network, params......")
        for((key, value) in inputData.keyValueMap) {
            Log.d("TEST", "$key ---- $value")
        }
        Thread.sleep(3000)
        Log.e("TEST", "Checking network done.")
        return Result.success(Data.Builder().let {
            it.putInt("code", 200)
            it.putString("msg", "Network is fine")
            it.build()
        })
    }
}

// 構建WorkerRequest,並提交系統
val checkNetwork = OneTimeWorkRequestBuilder<CheckNetworkWorker>()
    .setInputData(Data.Builder().let {
        // Data以Builder的方式進行構建,傳入的是鍵值對
        it.putString("operator", "Owen")
        it.putString("description", "Check network state")
        it.build()
    })
    .build()
WorkManager.getInstance(applicationContext).enqueue(checkNetwork)

注意:按照設計要求,Data 對象應該很小,大小上限爲 10KB,值可以是字符串、基元類型或數組變體。如果需要將更多數據傳入和傳出工作器,應該將數據放在其他位置,例如 存儲或者數據庫。

    向任務輸入數據已經知道如何實現了,可是在程序中如何獲取任務輸出的數據呢?主要有以下幾步:

  1. 通過 WorkManagergetWorkInfoByXXXLiveData() 獲取 WorkRequestLiveData<WorkInfo> 對象
  2. LiveData<WorkInfo> 對象添加觀察者
  3. 在觀察者的 onChanged() 方法中獲取返回的數據
  4. WorkRequest 提交到系統

獲取LiveData<WorkInfo> 對象的接口有多個,詳細請參考:WorkManager,使用合適的方法獲取,在後續章節也會詳細介紹。

那麼,以上的例子可以改爲:

val checkNetwork = OneTimeWorkRequestBuilder<CheckNetworkWorker>()
    .setInputData(Data.Builder().let {
        // Data以Builder的方式進行構建,傳入的是鍵值對
        it.putString("operator", "Owen")
        it.putString("description", "Check network state")
        it.build()
    })
    .build()

WorkManager.getInstance(applicationContext)
    .getWorkInfoByIdLiveData(checkNetwork.id)
    // observe方法是添加觀察者,這個方法有兩個參數,第一個參數是LifecycleOwner,可以傳入
    .observe(this, object : Observer<WorkInfo> {
    override fun onChanged(t: WorkInfo?) {
        // 任務執行完畢之後,會在這裏獲取到返回的結果
        if(t?.state == WorkInfo.State.SUCCEEDED) {
            for((key, value) in t.outputData!!.keyValueMap) {
                Log.d("TEST", "Out Data $key ---- $value")
            }
        }
    }
})
WorkManager.getInstance(applicationContext).enqueue(checkNetwork)

參考資料:

  1. LifecycleOwner
  2. Observer

3.1.5 標記任務

    可以爲任意 WorkRequest 對象分配標記字符串(tag),實現對任務按邏輯進行分組,這樣就可以對使用特定標記的所有任務執行操作。例如,WorkManager.cancelAllWorkByTag(String) 會取消所有使用特定標記的任務,而 WorkManager.getWorkInfosByTagLiveData(String) 會返回具有該標記的所有任務的 LiveData 和狀態列表。

    在構建 WorkRequest對象時,通過 WorkRequest.Builder.addTag(String) 向任務添加標記.

示例:

val checkNetwork = OneTimeWorkRequestBuilder<CheckNetworkWorker>()
    .setInputData(Data.Builder().let {
        // Data以Builder的方式進行構建,傳入的是鍵值對
        it.putString("operator", "Owen")
        it.putString("description", "Check network state")
        it.build()
    })
    .addTag("networkWork")
    .build()

3.2 任務的狀態及任務狀態跟蹤

3.2.1 任務的狀態

    在任務的整個生命週期內,它會經歷多個不同的狀態(State)。下面將介紹這些狀態:

  • 如果當前任務還有依賴的前提任務未完成,則當前任務處於 BLOCKED 狀態
  • 如果當前任務能夠在滿足任務約束(Constraints)和時機條件好後立即運行,則任務被視爲 ENQUEUED 狀態
  • 如果任務正在執行時,則處於 RUNNING 狀態
  • 如果任務執行完,在工作器中返回 Result.success() ,則任務被視爲 SUCCEEDED 狀態。這是一種終止狀態,只有一次性任務 OneTimeWorkRequest 纔可以進入這種狀態。
  • 如果任務執行完,在工作器中返回 Result.failure() ,責任無被視爲 FAILED 狀態。這是一種終止狀態,只有一次性任務 OneTimeWorkRequest 纔可以進入這種狀態。如果任務進入 FAILED 狀態,則其所依賴的任務也會被標記爲 FAILED ,並且不會運行。
  • 如果取消未完成的任務請求時,則任務會進入 CANCELLED 狀態,這是一種終止狀態,只有一次性任務 OneTimeWorkRequest 纔可以進入這種狀態。如果任務進入 CANCELLED 狀態,則其所依賴的任務也會被標記爲 CANCELLED ,並且不會運行。

這裏介紹了各種狀態,在後續會探討如何跟蹤這些狀態

3.2.2 任務狀態跟蹤

    將工作任務加入隊列後,可以通過 WorkManager 檢查其狀態。任務的相關信息在 WorkInfo 對象中提供,包括工作任務的 id、標籤(tag)、當前狀態(State) 和所有輸出數據。

可以通過以下三種方式之一來獲取 WorkInfo:

    利用以上各種方法獲取到 LiveData 變量,然後通過註冊監聽器來觀察 WorkInfo 的變化。

示例:

WorkManager.getInstance(applicationContext)
    .getWorkInfoByIdLiveData(checkNetwork.id)
    .observe(this, object : Observer<WorkInfo> {
    override fun onChanged(t: WorkInfo?) {
        // 任務執行完畢之後,會在這裏獲取到返回的結果
        Log.e("TEST", "WorkRequest state: ${t?.state}")
        if(t?.state == WorkInfo.State.SUCCEEDED) {
            // 顯示任務完成通知提示
            Toast.makeText(this@MainActivity, "Check network success", Toast.LENGTH_LONG).show()
            for((key, value) in t.outputData!!.keyValueMap) {
                Log.d("TEST", "Out Data $key ---- $value")
            }
        }
    }
})
WorkManager.getInstance(applicationContext).enqueue(checkNetwork)

3.3 跟蹤工作任務的中間狀態

    從 WorkManager 2.3.0-alpha01 開始,爲設置和觀察工作器的中間進度添加了一些支持。如果應用在前臺運行時,工作器保持運行狀態,也可以使用返回 WorkInfoLiveData 的 API 向用戶顯示此信息。

    ListenableWorker 現在支持 setProgressAsync() API,此類 API 可以保留中間進度。開發者能夠藉助這些 API實現通過界面觀察工作任務的中間進度。進度由 Data 類型表示,這是一個可序列化的屬性容器(類似於任務數據輸入輸出,並且受到相同的限制)。

    值得注意的是,只有在 ListenableWorker 運行時才能觀察到和更新進度信息。如果嘗試在 ListenableWorker 完成執行後在其中設置進度,則將會被忽略。可以通過WorkManagergetWorkInfoByXXX()getWorkInfoByXXXLiveData() 方法獲取 WorkInfo 的實例,然後通過 getProgress() 方法獲取包含進度信息的 Data 對象。

3.3.1 更新進度

    使用 ListenableWorkerWorker 調用 setProgressAsync() API 設置進度,會返回 ListenableFuture<Void>;更新進度是異步過程,因爲更新過程包括將進度信息存儲到數據庫中。在 Kotlin 中,您可以使用 CoroutineWorker 對象的 setProgress() 擴展函數來更新進度信息。

示例:

// 定義Worker
class CheckDiskProgressWorker(context: Context, workerParameters: WorkerParameters): Worker(context, workerParameters) {
    override fun doWork(): Result {
        Log.e("TEST", "Checking disk。。。。。。。。")
        Thread.sleep(1000)
        // 更新進度
        setProgressAsync(Data.Builder().let {
            it.putInt("progress", 20)
            it.build()
        })
        Thread.sleep(1000)
        // 更新進度
        setProgressAsync(Data.Builder().let {
            it.putInt("progress", 40)
            it.build()
        })
        Thread.sleep(1000)
        // 更新進度
        setProgressAsync(Data.Builder().let {
            it.putInt("progress", 60)
            it.build()
        })
        Thread.sleep(1000)
        // 更新進度
        setProgressAsync(Data.Builder().let {
            it.putInt("progress", 80)
            it.build()
        })
        Thread.sleep(1000)
        // 更新進度
        setProgressAsync(Data.Builder().let {
            it.putInt("progress", 100)
            it.build()
        })
        Log.e("TEST", "Checking disk done.")
        return Result.success(Data.Builder().let {
            it.putInt("code", 200)
            it.putString("msg", "Disk is fine")
            it.build()
        })
    }
}

提示:

  1. Worker類就是繼承自ListenableWorker,所以工作任務類繼承自Worker類,就擁有了設置中間進度的API。
  2. 進度的設置類似於任務數據輸入輸出,但是獲取的接口不一樣,使用getProgress()

3.3.2 跟蹤任務進度

    跟蹤進度信息也很簡單。可以通過WorkManagergetWorkInfoByXXX()getWorkInfoByXXXLiveData() 方法獲取 WorkInfo 的實例,然後通過 getProgress() 方法獲取包含進度信息的 Data 對象。

示例:

val checkDisk = OneTimeWorkRequestBuilder<CheckDiskProgressWorker>()
    .build()

WorkManager.getInstance(applicationContext)
    .getWorkInfoByIdLiveData(checkDisk.id)
    .observe(this, object : Observer<WorkInfo> {
    override fun onChanged(t: WorkInfo?) {
        // 任務執行完畢之後,會在這裏獲取到返回的結果
        Log.e("TEST", "WorkRequest state: ${t?.state}")
        if(t?.state == WorkInfo.State.RUNNING) {
            Log.d("TEST", "Work progress --- ${t.progress.getInt("progress", 0)}")
            Toast.makeText(this@MainActivity, "Check disk... ${t.progress.getInt("progress", 0)}%", Toast.LENGTH_LONG).show()
        } else if(t?.state == WorkInfo.State.SUCCEEDED){
            Toast.makeText(this@MainActivity, "Check disk success", Toast.LENGTH_LONG).show()
        } else if(t?.state == WorkInfo.State.FAILED){
            Toast.makeText(this@MainActivity, "Check disk failed", Toast.LENGTH_LONG).show()
        }
    }
})
WorkManager.getInstance(applicationContext).enqueue(checkDisk)

注意:跟蹤的觀察者的 Lifecycle 如果終止,那麼改觀察者將不會接受到更新消息。

3.4 將多個工作任務鏈接在一起

    工作鏈用於將多個任務關聯起來,並定義這些任務的運行順序,當需要以特定的順序運行多個任務時,工作鏈就非常有用。可以使用 WorkManager 創建工作鏈並對多個任務進行排隊。

3.4.1 創建工作任務鏈

    創建工作鏈,主要有一下幾步:

  1. 使用 WorkManager.beginWith(OneTimeWorkRequest)WorkManager.beginWith(List<OneTimeWorkRequest>) ,接口會返回 WorkContinuation 工作鏈實例;
  2. 通過返回的 WorkContinuation 對象使用 WorkContinuation.then(OneTimeWorkRequest)WorkContinuation.then(List<OneTimeWorkRequest>) API來添加從屬的任務;
  3. 最後使用 WorkContinuation.enqueue() 方法將工作任務鏈提交到系統排隊。

注意:
1. 每次調用 WorkContinuation.then(...) 都會返回一個新的 WorkContinuation 實例;
2. 通過帶 List 類型參數的 API 添加的工作任務,可能會並行運行。

示例:

//定義Worker
class ContinuationCheck1(context: Context, workerParameters: WorkerParameters): Worker(context, workerParameters) {

    override fun doWork(): Result {
        Log.e("TEST", "ContinuationCheck1 .......")

        // 打印輸入數據
        val strB = StringBuilder("ContinuationCheck1 輸入數據:\n")
        for ((key, value) in inputData.keyValueMap) {
            strB.append(key)
                .append(" : ")
                .append(value)
                .append("\n")
        }
        Log.e("TEST", strB.toString())

        Thread.sleep(3000)

        Log.e("TEST", "ContinuationCheck1 done .......")

        return Result.success(workDataOf("whereFrom" to "This is from ContinuationCheck1"))
    }
}

class ContinuationCheck2(context: Context, workerParameters: WorkerParameters): Worker(context, workerParameters) {

    override fun doWork(): Result {
        Log.e("TEST", "ContinuationCheck2 .......")

        // 打印輸入數據
        val strB = StringBuilder("ContinuationCheck2 輸入數據:\n")
        for ((key, value) in inputData.keyValueMap) {
            strB.append(key)
                .append(" : ")
                .append(value)
                .append("\n")
        }
        Log.e("TEST", strB.toString())

        Thread.sleep(3000)

        Log.e("TEST", "ContinuationCheck2 done .......")

        return Result.success(workDataOf("whereFrom" to "This is from ContinuationCheck2"))
    }
}

class ContinuationCheck3(context: Context, workerParameters: WorkerParameters): Worker(context, workerParameters) {

    override fun doWork(): Result {
        Log.e("TEST", "ContinuationCheck3 .......")

        // 打印輸入數據
        val strB = StringBuilder("ContinuationCheck3 輸入數據:\n")
        for ((key, value) in inputData.keyValueMap) {
            strB.append(key)
                .append(" : ")
                .append(value)
                .append("\n")
        }
        Log.e("TEST", strB.toString())

        Thread.sleep(3000)

        Log.e("TEST", "ContinuationCheck3 done .......")

        return Result.success(workDataOf("whereFrom" to "This is from ContinuationCheck3"))
    }
}

class ContinuationCheck4(context: Context, workerParameters: WorkerParameters): Worker(context, workerParameters) {

    override fun doWork(): Result {
        Log.e("TEST", "ContinuationCheck4 .......")

        // 打印輸入數據
        val strB = StringBuilder("ContinuationCheck4 輸入數據:\n")
        for ((key, value) in inputData.keyValueMap) {
            strB.append(key)
                .append(" : ")
                .append(value)
                .append("\n")
        }
        Log.e("TEST", strB.toString())

        Thread.sleep(3000)

        Log.e("TEST", "ContinuationCheck4 done .......")

        return Result.success(workDataOf("whereFrom" to "This is from ContinuationCheck4"))
    }
}


// 創建工作鏈並提交到系統排隊
WorkManager.getInstance(applicationContext)
    .beginWith(OneTimeWorkRequestBuilder<ContinuationCheck1>().also {
        it.setInputData(workDataOf("name" to "Owen", "position" to "Manager"))
    }.build())
    .then(OneTimeWorkRequestBuilder<ContinuationCheck2>().build())
    .then(listOf(OneTimeWorkRequestBuilder<ContinuationCheck3>().build(), OneTimeWorkRequestBuilder<ContinuationCheck4>().build()))
    .enqueue()

3.4.2 輸入數據合併

    OneTimeWorkRequest 類型的工作鏈,父級的輸出將會成爲下一級的輸入,所以在工作鏈中的輸入存在兩種情況:

  1. 只有一個父級 OneTimeWorkRequest :父級的輸出即是下一級的輸入,沒有衝突。
  2. 包含多個父級 OneTimeWorkRequest :可以使用 WorkerManager 的 InputMerger 對輸入參數進行合併(在構建 OneTimeWorkRequest 對象時通過 OneTimeWorkRequest.Builder.setInputMerger() API 進行設置),WorkerManager 提供了兩種參數合併的方式:
  • OverwritingInputMerger:覆蓋合併模式,系統會嘗試將所有的鍵值對添加到輸入數據中,如果有衝突(鍵一樣),則會覆蓋之前添加的鍵值;
  • ArrayCreatingInputMerger:創建數組合並模式,系統會將所有鍵值對添加到輸入數據中,如果有衝突(鍵一樣),則會將所有鍵一致的值放到一個新建的數組中,然後通過鍵添加到輸入數據中。

示例:

// 定義Worker
class ContinuationCheck5(context: Context, workerParameters: WorkerParameters): Worker(context, workerParameters) {

    override fun doWork(): Result {
        Log.e("TEST", "ContinuationCheck5 .......")

        // 打印輸入數據
        val strB = StringBuilder("ContinuationCheck5 輸入數據:\n")
        for ((key, value) in inputData.keyValueMap) {

            strB.append(key)
                .append(" : ")
            // 使用了ArrayCreatingInputMerger,判斷值是否爲數組
            if (value is Array<*>) {
                strB.append("[")
                (value as Array<*>).forEach {
                    strB.append(it)
                        .append(",")
                }
                strB.also {
                    it.delete(it.lastIndexOf(",") - 1, it.length)
                }.append("]")
            } else {
                strB.append(value)
            }

            strB.append("\n")
        }
        Log.e("TEST", strB.toString())

        Thread.sleep(3000)

        Log.e("TEST", "ContinuationCheck5 done .......")

        return Result.success(workDataOf("whereFrom" to "This is from ContinuationCheck5"))
    }
}

// 創建工作鏈並提交到系統排隊
WorkManager.getInstance(applicationContext)
    .beginWith(OneTimeWorkRequestBuilder<ContinuationCheck1>().also {
        it.setInputData(workDataOf("name" to "Owen", "position" to "Manager"))
    }.build())
    .then(OneTimeWorkRequestBuilder<ContinuationCheck2>().build())
    .then(listOf(OneTimeWorkRequestBuilder<ContinuationCheck3>().build(), OneTimeWorkRequestBuilder<ContinuationCheck4>().build()))
    .then(OneTimeWorkRequestBuilder<ContinuationCheck5>().also {
        // 設置輸入數據合併模式
        it.setInputMerger(ArrayCreatingInputMerger::class)
    }.build())
    .enqueue()

注意:如果工作任務鏈中可能存在多個父級,輸出可能存在衝突且使用了 ArrayCreatingInputMerger 合併模式的情況下,在下一級 OneTimeWorkRequest 讀取參數時一定要判斷值是否爲數組

3.4.3 工作任務鏈的狀態

     OneTimeWorkRequest 類型的工作鏈中,工作任務的狀態需要注意以下幾點:

  • 一個從屬的 OneTimeWorkRequest 僅在其所有的父級 OneTimeWorkRequest 都完成並且成功(即返回 Result.success())時纔會被解除阻塞(變爲 ENQUEUED 狀態)。

  • 如果有任何父級 OneTimeWorkRequest 失敗(即返回 Result.failure()),則所有從屬 OneTimeWorkRequest 也會被標記爲 FAILED 且不會執行。

  • 如果有任何父級 OneTimeWorkRequest 被取消,則所有從屬 OneTimeWorkRequest 也會被標記爲 CANCELLED 且不會執行。

3.5 取消和停止工作任務

3.5.1 取消工作任務

    如果不再需要運行先前加入隊列的作業,則可以申請取消。最簡單的方法是使任務 id 並調用 WorkManager.cancelWorkById(UUID) 來取消單個 WorkRequest

示例:

WorkManager.getInstance(applicationContext).cancelWorkById(workId)

    在後臺,WorkManager 會檢查工作任務的狀態(State),並做相應的處理:

  • 如果工作任務已經完成,則不會發生任何變化。否則,其狀態將更改爲 CANCELLED,之後就不會運行這個工作。任何依賴於這項工作任務(從屬)的 WorkRequests 的狀態也將變爲 CANCELLED

  • 如果工作當前的狀態爲 RUNNING,則工作器也會收到對 ListenableWorker.onStopped() 回調。重寫此方法以處理所有可能需要的清理操作。

    您也可以使用 WorkManager.cancelAllWorkByTag(String) API 取消指定標記的所有工作任務。請注意,此方法會取消所有具有此標記的工作任務。此外,您還可以使用 WorkManager.cancelUniqueWork(String) 取消具有唯一名稱的所有工作任務。

3.5.2 停止正在運行的工作器

    WorkManager 停止正在運行的工作器可能以下幾種原因:

  • 您明確要求取消它(例如,通過調用 WorkManager.cancelWorkById(UUID) 取消)。
  • 如果是唯一工作 (使用 WorkManager.enqueueUniqueWork() API 提交的任務),並且使用了 ExistingWorkPolicy.REPLACE 策略添加新的 WorkRequest 到隊列。舊的 WorkRequest 會立即被視爲已終止。
  • 工作任務的約束已不再得到滿足。
  • 系統出於某種原因指示您的應用停止工作。例如:如果工作任務超過 10 分鐘的執行期限,可能會發生這種情況,系統會將工作安排在稍後重試。

    在上面這些情況下,WorkRequest 收到對 ListenableWorker.onStopped() 回調,在必要時,應該重寫這個方法,執行清理工作並以協作方式完成工作器(例如,您應該在此時或者儘早關閉數據庫和文件的打開句柄)。此外,可以通過調用 ListenableWorker.isStopped() 接口確認系統是否已經停止您的應用。另外需要注意的是,在調用了 onStopped() 後,即使通過返回 Result 來指示工作已完成,WorkManager 都會忽略該 Result,因爲工作器已經被視爲停止

注意:當停止一個正在運行的工作任務時,會回調 onStopped() ,如果在 doWork() 裏面的任務有多個步驟,需要在每一個步驟完成處使用 isStopped (或者其他方法)判斷是否停止執行下一步,否則仍舊會繼續往下執行代碼(這個類似Java中的 Thread),當然,除非你想在工作停止後繼續執行所有代碼,可以不進行處理。

3.6 重複性工作

    如果應用需要定期執行某些任務,例如,需要定期備份數據或者上傳日誌到服務器等。那麼將考慮使用 PeriodicWorkRequest 實現定期任務。

示例:

val checkDisk = PeriodicWorkRequestBuilder<CheckDiskProgressWorker>(
                PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, TimeUnit.MILLISECONDS).build()

WorkManager.getInstance(applicationContext).enqueue(checkDisk!!)

注意事項
1. PeriodicWorkRequest最小重複執行時間間隔必須是PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS(900000毫秒,15分鐘),小於這個值將取該臨界值作爲重複執行時間間隔;
2. PeriodicWorkRequestOneTimeWorkRequest 一樣可以可以添加任務約束跟蹤任務狀態跟蹤任務的中間狀態以及取消和停止共工作等;
3. PeriodicWorkRequest無法使用鏈接功能,如果需要將服務鏈接起來,請使用 OneTimeWorkRequest
4. 重複性工作在單次任務執行完畢後即返回了 Result.success() ,也不會監聽到 State.SUCCEEDED 狀態。

3.7 唯一工作任務

    唯一工作任務是一個概念性非常強的術語,它可確保一次只有一個具有特定名稱的工作任務。與 id 不同的是,唯一名稱是人類可讀的,由開發者指定,而不是由 WorkManager 自動生成。也與標記不同,唯一名稱僅與“一個”工作任務關聯。

3.7.1 創建唯一工作任務

    創建爲一個工作任務序列,可以通過調用 WorkManager.enqueueUniqueWork(String, ExistingWorkPolicy, OneTimeWorkRequest)WorkManager.enqueueUniquePeriodicWork(String, ExistingPeriodicWorkPolicy, PeriodicWorkRequest) API 接口。第一個參數是唯一名稱(這是我們用來標識 WorkRequest 的鍵);第二個參數是衝突解決策略(它指定了如果已經存在一個具有該唯一名稱的未完成工作任務,WorkManager 應該如何處理);第三個參數是WorkRequest

示例:

class DoWorker(context: Context, workerParameters: WorkerParameters): Worker(context, workerParameters) {

    override fun doWork(): Result {

        val op = inputData.getString("op");

        Log.e("TEST", "$id $op .......")

        try {
            Thread.sleep(3000)
        } catch (e: InterruptedException) {
            onStopped()
        }
        if(isStopped) {
            Log.e("TEST", "$id $op stopped!")
            return Result.failure()
        }
        Log.e("TEST", "$id $op 50%.......")
        try {
            Thread.sleep(3000)
        } catch (e: InterruptedException) {
            onStopped()
        }
        if(isStopped) {
            Log.e("TEST", "$id $op stopped!")
            return Result.failure()
        }
        Log.e("TEST", "$id $op done .......")

        return Result.success()
    }

    override fun onStopped() {
        super.onStopped()
    }
}

// 添加唯一任務
WorkManager.getInstance(applicationContext).enqueueUniqueWork("checkDisk",
    ExistingWorkPolicy.REPLACE,
    OneTimeWorkRequestBuilder<DoWorker>()
        .setInputData(workDataOf("op" to "Check"))
        .build().also {
            Log.e("TEST", "${it.id} Check")
        })

唯一工作任務的衝突策略有以下幾種:

  • ExistingWorkPolicy.REPLACE 如果系統隊列中存在有同樣唯一名稱的工作任務,取消並刪除它,然後用新的工作任務替代;
  • ExistingWorkPolicy.KEEP 如果系統隊列中存在同樣唯一名稱的工作任務,保留原來的,忽略掉添加請求;
  • ExistingWorkPolicy.APPEND 如果系統隊列中存在同樣唯一名稱的工作任務,將新的工作任務添加到隊列,等待隊列中同名的任務完成之後,再執行新的工作任務。

注意: APPEND 策略不能和 PeriodicWorkRequest 一起使用,因爲 PeriodicWorkRequest 不會自動進入完成狀態。

    當程序存在不能多次排隊的任務時,唯一工作就非常有用。例如,如果程序需要同步日誌到服務器,可以添加一個名爲“uploadLog”的唯一任務進行排隊,並且使用 KEEP 策略,如果任務未完成再次添加這個唯一名稱的任務,將會忽略後面添加的。

3.7.2 唯一工作鏈

    當要對一個工作任務鏈進行限制時,可以使用具有唯一名稱的唯一名稱任務鏈,使用 WorkManager.beginUniqueWork(String, ExistingWorkPolicy, OneTimeWorkRequest) 代替 WorkManager.beginWith() 即可,這樣就可以採用唯一名稱的策略來處理具有相同唯一名稱的工作鏈了。例如:檢查設備,需要檢查設備的多個項目,每一項檢查需要一定的時間來完成,將每一個檢查項的執行寫成一個Worker,添加到唯一工作鏈中,在工作鏈執行過程中,如果多次請求檢查,可以通過策略 KEEP 來忽略掉後面的檢查操作。

示例:

WorkManager.getInstance(applicationContext).beginUniqueWork("check",
    ExistingWorkPolicy.KEEP,
    OneTimeWorkRequestBuilder<DoWorker>()
        .setInputData(workDataOf("op" to "Check Disk"))
        .build())
    .then(OneTimeWorkRequestBuilder<DoWorker>()
        .setInputData(workDataOf("op" to "Check Network"))
        .build())
    .then(OneTimeWorkRequestBuilder<DoWorker>()
        .setInputData(workDataOf("op" to "Check System"))
        .build())
    .enqueue()

3.8 異步工作任務

    在 Workmanager 庫中,提供了異步工作任務的支持 RxWorker,這個 Worker 的實現基於 RxJava;還有Kotlin專有的CoroutineWorker,這個 Worker 重寫了 doWork 方法,便於執行掛起工作。

示例:

class CWorker(context: Context, workerParameters: WorkerParameters) : CoroutineWorker(context, workerParameters) {
    override suspend fun doWork(): Result { // 不同之處是doWork方法多了個suspend關鍵字
        val op = inputData.getString("op");

        Log.e("TEST", "$id $op .......")

        delay(3000)

        if(isStopped) {
            Log.e("TEST", "$id $op stopped!")
            return Result.failure()
        }
        Log.e("TEST", "$id $op 50%.......")

        delay(3000)

        if(isStopped) {
            Log.e("TEST", "$id $op stopped!")
            return Result.failure()
        }
        Log.e("TEST", "$id $op done .......")
        return Result.success()
    }
}

四、WorkManager 高級概念

4.1 WorkManager配置和初始化

    默認情況下,當您的應用啓動時,WorkManager 使用適合大多數應用的合理選項自動進行配置。如果您需要進一步自定義 WorkManager 管理和調度工作的方式,可以通過自己自定義 WorkManager 配置來初始化 WorkManager 。

4.1.1 WorkManager 2.1.0 及更高版本

    WorkManager 2.1.0 及更高版本有多種配置 WorkManager 的方式。其中最靈活方式是按需初始化。通過按需初始化,可以只在需要 WorkManager 時創建該組件,而不是每次啓動應用時都創建。這樣做可將 WorkManager 從關鍵啓動路徑中移除,從而提高應用啓動性能。要使用按需初始化,請執行以下操作:

移除默認初始化程序

    要提供自己的配置,必須先移除默認的初始化程序。爲此,請使用合併規則 tools:node="remove" 更新 AndroidManifest.xml

  1. AndroidManifest.xml 引入 tools
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.owen.workmanagerdm">
    <!-- ..... -->
</manifest>
  1. 添加 androidx.work.impl.WorkManagerInitializer provider 組件聲明,並添加合併規則 tools:node="remove"
<provider
    android:name="androidx.work.impl.WorkManagerInitializer"
    android:exported="false"
    android:multiprocess="true"
    android:authorities="${applicationId}.workmanager-init"
    tools:node="remove"/>

說明: WorkManager aar庫中的 AndroidManifest.xm 包含了初始化組件的聲明,添加合併規則 tools:node="remove" 之後,在編譯過程中,會將該組件刪除。

實現 Configuration.Provider

  1. 自定義 Application 類,並實現 Configuration.Provider 接口,重寫 getWorkManagerConfiguration() 方法,返回 Configuration 對象。

示例:

class MainApp : Application(), Configuration.Provider {


    override fun getWorkManagerConfiguration(): Configuration {
        return Configuration.Builder()
            .setMinimumLoggingLevel(android.util.Log.INFO)
            .setMaxSchedulerLimit(Configuration.MIN_SCHEDULER_LIMIT + 100)
            .build()
    }

}
  1. AndroidManifest.xml 中添加 application 聲明,在 <application> 標籤內添加 android:name 屬性,並指向自定義的 Application.

  2. 當需要使用 WorkManager 時,調用方法 WorkManager.getInstance(Context)。WorkManager 將會調用應用的自定義的 getWorkManagerConfiguration() 方法來獲取 Configuration。(無需手動調用 WorkManager.initialize()。)

4.1.2 WorkManager 2.0.1 及更早版本

    WorkManager 2.0.1 更早版本,有兩個初始化選項:

默認初始化

    當應用啓動時,WorkManager 使用自定義的 ContentProvider 進行初始化(此代碼位於內部類 androidx.work.impl.WorkManagerInitializer 中)。默認初始化使用默認初始化程序(除非明確停用它,默認初始化程序適合大多數應用)和默認 Configuration 對 WorkManager 進行初始化。

自定義初始化

    如果想完全控制初始化程序,按以下步驟進行:

  1. 首先,停用默認初始化程序
  2. 然後,定義您自己的自定義配置。
val config = Configuration.Builder()
    .setMinimumLoggingLevel(android.util.Log.INFO)
    .setMaxSchedulerLimit(Configuration.MIN_SCHEDULER_LIMIT + 100)
    .build()
  1. 最後,調用 WorkManager.initialize()手動初始化 WorkManager
WorkManager.initialize(this, config)

更多關於配置項的說明參考: Configuration.Builder

注意:爲了避免獲取WorkManager單例時出現未初始化的情況,請確保初始化在 Application.onCreate()ContentProvider.onCreate() 中進行。

4.2 在 WorkManager 中進行線程處理

    在前面的內中,提到了 WorkManager 可以異步執行後臺工作,這個實現可滿足大多數應用的需求。

WorkManager 提供了四種不同類型的工作基元:

  • Worker 是最簡單的實現,WorkManager 會在後臺線程上自動運行它。

  • CoroutineWorker 是基於Kotlin協程實現的,建議 Kotlin 用戶實現這個基類。CoroutineWorker 對後臺工作公開掛起函數,在默認情況下它們運行默認的 Dispatcher,也可以對其進行自定義。

  • RxWorker 是基於 RxJava2 實現,如果項目中很多現有異步代碼是用 RxJava 建模的,則應使用 RxWorker。與所有 RxJava2 概念一樣,您可以自由選擇所需的線程處理策略。

  • ListenableWorkerWorkerCoroutineWorkerRxWorker 的基類。該類專爲需要與基於回調的異步 API(例如 FusedLocationProviderClient)進行交互並且不使用 RxJava2 的 Java 開發者而設計。

4.2.1 在工作器中進行線程處理

    當使用 Worker 時,WorkManager 會自動在後臺線程中調用 Worker.doWork()。這個後臺線程是來自於 WorkManager 的 Configuration 中指定的 Executor。默認情況下,WorkManager 會爲您設置 Executor,但也可以通過手動初始化時自定義 Executor,詳情請參考:WorkManager配置和初始化

class MainApp : Application(), Configuration.Provider {


    override fun getWorkManagerConfiguration(): Configuration {
        return Configuration.Builder()
            .setMinimumLoggingLevel(android.util.Log.INFO)
            .setMaxSchedulerLimit(Configuration.MIN_SCHEDULER_LIMIT + 100)
            .setExecutor(Executors.newFixedThreadPool(16)) // 使用自定義的Executor
            .build()
    }

}

下面是一個簡單的工作器示例,檢查設備狀態:

class CWorker(context: Context, workerParameters: WorkerParameters) : CoroutineWorker(context, workerParameters) {
    override suspend fun doWork(): Result {
        val op = inputData.getString("op");

        Log.e("TEST", "$id $op .......")

        delay(3000)

        Log.e("TEST", "$id $op 50%.......")

        delay(3000)

        Log.e("TEST", "$id $op done .......")
        return Result.success()
    }
}

    請注意,Worker.doWork() 是同步調用的,在 Worker.doWork() 中將會以阻塞方式完成整個後臺工作,並在方法退出時完成工作。如果在 doWork() 中調用異步 API 並返回 Result,則回調可能無法正常運行。如果您遇到這種情況,請考慮使用 ListenableWorker(請參閱在 ListenableWorker 中進行線程處理)。

    正在運行的 Worker 停止時(無論是何種原因),工作器會收到對 Worker.onStopped() 的調用。重寫此方法或在代碼的檢查點處調用 Worker.isStopped(),並在必要時釋放資源。

示例:

class CWorker(context: Context, workerParameters: WorkerParameters) : CoroutineWorker(context, workerParameters) {
    override suspend fun doWork(): Result {
        val op = inputData.getString("op");

        Log.e("TEST", "$id $op .......")

        delay(3000)

        if(isStopped) {
            Log.e("TEST", "$id $op stopped!")
            return Result.failure()
        }
        Log.e("TEST", "$id $op 50%.......")

        delay(3000)

        if(isStopped) {
            Log.e("TEST", "$id $op stopped!")
            return Result.failure()
        }
        Log.e("TEST", "$id $op done .......")
        return Result.success()
    }
}

注意:當 Worker 停止後,從 Worker.doWork() 返回什麼已不重要了,因爲 Result 將被忽略。

4.2.2 在 CoroutineWorker 中進行線程處理

    WorkManager 爲 Kotlin 協程提供了非常完美的支持。如果使用 Kotlin 開發,請使用 work-runtime-ktx 依賴,定義工作器時不要擴展 Worker,而應擴展 CoroutineWorkerCoroutineWorker 使用的 API 有些不同。

    前面提到,doWork() 是同步運行的,這意味着內部的代碼是按順序執行,如果在內部實現了單項任務,這個也沒有什麼問題,但是如果裏面有多項任務時,順序執行無疑是增大了運行時間,如果將每一項任務放到協助中異步執行,將會節省很多時間,當然,前提是這些任務都是互不干擾,可以獨立執行的,例如,檢查多個文件的完整性,可以將對每個文件檢查降到一個獨立的協程中,這樣就可以節省大量的檢查時間。

示例:

class CheckFileWorker(context: Context, workerParameters: WorkerParameters) : CoroutineWorker(context, workerParameters) {
    override suspend fun doWork(): Result = coroutineScope {
        val jobs = (0 until 20).map {
            async {
                checkFile(it)
            }
        }

        val results = jobs.awaitAll()

        results.forEach {
            if(!it) {
                Result.failure()
            }
        }

        Result.success()
    }

    private suspend fun checkFile(num: Int): Boolean {
        Log.e("TEST", "Checking file $num .......")

        delay(3000)

        if(isStopped) {
            Log.e("TEST", "Checking file $num stopped!")
            return false
        }
        Log.e("TEST", "Checking file $num 50%.......")
        delay(3000)

        if(isStopped) {
            Log.e("TEST", "Checking file $num stopped!")
            return false
        }
        Log.e("TEST", "Checking file $num 100% done .......")

        return true
    }
}

注意:CoroutineWorker.doWork() 是一個“掛起”函數。跟 Worker 不同的是, CoroutineWorker.doWork() 的碼不會在 Configuration 中指定的 Executor 上運行,而是在默認的 Dispatchers.Default 上運行,也可以通過提供自己的 CoroutineContext 來自定義這個行爲。

4.2.3 在 RxWorker 中進行線程處理

    WorkManager 提供了與 RxJava2 之間的互操作性。要開始使用這種互操作性,除了 work-runtime,還應添加 work-rxjava2 依賴。定義工作器時擴展 RxWorker,而不是擴展 Worker。最後替換 RxWorker.createWork() 方法以返回 Single<Result>用於表示您執行的 Result

示例:

public class RxCheckFileWorker extends RxWorker {

    public RxCheckFileWorker(Context context, WorkerParameters params) {
        super(context, params);
    }

    @Override
    public Single<Result> createWork() {
        return Observable.range(0, 100)
            .flatMap { 
                checkFile(it)
            }
            .toList()
            .map { 
                Result.success() 
            };
    }
}

注意:RxWorker.createWork() 在主線程上調用,但默認情況下會在後臺線程上訂閱返回值。您可以替換 RxWorker.getBackgroundScheduler() 來更改訂閱線程。另外,如果使用Kotlin開發,強烈建議使用 CoroutineWorker

     RxWorker 在停止時會妥善處理 Observer,因此,在停止工作器時無需做任何的特殊處理。

4.2.4 在 ListenableWorker 中進行線程處理

    在某些情況下,可能需要自定義線程處理策略。例如,需要處理基於回調的異步操作。在這種情況下,不能只依靠 Worker 來完成操作,因爲它無法以阻塞方式完成這項任務。這時就需要通過 ListenableWorker 來實現了。ListenableWorker 是最低層級的工作器 API;WorkerCoroutineWorkerRxWorker 都是從這個類衍生而來的。ListenableWorker 只會發出應該開始和停止工作的信號,而線程處理則完全自定義編碼實現。開始工作的信號是在主線程上調用,因此手動選擇轉到後臺的線程也就只管重要。

    抽象方法 ListenableWorker.startWork() 會返回一個包含操作 ResultListenableFutureListenableFuture 是一個輕量級接口:它是一個 Future,用於提供設置監聽器和傳遞異常信息的功能。在 startWork 方法中,必須返回一個 ListenableFuture,在操作完成後,需要將操作的 Result 設置到到這個 ListenableFuture

可以通過以下兩種方式創建 ListenableFuture

  • 如果使用的是 Guava,請使用 ListeningExecutorService。
  • 否則,添加 councurrent-futures 依賴,然後使用 CallbackToFutureAdapter

示例:

class CheckFileListenableWorker(context: Context, workerParameters: WorkerParameters) : ListenableWorker(context, workerParameters) {

    override fun startWork(): ListenableFuture<Result> {
        return CallbackToFutureAdapter.getFuture(CallbackToFutureAdapter.Resolver {

            var succ = 0

            val callback = object : Callback {
                override fun onSuccess(id: Int) {
                    Log.e("TEST", "File $id check success")
                    succ++
                    if(succ == 8) {
                        it.set(Result.success())
                    }
                }

                override fun onFail(id: Int) {
                    Log.e("TEST", "File $id check failed")
                    it.set(Result.failure())
                }
            }

            val jobs = (0 until 8).map { index ->
                checkFileAsync(index, callback)
            }

            callback
        })
    }

    private fun checkFileAsync(num: Int, callback: Callback): Boolean {
        Thread(Runnable {
            Log.e("TEST", "Checking file $num .......")

            if(isStopped) {
                Log.e("TEST", "Checking file $num stopped!")
                return@Runnable
            }
            Log.e("TEST", "Checking file $num 50%.......")
            Thread.sleep(3000)

            if(isStopped) {
                Log.e("TEST", "Checking file $num stopped!")
                return@Runnable
            }
            Log.e("TEST", "Checking file $num 100% done .......")
            callback.onSuccess(num)
        }).start()

        return true
    }

    interface Callback {

        fun onSuccess(id: Int)

        fun onFail(id: Int)
    }
}

    如果工作停止,則始終會取消 ListenableWorkerListenableFuture。通過使用 CallbackToFutureAdapteraddCancellationListener() API 添加一個取消監聽器,即可監聽到 Work 被取消的事件。如果 Work 被取消,可以處理 Work 內部啓動的異步線程,節省資源,而不是任由那些線程自生自滅。

示例:

class CheckFileListenableWorker(context: Context, workerParameters: WorkerParameters) : ListenableWorker(context, workerParameters) {

    override fun startWork(): ListenableFuture<Result> {
        return CallbackToFutureAdapter.getFuture(CallbackToFutureAdapter.Resolver {

            var succ = 0

            val callback = object : Callback {
                override fun onSuccess(id: Int) {
                    Log.e("TEST", "File $id check success")
                    succ++
                    if(succ == 8) {
                        it.set(Result.success())
                    }
                }

                override fun onFail(id: Int) {
                    Log.e("TEST", "File $id check failed")
                    it.set(Result.failure())
                }
            }

            it.addCancellationListener(object : Runnable {
                override fun run() {
                    Log.e("Test", "Worker was canceled!!!!!!!")
                    // TODO 在這裏可以取消異步線程的運行
                }

            }, Executors.newFixedThreadPool(8))

            val jobs = (0 until 8).map { index ->
                checkFileAsync(index, callback)
            }

            callback
        })
    }

    private fun checkFileAsync(num: Int, callback: Callback): Boolean {
        Thread(Runnable {
            Log.e("TEST", "Checking file $num .......")

            if(isStopped) {
                Log.e("TEST", "Checking file $num stopped!")
                return@Runnable
            }
            Log.e("TEST", "Checking file $num 50%.......")
            Thread.sleep(3000)

            if(isStopped) {
                Log.e("TEST", "Checking file $num stopped!")
                return@Runnable
            }
            Log.e("TEST", "Checking file $num 100% done .......")
            callback.onSuccess(num)
        }).start()

        return true
    }

    interface Callback {

        fun onSuccess(id: Int)

        fun onFail(id: Int)
    }
}

編後語

本文篇幅有點長,是筆者在研究 WorkManager 過程中書寫,如果有不正確之處請指教。另外,喜歡閱讀官方文檔的同學可以參考:WorkManager

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章