10-WorkManager

WorkManager

概覽

使用 WorkManager API 可以輕鬆地調度即使在應用退出或設備重啓時仍應運行的可延遲異步任務。

主要功能

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

WorkManager 旨在用於可延遲運行(即不需要立即運行)並且在應用退出或設備重啓時必須能夠可靠運行的任務。例如:

  • 向後端服務發送日誌或分析數據
  • 定期將應用數據與服務器同步

WorkManager 不適用於應用進程結束時能夠安全終止的運行中後臺工作,也不適用於需要立即執行的任務。請查看後臺處理指南,瞭解哪種解決方案符合您的需求。

使用入門

將WorkManager添加到項目中

    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 類定義的。doWork() 方法在 WorkManager 提供的後臺線程上同步運行。

要創建後臺任務,請擴展 Worker 類並替換 doWork() 方法。

例如,要創建上傳圖像的 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 會通知 WorkManager 任務是否:

  • 已成功完成:Result.success()

  • 已失敗:Result.failure()

  • 需要稍後重試:Result.retry()

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

Worker 定義工作單元,WorkRequest 則定義工作的運行方式和時間。

任務可以是一次性的,也可以是週期性的。

在本例中,爲 UploadWorker 構建 WorkRequest 最簡單的示例爲:

    val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>().build()

WorkRequest 中還可以包含其他信息,例如任務在運行時應遵循的約束、工作輸入、延遲,以及重試工作的退避時間政策。

將任務提交給系統

定義 WorkRequest 之後,您現在可以通過 WorkManager 使用 enqueue() 方法來調度它。

    WorkManager.getInstance(myContext).enqueue(uploadWorkRequest)

後續步驟

方法指南

定義WorkRequest

工作約束

您可以向工作添加 Constraints,以指明工作何時可以運行。

例如,您可以指定工作應僅在設備空閒且接通電源時運行。

下面的代碼展示瞭如何將這些約束添加到 OneTimeWorkRequest。有關所支持約束的完整列表,請參閱 Constraints.Builder 參考文檔

    // Create a Constraints object that defines when the task should run
    val constraints = Constraints.Builder()
            .setRequiresDeviceIdle(true)
            .setRequiresCharging(true)
            .build()

    // ...then create a OneTimeWorkRequest that uses those constraints
    val compressionWork = OneTimeWorkRequestBuilder<CompressWorker>().setConstraints(constraints).build()

如果指定了多個約束,您的任務將僅在滿足所有約束時纔會運行。

如果在任務運行期間某個約束不再得到滿足,則 WorkManager 將停止工作器。當約束繼續得到滿足時,系統將重新嘗試執行該任務。

初始延遲

如果您的工作沒有約束,或者工作加入隊列時所有約束均已得到滿足,則系統可能會選擇立即運行任務。如果您不希望任務立即運行,則可以將工作指定爲在經過最短的初始延遲後啓動。

下面的示例展示瞭如何將任務設置爲在加入隊列後至少經過 10 分鐘再運行。

    val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
            .setInitialDelay(10, TimeUnit.MINUTES)
            .build()

注意:執行工作器的確切時間還取決於 WorkRequest 中使用的約束和系統優化。WorkManager 經過設計,能夠在滿足這些約束的情況下提供可能的最佳行爲。

重試和退避政策

如果您需要讓 WorkManager 重新嘗試執行您的任務,可以從工作器返回 Result.retry()

然後,系統會根據默認的退避延遲時間和政策重新調度您的工作。退避延遲時間指定重試工作前的最短等待時間。退避政策定義了在後續重試的嘗試過程中,退避延遲時間隨時間以怎樣的方式增長;默認情況下按 EXPONENTIAL 延長。

以下是自定義退避延遲時間和政策的示例。

		val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
            .setBackoffCriteria(
                    BackoffPolicy.LINEAR,
                    OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
                    TimeUnit.MILLISECONDS)
            .build()

Criteria 標準, 準則, 規範

定義任務的輸入/輸出

任務可能需要數據以輸入參數的形式傳入,或者將數據返回爲結果。例如,某個任務負責處理圖像上傳,它要求以要上傳的圖像的 URI 爲輸入,並且可能要求用已上傳圖像的網址作爲輸出。

輸入和輸出值以鍵值對的形式存儲在 Data 對象中。下面的代碼展示瞭如何在 WorkRequest 中設置輸入數據。

    // workDataOf (part of KTX) converts a list of pairs to a [Data] object.
    val imageData = workDataOf(Constants.KEY_IMAGE_URI to imageUriString)

    val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
            .setInputData(imageData)
            .build()

Worker 類可通過調用 Worker.getInputData() 訪問輸入參數。

類似地,Data 類可用於輸出返回值。要返回 Data 對象,請將它包含到 ResultResult.success()Result.failure() 中,如下所示。

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

        override fun doWork(): Result {

                // Get the input
                val imageUriInput = getInputData().getString(Constants.KEY_IMAGE_URI)
                // TODO: validate inputs.
                // Do the work
                val response = uploadFile(imageUriInput)

                // Create the output of the work
                val outputData = workDataOf(Constants.KEY_IMAGE_URL to response.imageUrl)

                // Return the output
                return Result.success(outputData)

        }
    }

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

標記工作

可以通過爲任意 WorkRequest 對象分配標記字符串,按邏輯對任務進行分組。這樣就可以對使用特定標記的所有任務執行操作。

例如,WorkManager.cancelAllWorkByTag(String) 會取消使用特定標記的所有任務,而 WorkManager.getWorkInfosByTagLiveData(String) 會返回 LiveData 和具有該標記的所有任務的狀態列表。

以下代碼展示瞭如何使用 WorkRequest.Builder.addTag(String) 向任務添加“cleanup”標記:

    val cacheCleanupTask =
            OneTimeWorkRequestBuilder<CacheCleanupWorker>()
        .setConstraints(constraints)
        .addTag("cleanup")
        .build()

觀察工作狀態

工作狀態

在工作的整個生命週期內,它會經歷多個不同的 State

  • 如果有尚未完成的前提性工作,則工作處於 BLOCKED State
  • 如果工作能夠在滿足 Constraints 和時機條件後立即運行,則被視爲處於 ENQUEUED 狀態。
  • 當工作器在活躍地執行時,其處於 RUNNING State
  • 如果工作器返回 Result.success(),則被視爲處於 SUCCEEDED 狀態。這是一種終止 State;只有 OneTimeWorkRequest 可以進入這種 State
  • 相反,如果工作器返回 Result.failure(),則被視爲處於 FAILED 狀態。這也是一個終止 State;只有 OneTimeWorkRequest 可以進入這種 State。所有依賴工作也會被標記爲 FAILED,並且不會運行。
  • 當您明確取消尚未終止的 WorkRequest 時,它會進入 CANCELLED State。所有依賴工作也會被標記爲 CANCELLED,並且不會運行。
觀察工作狀態

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

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

利用每個方法的 LiveData 變量,您可以通過註冊監聽器來觀察 WorkInfo 的變化。例如,如果您想要在某項工作成功完成後向用戶顯示消息,您可以進行如下設置:

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

觀察工作器的中間進度

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

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

只有在 ListenableWorker 運行時才能觀察到和更新進度信息。如果嘗試在 ListenableWorker 完成執行後在其中設置進度,則將會被忽略。您還可以使用 getWorkInfoBy…()getWorkInfoBy…LiveData() 方法來觀察進度信息。這兩個方法會返回 WorkInfo 的實例,後者有一個返回 Data 的新 getProgress() 方法。

更新進度

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

此示例展示了一個簡單的 ProgressWorker。該 Worker 啓動時將進度設置爲 0,完成時將進度值更新爲 100。

    import android.content.Context
    import androidx.work.CoroutineWorker
    import androidx.work.Data
    import androidx.work.WorkerParameters
    import kotlinx.coroutines.delay

    class ProgressWorker(context: Context, parameters: WorkerParameters) :
        CoroutineWorker(context, parameters) {

        companion object {
            const val Progress = "Progress"
            private const val delayDuration = 1L
        }

        override suspend fun doWork(): Result {
            val firstUpdate = workDataOf(Progress to 0)
            val lastUpdate = workDataOf(Progress to 100)
            setProgress(firstUpdate)
            delay(delayDuration)
            setProgress(lastUpdate)
            return Result.success()
        }
    }
觀察進度

觀察進度信息也很簡單。您可以使用 getWorkInfoBy…()getWorkInfoBy…LiveData() 方法,並引用 WorkInfo

以下是使用 getWorkInfoByIdLiveData API 的示例。

    WorkManager.getInstance(applicationContext)
        // requestId is the WorkRequest id
        .getWorkInfoByIdLiveData(requestId)
        .observe(observer, Observer { workInfo: WorkInfo? ->
                if (workInfo != null) {
                    val progress = workInfo.progress
                    val value = progress.getInt(Progress, 0)
                    // Do something with progress information
                }
        })

將工作鏈接在一起

簡介

使用 WorkManager 創建工作鏈併爲其排隊。工作鏈用於指定多個關聯任務並定義這些任務的運行順序。當您需要以特定的順序運行多個任務時,這尤其有用。

要創建工作鏈,您可以使用 WorkManager.beginWith(OneTimeWorkRequest)WorkManager.beginWith(List),這會返回 WorkContinuation 實例。

然後,可以通過 WorkContinuation 使用 WorkContinuation.then(OneTimeWorkRequest)WorkContinuation.then(List) 來添加從屬 OneTimeWorkRequest

每次調用 WorkContinuation.then(...) 都會返回一個新的 WorkContinuation 實例。如果添加了 OneTimeWorkRequestList,這些請求可能會並行運行。

最後,您可以使用 WorkContinuation.enqueue() 方法爲 WorkContinuation 鏈排隊。

讓我們看一個示例:某個應用對 3 個不同的圖像執行圖像濾鏡(可能會並行執行),然後將這些圖像壓縮在一起,再上傳它們。

    WorkManager.getInstance(myContext)
        // Candidates to run in parallel
        .beginWith(listOf(filter1, filter2, filter3))
        // Dependent work (only runs after all previous work in chain)
        .then(compress)
        .then(upload)
        // Don't forget to enqueue()
        .enqueue()
Input Merger

在使用 OneTimeWorkRequest 鏈時,父級 OneTimeWorkRequest 的輸出將作爲輸入傳遞給子級。因此在上面的示例中,filter1filter2filter3 的輸出將作爲輸入傳遞給 compress 請求。

爲了管理來自多個父級 OneTimeWorkRequest 的輸入,WorkManager 使用 InputMerger

WorkManager 提供兩種不同類型的 InputMerger

對於上面的示例,假設我們要保留所有圖像濾鏡的輸出,則應使用 ArrayCreatingInputMerger

    val compress: OneTimeWorkRequest = OneTimeWorkRequestBuilder<CompressWorker>()
        .setInputMerger(ArrayCreatingInputMerger::class)
        .setConstraints(constraints)
        .build()
鏈接和工作狀態

創建 OneTimeWorkRequest 鏈時,需要注意以下幾點:

  • 從屬 OneTimeWorkRequest 僅在其所有父級 OneTimeWorkRequest 都成功完成(即返回 Result.success())時纔會被解除阻塞(變爲 ENQUEUED 狀態)。
  • 如果有任何父級 OneTimeWorkRequest 失敗(返回 Result.failure()),則所有從屬 OneTimeWorkRequest 也會被標記爲 FAILED
  • 如果有任何父級 OneTimeWorkRequest 被取消,則所有從屬 OneTimeWorkRequest 也會被標記爲 CANCELLED

取消和停止工作

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

    WorkManager.cancelWorkById(workRequest.id)

在後臺,WorkManager 會檢查工作的 State。如果工作已經完成,則不會發生任何變化。否則,其狀態將更改爲 CANCELLED,之後就不會運行這個工作。任何依賴於這項工作WorkRequests 的狀態也將變爲 CANCELLED

此外,如果工作當前的狀態爲 RUNNING,則工作器也會收到對 ListenableWorker.onStopped() 的調用。替換此方法以處理任何可能的清理操作。我們會在下文詳細討論相關內容。

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

停止正在運行的工作器

WorkManager 停止正在運行的工作器可能有幾種不同的原因:

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

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

處理重複性工作

應用場景:

應用有時可能需要定期運行某些任務。例如,您可能要定期備份數據、下載應用中的新鮮內容,或者上傳日誌到服務器。將 PeriodicWorkRequest 用於這種需要定期執行的任務。

PeriodicWorkRequest 無法鏈接。如果您的任務需要鏈接任務,請考慮 OneTimeWorkRequest

您可以按照以下方式創建 PeriodicWorkRequest:

    val constraints = Constraints.Builder()
            .setRequiresCharging(true)
            .build()

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

    WorkManager.getInstance(myContext)
        .enqueue(saveRequest)

示例中展示了一個重複間隔爲一小時的定期工作請求。

重複間隔定義爲重複之間的最短時間。工作器的確切執行時間取決於您在工作請求中使用的約束,也取決於系統進行的優化。

在示例中,PeriodicWorkRequest 還要求設備接通電源。在這種情況下,即使過了定義的一小時重複間隔,PeriodicWorkRequest 也將在設備接通電源時運行。

注意:可以定義的最短重複間隔是 15 分鐘(與 JobScheduler API 相同)。

處理唯一作業

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

您可以通過調用 [WorkManager.enqueueUniqueWork(String, ExistingWorkPolicy, OneTimeWorkRequest)](https://developer.android.com/reference/androidx/work/WorkManager#enqueueUniqueWork(java.lang.String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest)) 或 [WorkManager.enqueueUniquePeriodicWork(String, ExistingPeriodicWorkPolicy, PeriodicWorkRequest)](https://developer.android.com/reference/androidx/work/WorkManager#enqueueUniquePeriodicWork(java.lang.String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest)) 創建唯一工作序列。第一個參數是唯一名稱 - 這是我們用來標識 WorkRequest 的鍵。第二個參數是衝突解決策略,它指定了如果已經存在一個具有該唯一名稱的未完成工作鏈,WorkManager 應該如何處理:

  • 取消現有工作鏈,並將其 REPLACE 爲新工作鏈。
  • KEEP 現有序列並忽略您的新請求。
  • 將新序列 APPEND 到現有序列,在現有序列的最後一個任務完成後運行新序列的第一個任務。您不能將 APPENDPeriodicWorkRequest 一起使用。

當您有不能夠多次排隊的任務時,唯一工作將非常有用。例如,如果您的應用需要將其數據同步到網絡,您可能需要對一個名爲“sync”的序列進行排隊,並指定當已經存在具有該名稱的序列時,應該忽略新的任務。

當您需要逐步構建一個長任務鏈時,也可以利用唯一工作序列。例如,照片編輯應用可能允許用戶撤消一長串操作。其中的每一項撤消操作可能都需要一些時間來完成,但必須按正確的順序執行。在這種情況下,應用可以創建一個“撤消”鏈,並根據需要將每個撤消操作附加到該鏈上。

最後,如果您需要創建一個唯一工作鏈,可以使用 [WorkManager.beginUniqueWork(String, ExistingWorkPolicy, OneTimeWorkRequest)](https://developer.android.com/reference/androidx/work/WorkManager?hl=en#beginUniqueWork(java.lang.String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest)) 代替 beginWith()

測試Worker實現

從2.1.0版本開始,WorkerManager提供了APIs,用來測試Woker,ListenableWorker,以及ListenableWorker的子類(CoroutineWorker / RxWorker / Worker)

在V2.1.0之前,如果要測試Workers,需要使用WorkManagerTestInitHelper去初始化WorkerManager。通過V2.1.0,不需要WorkerManagerTestInitHelper,就可以測試Woker的實現。

測試ListenableWorker及其子類

測試ListenableWorker或者它的子類(CoroutineWorker / RxWorker),使用TestListenableWorkerBuilder。這個構建器可以幫助構建ListenableWorker的實例,用於測試Worker的業務邏輯。

比如,假設我們需要測試一個如下的CoroutineWorker:

class SleepWorker(context: Context, parameters: WorkerParameters) :
    CoroutineWorker(context, parameters) {
    override suspend fun doWork(): Result {
        delay(1000) // milliseconds
        return Result.success()
    }
}

爲了測試SleepWorker,我們首先通過TestListenableWorkerBuilder創建Worker的實例。這個構建器也可以用來設置標籤、inputData、runAttemptCount等等。具體請參考 TestListenableWorker

@RunWith(AndroidJUnit4::class)
class SleepWorkerTest {
    private lateinit var context: Context

    @Before
    fun setUp() {
        context = ApplicationProvider.getApplicationContext()
    }

    @Test
    fun testSleepWorker() {
        // Kotlin code can use the TestListenableWorkerBuilder extension to
        // build the ListenableWorker
        val worker = TestListenableWorkerBuilder<SleepWorker>(context).build()
        runBlocking {
            val result = worker.doWork()
            assertThat(result, `is`(Result.success()))
        }
    }
}
測試Workers

假設我們有一個Worker,如下所示

class SleepWorker(context: Context, parameters: WorkerParameters) :
    Worker(context, parameters) {

    companion object {
        const val SLEEP_DURATION = "SLEEP_DURATION"
    }

    override fun doWork(): Result {
        // Sleep on a background thread.
        val sleepDuration = inputData.getLong(SLEEP_DURATION, 1000)
        Thread.sleep(sleepDuration)
        return Result.success()
    }
}

爲了測試這個Worker,現在我們可以使用TestWorkerBuilder。TestWorkerBuilder和TestListenableWorkerBuilder的主要區別在於,TestWorkerBuilder允許你指定Executor來運行Worker

// Kotlin code can use the TestWorkerBuilder extension to
// build the Worker
@RunWith(AndroidJUnit4::class)
class SleepWorkerTest {
    private lateinit var context: Context
    private lateinit var executor: Executor

    @Before
    fun setUp() {
        context = ApplicationProvider.getApplicationContext()
        executor = Executors.newSingleThreadExecutor()
    }

    @Test
    fun testSleepWorker() {
        val worker = TestWorkerBuilder<SleepWorker>(
            context = context,
            executor = executor,
            inputData = workDataOf("SLEEP_DURATION" to 10000L)
        ).build()

        val result = worker.doWork()
        assertThat(result, `is`(Result.success()))
    }
}

使用WorkManager進行集成測試

介紹和設置

WorkManager提供了一個work-testing神器,可以幫助Wokers進行Android Instrumentation測試的單元測試。

爲了使用work-testing神器,需要在build.gradle添加依賴

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

注意:從2.1.0開始,WorkManager提供了新的TestWorkerBuilder和TestListenableWorkerBuilder類,這兩個類可以讓你在不需要用WorkManagerTestInitHelper初始化WorkManager的情況下,就可以測試Worker中的業務邏輯。本頁中的材料對於你需要在Worker實現之外進行Worker測試時仍然很有用。

概念

測試模式下,work-testing提供了WorkerManager的特殊實現,通過WorkerManagerTestInitHelper初始化。

work-testing還提供了一個SynchronousExecutor,它讓我們可以更容易地以同步的方式編寫測試,而不需要處理多個線程、鎖或鎖的問題。

通過下面的例子,來展示一起使用這些類:

@RunWith(AndroidJUnit4::class)
class BasicInstrumentationTest {
    @Before
    fun setup() {
        val context = InstrumentationRegistry.getTargetContext()
        val config = Configuration.Builder()
            // Set log level to Log.DEBUG to make it easier to debug
            .setMinimumLoggingLevel(Log.DEBUG)
            // Use a SynchronousExecutor here to make it easier to write tests
            .setExecutor(SynchronousExecutor())
            .build()

        // Initialize WorkManager for instrumentation tests.
        WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
    }
}
結構化測試

假設我們有一個EchoWorker,期望有一些輸入數據,並簡單地將其複製(回傳)到其輸出數據

class EchoWorker(context: Context, parameters: WorkerParameters)
   : Worker(context, parameters) {
   override fun doWork(): Result {
       return when(inputData.size()) {
           0 -> Result.failure()
           else -> Result.success(inputData)
       }
   }
}
基本測試

​ 下面是一個測試EchoWorker的Android Instrumentation測試。這裏的要點是,在測試模式下測試EchoWorker與實際應用中使用EchoWorker的方式非常相似。

@Test
@Throws(Exception::class)
fun testSimpleEchoWorker() {
    // Define input data
    val input = workDataOf(KEY_1 to 1, KEY_2 to 2)

    // Create request
    val request = OneTimeWorkRequestBuilder<EchoWorker>()
        .setInputData(input)
        .build()

    val workManager = WorkManager.getInstance(applicationContext)
    // Enqueue and wait for result. This also runs the Worker synchronously
    // because we are using a SynchronousExecutor.
    workManager.enqueue(request).result.get()
    // Get WorkInfo and outputData
    val workInfo = workManager.getWorkInfoById(request.id).get()
    val outputData = workInfo.outputData
    // Assert
    assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
    assertThat(outputData, `is`(input))
}

當EchoWorker沒有獲取到任何輸入數據的時候,我們希望返回的Result是Result.failure()

@Test
@Throws(Exception::class)
fun testEchoWorkerNoInput() {
   // Create request
   val request = OneTimeWorkRequestBuilder<EchoWorker>()
       .build()

   val workManager = WorkManager.getInstance(applicationContext)
   // Enqueue and wait for result. This also runs the Worker synchronously
   // because we are using a SynchronousExecutor.
   workManager.enqueue(request).result.get()
   // Get WorkInfo
   val workInfo = workManager.getWorkInfoById(request.id).get()
   // Assert
   assertThat(workInfo.state, `is`(WorkInfo.State.FAILED))
}
模擬約束、延遲和週期性工作

WorkManagerTestInitHelper爲你提供了一個TestDriver的實例,它可以用來模擬初始化延遲、ListenableWorkers滿足約束的條件,以及PeriodicWorkRequests的時間間隔。

測試初始化延遲

Worker可以有初始延遲。要測試EchoWorker的初始延遲,相比於在測試中等待初始延遲,我們更傾向於使用TestDriver將WorkRequests的初始延遲標記爲滿足。

@Test
@Throws(Exception::class)
fun testWithInitialDelay() {
    // Define input data
    val input = workDataOf(KEY_1 to 1, KEY_2 to 2)

    // Create request
    val request = OneTimeWorkRequestBuilder<EchoWorker>()
        .setInputData(input)
        .setInitialDelay(10, TimeUnit.SECONDS)
        .build()

    val workManager = WorkManager.getInstance(getApplicationContext())
    val testDriver = WorkManagerTestInitHelper.getTestDriver()
    // Enqueue and wait for result.
    workManager.enqueue(request).result.get()
    testDriver.setInitialDelayMet(request.id)
    // Get WorkInfo and outputData
    val workInfo = workManager.getWorkInfoById(request.id).get()
    val outputData = workInfo.outputData
    // Assert
    assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
    assertThat(outputData, `is`(input))
}
測試約束

TestDriver通過setAllConstraintsMet方法,可以標記約束條件被滿足。

下面是一個關於如何用約束來測試Worker的例子。

@Test
@Throws(Exception::class)
fun testWithConstraints() {
    // Define input data
    val input = workDataOf(KEY_1 to 1, KEY_2 to 2)

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

    // Create request
    val request = OneTimeWorkRequestBuilder<EchoWorker>()
        .setInputData(input)
        .setConstraints(constraints)
        .build()

    val workManager = WorkManager.getInstance(myContext)
    val testDriver = WorkManagerTestInitHelper.getTestDriver()
    // Enqueue and wait for result.
    workManager.enqueue(request).result.get()
    testDriver.setAllConstraintsMet(request.id)
    // Get WorkInfo and outputData
    val workInfo = workManager.getWorkInfoById(request.id).get()
    val outputData = workInfo.outputData
    // Assert
    assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
    assertThat(outputData, `is`(input))
}
測試周期性工作

TestDriver還公開了一個setPeriodDelayMet,它可以用來表示一個間隔已經完成。

下面是一個使用setPeriodDelayMet的例子。

@Test
@Throws(Exception::class)
fun testPeriodicWork() {
    // Define input data
    val input = workDataOf(KEY_1 to 1, KEY_2 to 2)

    // Create request
    val request = PeriodicWorkRequestBuilder<EchoWorker>(15, MINUTES)
        .setInputData(input)
        .build()

    val workManager = WorkManager.getInstance(myContext)
    val testDriver = WorkManagerTestInitHelper.getTestDriver()
    // Enqueue and wait for result.
    workManager.enqueue(request).result.get()
    // Tells the testing framework the period delay is met
    testDriver.setPeriodDelayMet(request.id)
    // Get WorkInfo and outputData
    val workInfo = workManager.getWorkInfoById(request.id).get()
    // Assert
    assertThat(workInfo.state, `is`(WorkInfo.State.ENQUEUED))
}

調試WorkManager

高級概念

配置和初始化

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

WorkManager 2.1.0及更高版本

WorkManager 2.1.0 有多種配置 WorkManager 的方式。爲 WorkManager 提供自定義初始化的最靈活方式是使用 WorkManager 2.1.0 及更高版本中提供的按需初始化

按需初始化

通過按需初始化,您可以僅在需要 WorkManager 時創建該組件,而不必每次應用啓動時都創建。這樣做可將 WorkManager 從關鍵啓動路徑中移出,從而提高應用啓動性能。

移除默認初始化程序

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

<provider
        android:name="androidx.work.impl.WorkManagerInitializer"
        android:authorities="${applicationId}.workmanager-init"
        tools:node="remove" />
    

要詳細瞭解如何在清單中使用合併規則,請參閱有關合併多個清單文件的文檔。

實現Configration.Provider

讓您的 Application 類實現 Configuration.Provider 接口,並提供您自己的 Configuration.Provider.getWorkManagerConfiguration() 實現。

當您需要使用 WorkManager 時,請確保調用方法 WorkManager.getInstance(Context)。WorkManager 調用應用的自定義 getWorkManagerConfiguration() 方法來發現其 Configuration。(您無需自己調用 WorkManager.initialize()。)

注意:如果您在 WorkManager 初始化之前調用已棄用的無參數 WorkManager.getInstance() 方法,該方法將拋出異常。您應始終使用 WorkManager.getInstance(Context) 方法,即使您不自定義 WorkManager。

以下示例展示了自定義 getWorkManagerConfiguration() 實現:

    class myApplication() : Application(), Configuration.Provider {
         override getWorkManagerConfiguration() =
               Configuration.Builder()
                    .setMinimumLoggingLevel(android.util.Log.INFO)
                    .build()
    }
WorkManager 2.0.1及更早版本
默認初始化

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

自定義初始化

如果您想控制初始化進程,則必須停用默認初始化程序,然後定義您自己的自定義配置。

移除默認初始化程序後,可以手動初始化 WorkManager:

    // provide custom configuration
    val myConfig = Configuration.Builder()
        .setMinimumLoggingLevel(android.util.Log.INFO)
        .build()

    // initialize WorkManager
    WorkManager.initialize(this, myConfig)
    

WorkManager 單例。確保初始化在 Application.onCreate()ContentProvider.onCreate() 中運行。

有關可用自定義的完整列表,請參閱 Configuration.Builder() 參考文檔。

WorkManager中的線程處理

概覽

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

  • Worker 是最簡單的實現,前面幾節已經有所介紹。WorkManager 會在後臺線程上自動運行它(您可以將它替換掉)。請參閱工作器中的線程處理,詳細瞭解 Worker 中的線程處理。
  • 建議 Kotlin 用戶實現 CoroutineWorkerCoroutineWorker 針對後臺工作公開掛起函數。默認情況下,它們運行默認的 Dispatcher,您可以對其進行自定義。請參閱 CorventineWorker 中的線程處理,詳細瞭解 CoroutineWorker 中的線程處理。
  • 建議 RxJava2 用戶實現 RxWorker。如果您有很多現有異步代碼是用 RxJava 建模的,則應使用 RxWirkers。與所有 RxJava2 概念一樣,您可以自由選擇所需的線程處理策略。請參閱 RxWorker 中的線程處理,詳細瞭解 RxWorker 中的線程處理。
  • ListenableWorkerWorkerCoroutineWorkerRxWorker 的基類。該類專爲需要與基於回調的異步 API(例如 FusedLocationProviderClient)進行交互並且不使用 RxJava2 的 Java 開發者而設計。請參閱 ListenableWorker 中的線程處理,詳細瞭解 ListenableWorker 中的線程處理。
用Worker處理線程

​ 當您使用 Worker 時,WorkManager 會自動在後臺線程中調用 Worker.doWork()。該後臺線程來自於 WorkManager 的 Configuration 中指定的 Executor

​ 默認情況下,WorkManager 會爲您設置 Executor,但您也可以自己進行自定義。

​ 例如,您可以在應用中共享現有的後臺 Executor,也可以創建單線程 Executor 以確保所有後臺工作都按順序執行,甚至可以指定一個具有不同線程數的 ThreadPool

​ 要自定義 Executor,請確保您已啓用 WorkManager 的手動初始化。在配置 WorkManager 時,您可以按以下方式指定 Executor

    WorkManager.initialize(
        context,
        Configuration.Builder()
            .setExecutor(Executors.newFixedThreadPool(8))
            .build())

下面是一個簡單的工作器示例,它按順序下載某些網站的內容:

    class DownloadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {

        override fun doWork(): ListenableWorker.Result {
            for (i in 0..99) {
                try {
                    downloadSynchronously("https://www.google.com")
                } catch (e: IOException) {
                    return ListenableWorker.Result.failure()
                }
            }

            return ListenableWorker.Result.success()
        }
    }

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

當前正在運行的 Worker 因爲任何原因而停止時,它會收到對 Worker.onStopped() 的調用。替換此方法或在代碼的檢查點處調用 Worker.isStopped(),並在必要時釋放資源。當上述示例中的 Worker 被停止時,下載項目可能才下載了一半,並且會繼續下載,即使已經被停止也不受影響。要優化此行爲,您可以執行以下操作:

    class DownloadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {

        override fun doWork(): ListenableWorker.Result {
            for (i in 0..99) {
                if (isStopped) {
                    break
                }

                try {
                    downloadSynchronously("https://www.google.com")
                } catch (e: IOException) {
                    return ListenableWorker.Result.failure()
                }

            }

            return ListenableWorker.Result.success()
        }
    }

Worker 停止後,從 Worker.doWork() 返回什麼已不重要;Result 將被忽略。

用CoroutineWorker處理線程

對於 Kotlin 用戶,WorkManager 爲協程提供了一流的支持。要開始使用,請將 work-runtime-ktx 包含到您的 gradle 文件中。不要擴展 Worker,而應擴展 CoroutineWorker,後者使用的 API 略有不同。例如,如果要構建簡單的 CoroutineWorker 來執行某些網絡操作,則需要執行以下操作:

    class CoroutineDownloadWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {

        override suspend fun doWork(): Result = coroutineScope {
            val jobs = (0 until 100).map {
                async {
                    downloadSynchronously("https://www.google.com")
                }
            }

            // awaitAll will throw an exception if a download fails, which CoroutineWorker will treat as a failure
            jobs.awaitAll()
            Result.success()
        }
    }

請注意,CoroutineWorker.doWork() 是一個“掛起”函數。不同於 Worker,此代碼不會在 Configuration 中指定的 Executor 上運行,而是默認爲 Dispatchers.Default。您可以提供自己的 CoroutineContext 來自定義這個行爲。在上面的示例中,您可能希望在 Dispatchers.IO 上完成此操作,如下所示:

    class CoroutineDownloadWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {

        override val coroutineContext = Dispatchers.IO

        override suspend fun doWork(): Result = coroutineScope {
            val jobs = (0 until 100).map {
                async {
                    downloadSynchronously("https://www.google.com")
                }
            }

            // awaitAll will throw an exception if a download fails, which CoroutineWorker will treat as a failure
            jobs.awaitAll()
            Result.success()
        }
    }

CoroutineWorker 通過取消協程並傳播取消信號來自動處理停工情況。您無需執行任何特殊操作來處理停工情況

用RxWorker處理線程

我們提供了 WorkManager 與 RxJava2 之間的互操作性。要開始使用這種互操作性,除了 work-runtime,還應將 work-rxjava2 包含到 gradle 文件中。然後,您應該擴展 RxWorker,而不是擴展 Worker。最後替換 RxWorker.createWork() 方法以返回 Single<Result>,用於表示您執行的 Result,如下所示:

    public class RxDownloadWorker extends RxWorker {

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

        @Override
        public Single<Result> createWork() {
            return Observable.range(0, 100)
                .flatMap { download("https://www.google.com") }
                .toList()
                .map { Result.success() };
        }
    }
    

請注意,RxWorker.createWork() 在主線程上調用,但默認情況下會在後臺線程上訂閱返回值。您可以替換 RxWorker.getBackgroundScheduler() 來更改訂閱線程。

停止 RxWorker 會妥善處理 Observer,因此您無需以任何特殊方式處理停工

用ListenableWorker處理線程

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

抽象方法 ListenableWorker.startWork() 會返回一個將使用操作的 Result 設置的 ListenableFutureListenableFuture 是一個輕量級接口:它是一個 Future,用於提供附加監聽器和傳播異常的功能。在 startWork 方法中,應該返回 ListenableFuture,完成操作後,您需要使用操作的 Result 設置這個返回結果。您可以通過以下兩種方式創建 ListenableFuture

  1. 如果您使用的是 Guava,請使用 ListeningExecutorService
  2. 否則,請將 councurrent-futures 包含到您的 gradle 文件並使用 CallbackToFutureAdapter

如果您希望基於異步回調執行某些工作,可以執行如下操作:

    public class CallbackWorker extends ListenableWorker {

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

        @NonNull
        @Override
        public ListenableFuture<Result> startWork() {
            return CallbackToFutureAdapter.getFuture(completer -> {
                Callback callback = new Callback() {
                    int successes = 0;

                    @Override
                    public void onFailure(Call call, IOException e) {
                        completer.setException(e);
                    }

                    @Override
                    public void onResponse(Call call, Response response) {
                        ++successes;
                        if (successes == 100) {
                            completer.set(Result.success());
                        }
                    }
                };

                for (int i = 0; i < 100; ++i) {
                    downloadAsynchronously("https://www.google.com", callback);
                }
                return callback;
            });
        }
    }
    

如果您的工作停止會發生什麼?如果預計工作會停止,則始終會取消 ListenableWorkerListenableFuture。通過使用 CallbackToFutureAdapter,您只需添加一個取消監聽器即可,如下所示:

    public class CallbackWorker extends ListenableWorker {

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

        @NonNull
        @Override
        public ListenableFuture<Result> startWork() {
            return CallbackToFutureAdapter.getFuture(completer -> {
                Callback callback = new Callback() {
                    int successes = 0;

                    @Override
                    public void onFailure(Call call, IOException e) {
                        completer.setException(e);
                    }

                    @Override
                    public void onResponse(Call call, Response response) {
                        ++successes;
                        if (successes == 100) {
                            completer.set(Result.success());
                        }
                    }
                };

                completer.addCancellationListener(cancelDownloadsRunnable, executor);

                for (int i = 0; i < 100; ++i) {
                    downloadAsynchronously("https://www.google.com", callback);
                }
                return callback;
            });
        }
    }

支持長時間運行的工作器

​ WorkManager 2.3.0-alpha02增加了對長期運行的工作者的一流支持。在這種情況下,WorkManager可以向操作系統提供一個信號,即如果可能的話,在執行該工作的同時,應該保持進程的活力。這些Worker的運行時間可以超過10分鐘。這個新功能的用例包括批量上傳或下載(不能分組),在本地進行ML模型的壓縮,或對應用的用戶來說很重要的任務。

​ 在引擎蓋下,WorkManager代表你管理並運行一個前臺服務來執行WorkRequest,同時也會顯示一個可配置的通知。

​ ListenableWorker現在支持setForegroundAsync()API,而CoroutineWorker則支持暫停setForeground()API。這些API允許開發人員指定這個WorkRequest是重要的(從用戶的角度來看)或長期運行的WorkRequest。

​ 從 2.3.0-alpha03 版本開始,WorkManager還允許用戶創建一個PendingIntent,無需使用createCancelPendingIntent()API註冊一個新的Android組件,就可以用來取消Worker。這種方法在與setForegroundAsync()或setForeground()API一起使用時特別有用,可以用來添加一個取消Worker的通知動作。

創建和管理長時間運行的任務
Java

使用ListenableWorker或Worker的開發者可以調用setForegroundAsync()API,返回一個ListenableFuture。你也可以調用setForegroundAsync(),來更新一個正在運行的Notification。

下面是一個簡單的例子,它是一個長期運行的worker下載文件。這個Worker會跟蹤進度,更新一個正在進行的Notification,顯示下載進度。

public class DownloadWorker extends Worker {
    private static final String KEY_INPUT_URL = "KEY_INPUT_URL";
    private static final String KEY_OUTPUT_FILE_NAME = "KEY_OUTPUT_FILE_NAME";

    private NotificationManager notificationManager;

    public DownloadWorker(
        @NonNull Context context,
        @NonNull WorkerParameters parameters) {
            super(context, parameters);
            notificationManager = (NotificationManager)
                context.getSystemService(NOTIFICATION_SERVICE);
    }

    @NonNull
    @Override
    public Result doWork() {
        Data inputData = getInputData();
        String inputUrl = inputData.getString(KEY_INPUT_URL);
        String outputFile = inputData.getString(KEY_OUTPUT_FILE_NAME);
        // Mark the Worker as important
        String progress = "Starting Download";
        setForegroundAsync(createForegroundInfo(progress));
        download(inputUrl, outputFile);
        return Result.success();
    }

    private void download(String inputUrl, String outputFile) {
        // Downloads a file and updates bytes read
        // Calls setForegroundInfoAsync() periodically when it needs to update
       // the ongoing Notification
    }

    @NonNull
    private ForegroundInfo createForegroundInfo(@NonNull String progress) {
        // Build a notification using bytesRead and contentLength

        Context context = getApplicationContext();
        String id = context.getString(R.string.notification_channel_id);
        String title = context.getString(R.string.notification_title);
        String cancel = context.getString(R.string.cancel_download);
        // This PendingIntent can be used to cancel the worker
        PendingIntent intent = WorkManager.getInstance(context)
                .createCancelPendingIntent(getId());

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createChannel();
        }

        Notification notification = new NotificationCompat.Builder(context, id)
                .setContentTitle(title)
                .setTicker(title)
                .setSmallIcon(R.drawable.ic_work_notification)
                .setOngoing(true)
                // Add the cancel action to the notification which can
                // be used to cancel the worker
                .addAction(android.R.drawable.ic_delete, cancel, intent)
                .build();

        return new ForegroundInfo(notification);
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private void createChannel() {
        // Create a Notification channel
    }
}
Kotlin

Kotlin開發者應該使用CoroutineWorker,而不是使用setForegroundAsync(),你可以使用該方法的懸浮版本的setForeground()來代替。

class DownloadWorker(context: Context, parameters: WorkerParameters) :
    CoroutineWorker(context, parameters) {

    private val notificationManager =
        context.getSystemService(Context.NOTIFICATION_SERVICE) as
                NotificationManager

    override suspend fun doWork(): Result {
        val inputUrl = inputData.getString(KEY_INPUT_URL)
                       ?: return Result.failure()
        val outputFile = inputData.getString(KEY_OUTPUT_FILE_NAME)
                       ?: return Result.failure()
        // Mark the Worker as important
        val progress = "Starting Download"
        setForeground(createForegroundInfo(progress))
        download(inputUrl, outputFile)
        return Result.success()
    }

    private fun download(inputUrl: String, outputFile: String) {
        // Downloads a file and updates bytes read
        // Calls setForegroundInfo() periodically when it needs to update
        // the ongoing Notification
    }
    // Creates an instance of ForegroundInfo which can be used to update the
    // ongoing notification.
    private fun createForegroundInfo(progress: String): ForegroundInfo {
        val id = applicationContext.getString(R.string.notification_channel_id)
        val title = applicationContext.getString(R.string.notification_title)
        val cancel = applicationContext.getString(R.string.cancel_download)
        // This PendingIntent can be used to cancel the worker
        val intent = WorkManager.getInstance(applicationContext)
                .createCancelPendingIntent(getId())

        // Create a Notification channel if necessary
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createChannel()
        }

        val notification = NotificationCompat.Builder(applicationContext, id)
            .setContentTitle(title)
            .setTicker(title)
            .setContentText(progress)
            .setSmallIcon(R.drawable.ic_work_notification)
            .setOngoing(true)
            // Add the cancel action to the notification which can
            // be used to cancel the worker
            .addAction(android.R.drawable.ic_delete, cancel, intent)
            .build()

        return ForegroundInfo(notification)
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private fun createChannel() {
        // Create a Notification channel
    }

    companion object {
        const val KEY_INPUT_URL = "KEY_INPUT_URL"
        const val KEY_OUTPUT_FILE_NAME = "KEY_OUTPUT_FILE_NAME"
    }
}

從Firebase JobDispatcher遷移

從GCMNetworkManager遷移

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