Android Jetpack架構元件(七)之WorkManager

xiangzhihong發表於2020-12-29

一、WorkManager概述

1.1 WorkManager簡介

在Android應用開發中,或多或少的會有後臺任務的需求,根據需求場景的不同,Android為後臺任務提供了多種不同的解決方案,如Service、Loader、JobScheduler和AlarmManger等。後臺任務通常用在不需要使用者感知的功能,並且後臺任務執行完成後需要即時關閉任務回收資源,如果沒有合理的使用這些API就會造成電量的大量消耗。為了解決Android電量大量消耗的問題,Android官方做了各種優化嘗試,從Doze到app Standby,通過新增各種限制和管理應用程式程式來包裝應用程式不會大量的消耗電量。

為了解決Android耗電的問題,Android提供了WorkManager ,用來對應用中那些不需要及時完成的任務提供一個統一的解決方案,藉助WorkManager,開發者可以輕鬆排程那些即使在退出應用或重啟裝置時仍應執行的可延期非同步任務。WorkManager是一套AP,用來替換先前的 Android 後臺排程 API(包括 FirebaseJobDispatcher、GcmNetworkManager 和 JobScheduler)等元件。WorkManager需要API級別為14,同時可保證電池續航時間。

WorkManager的相容性體現在能夠根據系統版本,選擇不同的方案來實現,在API低於23時,採用AlarmManager+Broadcast Receiver,高於23時採用JobScheduler。但無論採用哪種方式,任務最終都是交由Executor來執行。下圖展示了WorkManager底層作業排程服務的運作流程。
在這裡插入圖片描述
需要注意的是,WorkManager不是一種新的工作執行緒,它的出現不是為了替換其他型別的工作執行緒。工作執行緒通常能夠立即執行,並在任務完成後將結果反饋給使用者,而WorkManager不是即時的,它不能保證任務能夠被立即執行。

1.2 WorkManager特點

WorkManager有以下三個特點:

  • 用來實現不需要即時完成的任務,如後臺下載開屏廣告、上傳日誌資訊等;
  • 能夠保證任務一定會被執行;
  • 相容性強。
針對不需要即時完成的任務

在Android開發中,經常會遇到後臺下載、上傳日誌資訊等需求,一般來說,這些任務是不需要立即完成的,如果我們自己使用來管理這些任務,邏輯可能會非常負責,並且如果處理不恰當會造成大量的電量消耗。

後臺延時任務

WorkManager能夠保證任務一定會被執行,但不是不能保證被立即執行,也即說在適當的時候被執行。因為WorkManager有自己的資料庫,與任務相關的資訊和資料就儲存到資料庫中。所以,只要任務已經提交到WorkManager,即使應用推出或者裝置重啟也不需要擔心任務被丟失。

相容性廣

WorkManager能夠相容API 14,並且不需要你的裝置安裝Google Play Services,因此不用擔心出現相容性問題。

除此之外,WorkManager 還具備許多其他關鍵優勢。

工作約束

使用工作約束明確定義工作執行的最佳條件。例如,僅在裝置採用 Wi-Fi 網路連線時、當裝置處於空閒狀態或者有足夠的儲存空間時再執行。

強大的排程

WorkManager 允許開發者使用靈活的排程視窗排程工作,以執行一次性或重複工作。還可以對工作進行標記或命名,以便排程唯一的、可替換的工作以及監控或取消工作組。已排程的工作儲存在內部託管的 SQLite 資料庫中,由 WorkManager 負責確保該工作持續進行,並在裝置重新啟動後重新排程。此外,WorkManager 遵循低電耗模式等省電功能和最佳做法,因此開發者無需考慮電量消耗的問題。

靈活的重試政策

有時任務執行會出現失敗,WorkManager 提供了靈活的重試政策,包括可配置的指數退避政策。

工作連結

對於複雜的相關工作,我們可以使用流暢自然的介面將各個工作任務連結在一起,這樣便可以控制哪些部分依序執行,哪些部分並行執行,如下所示。

WorkManager.getInstance(...)
    .beginWith(Arrays.asList(workA, workB))
    .then(workC)
    .enqueue();
內建執行緒互操作性

WorkManager 無縫整合 RxJava 和 協程,靈活地插入您自己的非同步 API。

1.3 WorkManager的幾個概念

使用WorkManager時有幾個重要的概念需要注意。

  • Worker:任務的執行者,是一個抽象類,需要繼承它實現要執行的任務。
  • WorkRequest:指定讓哪個 Woker 執行任務,指定執行的環境,執行的順序等。要使用它的子類 OneTimeWorkRequest 或 PeriodicWorkRequest。
  • WorkManager:管理任務請求和任務佇列,發起的 WorkRequest 會進入它的任務佇列。
  • WorkStatus:包含有任務的狀態和任務的資訊,以 LiveData 的形式提供給觀察者。

二、基本使用

2.1 新增依賴

如需開始使用 WorkManager,請先將庫匯入您的 Android 專案中。

dependencies {
  def work_version = "2.4.0"
  implementation "androidx.work:work-runtime:$work_version"
}

新增依賴項並同步 Gradle 專案後。

2.2 定義 Worker

建立一個繼承自Worker的Worker類,然後在Worker類的doWork()方法中執行要執行的任務,並且需要返回任務狀態的結果。例如,在doWork()方法實現上傳影像的 任務。

public class UploadWorker extends Worker {
   public UploadWorker(
       @NonNull Context context,
       @NonNull WorkerParameters params) {
       super(context, params);
   }

   @Override
   public Result doWork() {
     // Do the work here--in this case, upload the images.
     uploadImages();
     return Result.success();
   }
}

在doWork()方法中執行的任務最終需要返回一個Result型別物件,表示任務執行結果,有三個列舉值。

  • Result.success():工作成功完成。
  • Result.failure():工作失敗。
  • Result.retry():工作失敗,根據其重試政策在其他時間嘗試。

2.3 建立 WorkRequest

完成Worker的定義後,必須使用 WorkManager 服務進行排程該工作才能執行。對於如何排程工作,WorkManager 提供了很大的靈活性。開發者可以將其安排為在某段時間內定期執行,也可以將其安排為僅執行一次。

不論您選擇以何種方式排程工作,請使用 WorkRequest執行任務的請求。Worker 定義工作單元,WorkRequest(及其子類)則定義工作執行方式和時間,如下所示。

WorkRequest uploadWorkRequest =
   new OneTimeWorkRequest.Builder(UploadWorker.class)
       .build();

然後,使用 WorkManager的enqueue() 方法將 WorkRequest 提交到 WorkManager,如下所示。

WorkManager
    .getInstance(myContext)
    .enqueue(uploadWorkRequest);

執行工作器的確切時間取決於 WorkRequest 中使用的約束和系統優化方式。

三、方法指南

3.1 WorkRequest

3.1.1 WorkRequest概覽

WorkRequest主要用於向Worker提交任務請求,我們可以使用WorkRequest來處理以下一些常見的場景。

  • 排程一次性工作和重複性工作
  • 設定工作約束條件,例如要求連線到 Wi-Fi 網路或正在充電才會執行WorkRequest
  • 確保至少延遲一定時間再執行工作
  • 設定重試和退避策略
  • 將輸入資料傳遞給工作
  • 使用標記將相關工作分組在一起

WorkRequest是一個抽象類,它有兩個子類,分別是OneTimeWorkRequest和PeriodicWorkRequest,前者實現只執行一次的任務,後者用來實現週期性任務。

3.1.2 一次性任務

如果任務只需要執行一次,那麼可以使用WorkRequest的子類OneTimeWorkRequest。對於無需額外配置的簡單工作,可以使用OneTimeWorkRequest類的靜態方法 from(),如下所示。

WorkRequest myWorkRequest = OneTimeWorkRequest.from(MyWork.class);

對於更復雜的工作,則可以使用構建器的方式來建立WorkRequest,如下所示。

WorkRequest uploadWorkRequest =
   new OneTimeWorkRequest.Builder(MyWork.class)
       .build();

3.1.3 定期任務

如果需要定期執行某些工作,那麼可以使用PeriodicWorkRequest。例如,可能需要定期備份資料、定期下載應用中的新鮮內容或者定期上傳日誌到伺服器等。

PeriodicWorkRequest saveRequest =
       new PeriodicWorkRequest.Builder(SaveImageToFileWorker.class, 1, TimeUnit.HOURS)
           .build();

上面的程式碼定義了一個執行時間間隔定為一小時的定期任務。不過,工作器的確切執行時間取決於您在 WorkRequest 物件中設定的約束以及系統執行的優化。

如果任務的性質對執行的時間比較敏感,可以將 PeriodicWorkRequest 配置為在每個時間間隔的靈活時間段內執行,如圖 1 所示。
在這裡插入圖片描述

如需定義具有靈活時間段的定期工作,請在建立 PeriodicWorkRequest 時傳遞 flexInterval和 repeatInterval兩個引數,如下所示。

WorkRequest saveRequest =
       new PeriodicWorkRequest.Builder(SaveImageToFileWorker.class,
               1, TimeUnit.HOURS,
               15, TimeUnit.MINUTES)
           .build();

上面的程式碼的含義是在每小時的最後 15 分鐘內執行定期工作。

3.1.4 工作約束

為了讓工作在指定的環境下執行,我們可以給WorkRequest新增約束條件,常見的約束條件如下所示。

  • NetworkType:約束執行工作所需的網路型別,例如 Wi-Fi (UNMETERED)。
  • BatteryNotLow :如果設定為 true,那麼當裝置處於“電量不足模式”時,工作不會執行。
  • RequiresCharging:如果設定為 true,那麼工作只能在裝置充電時執行。
  • DeviceIdle:如果設定為 true,則要求使用者的裝置必須處於空閒狀態才能執行工作。
  • StorageNotLow:如果設定為 true,那麼當使用者裝置上的儲存空間不足時,工作不會執行。

例如,以下程式碼會構建了一個工作請求,該工作請求僅在使用者裝置正在充電且連線到 Wi-Fi 網路時才會執行。

Constraints constraints = new Constraints.Builder()
       .setRequiredNetworkType(NetworkType.UNMETERED)
       .setRequiresCharging(true)
       .build();

WorkRequest myWorkRequest =
       new OneTimeWorkRequest.Builder(MyWork.class)
               .setConstraints(constraints)
               .build();

如果在工作執行時不滿足某個約束,那麼WorkManager 將停止工作,並且系統將在滿足所有約束後重試工作。

3.1.5 延遲工作

如果工作沒有約束,並且所有約束都得到了滿足,那麼當工作加入佇列時系統可能會選擇立即執行該工作。如果您不希望工作立即執行,可以將工作指定為在經過一段最短初始延遲時間後再啟動。

WorkRequest myWorkRequest =
      new OneTimeWorkRequest.Builder(MyWork.class)
               .setInitialDelay(10, TimeUnit.MINUTES)
               .build();

上面程式碼的作用是,設定任務在加入佇列後至少經過 10 分鐘後再執行。

3.1.6 重試和退避政策

如果需要讓WorkManager重試工作,可以使用工作器返回 Result.retry(),然後系統將根據退避延遲時間和退避政策重新排程工作。

  • 退避延遲時間指定了首次嘗試後重試工作前的最短等待時間,一般不能超過 10 秒(或者MIN_BACKOFF_MILLIS)。
  • 退避政策定義了在後續重試過程中,退避延遲時間隨時間以怎樣的方式增長。WorkManager 支援 2 個退避政策,即 LINEAR 和 EXPONENTIAL。

每個工作請求都有退避政策和退避延遲時間。預設政策是 EXPONENTIAL,延遲時間為 10 秒,開發者可以在工作請求配置中替換此預設設定。

WorkRequest myWorkRequest =
       new OneTimeWorkRequest.Builder(MyWork.class)
               .setBackoffCriteria(
                       BackoffPolicy.LINEAR,
                       OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
                       TimeUnit.MILLISECONDS)
               .build();

3.1.7 標記WorkRequest

每個工作請求都有一個唯一識別符號,該識別符號可用於標識該工作,以便取消工作或觀察其進度。如果有一組在邏輯上相關的工作,對這些工作項進行標記可能也會很有幫助。為WorkRequest新增標記使用的是addTag()方法,如下所示。

WorkRequest myWorkRequest =
       new OneTimeWorkRequest.Builder(MyWork.class)
       .addTag("cleanup")
       .build();

最後,可以向單個工作請求新增多個標記,這些標記在內部以一組字串的形式進行儲存。對於工作請求,我們可以通過 WorkRequest.getTags() 檢索其標記集。

3.1.8 分配輸入資料

有時候,任務需要輸入資料才能正常執行。例如處理圖片上傳任務時需要上傳圖片的 URI 作為輸入資料,我們將此種場景稱為分配輸入資料。

輸入值以鍵值對的形式儲存在 Data 物件中,並且可以在工作請求中設定,WorkManager 會在執行工作時將輸入 Data 傳遞給工作,Worker 類可通過呼叫 Worker.getInputData() 訪問輸入引數,如下所示。

public class UploadWork extends Worker {

   public UploadWork(Context appContext, WorkerParameters workerParams) {
       super(appContext, workerParams);
   }

   @NonNull
   @Override
   public Result doWork() {
       String imageUriInput = getInputData().getString("IMAGE_URI");
       if(imageUriInput == null) {
           return Result.failure();
       }

       uploadFile(imageUriInput);
       return Result.success();
   }
   ...
}

// Create a WorkRequest for your Worker and sending it input
WorkRequest myUploadWork =
      new OneTimeWorkRequest.Builder(UploadWork.class)
           .setInputData(
               new Data.Builder()
                   .putString("IMAGE_URI", "http://...")
                   .build()
           )
           .build();

上面的程式碼展示瞭如何建立需要輸入資料的 Worker 例項,以及如何在工作請求中傳送該例項。

3.2 Work狀態

Work在其整個生命週期內經歷了一系列 State 更改,狀態的更改分為一次性任務的狀態和週期性任務的狀態。

3.2.1 一次性任務狀態

對於一次性任務請求,工作的初始狀態為 ENQUEUED。在 ENQUEUED 狀態下,任務會在滿足其 Constraints 和初始延遲計時要求後立即執行。接下來,該工作會轉為 RUNNING 狀態,然後可能會根據工作的結果轉為 SUCCEEDEDFAILED 狀態;或者,如果結果是 retry,它可能會回到 ENQUEUED 狀態。在此過程中,隨時都可以取消工作,取消後工作將進入 CANCELLED 狀態。
在這裡插入圖片描述
上圖展示了一次性工作的生命週期狀態的變化過程,SUCCEEDED、FAILED 和 CANCELLED 均表示此工作的終止狀態。如果您的工作處於上述任何狀態,WorkInfo.State.isFinished() 都將返回 true。

3.2.2 定期任務狀態

成功和失敗狀態僅適用於一次性任務和鏈式工作,定期工作只有一個終止狀態 CANCELLED,這是因為定期工作永遠不會結束。每次執行後,無論結果如何,系統都會重新對其進行排程。

在這裡插入圖片描述
上圖展示了定時任務的生命週期狀態的變化過程。

3.3 任務管理

3.3.1 唯一任務

在定義了Worker 和 WorkRequest之後,最後一步是將工作加入佇列,將工作加入佇列的最簡單方法是呼叫 WorkManager enqueue() 方法,然後傳遞要執行的 WorkRequest。在將工作加入佇列時需要注意避免重複加入的問題,為了實現此目標,我們可以將工作排程為唯一任務。

唯一任務可確保同一時刻只有一個具有特定名稱的工作例項。與系統生成的ID不同,唯一名稱是由開發者指定,而不是由 WorkManager 自動生成。唯一任務既可用於一次性任務,也可用於定期任務。您可以通過呼叫以下方法之一建立唯一任務序列,具體取決於您是排程重複任務還是一次性任務。

  • WorkManager.enqueueUniqueWork():用於一次性工作
  • WorkManager.enqueueUniquePeriodicWork():用於定期工作

並且,這兩個方法都接受3個引數。

  • niqueWorkName:用於唯一標識工作請求的 String。
  • existingWorkPolicy:此 enum 可告知 WorkManager 如果已有使用該名稱且尚未完成的唯一工作鏈,應執行什麼操作。如需瞭解詳情,請參閱衝突解決政策。
  • work:要排程的 WorkRequest。

下面是使用唯一任務解決重複排程問題,程式碼如下。

PeriodicWorkRequest sendLogsWorkRequest = new
      PeriodicWorkRequest.Builder(SendLogsWorker.class, 24, TimeUnit.HOURS)
              .setConstraints(new Constraints.Builder()
              .setRequiresCharging(true)
          .build()
      )
     .build();
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
     "sendLogs",
     ExistingPeriodicWorkPolicy.KEEP,
     sendLogsWorkRequest);

上述程式碼在 sendLogs 作業時,如果已處於佇列中的情況下執行則系統會保留現有的作業,並且不會新增新的作業。

3.3.2 衝突解決策略

有時候,任務的排程會出現衝突,此時我們需要告知 WorkManager 在發生衝突時要執行的操作,可以通過在將工作加入佇列時傳遞一個列舉來實現此目的。對於一次性任務,系統提供了一個 ExistingWorkPolicy列舉累,它支援用於處理衝突的選項有如下幾個。

  • REPLACE:用新工作替換現有工作。此選項將取消現有工作。
  • KEEP:保留現有工作,並忽略新工作。
  • APPEND:將新工作附加到現有工作的末尾。此政策將導致您的新工作連結到現有工作,在現有工作完成後執行。

現有工作將成為新工作的先決條件,如果現有工作變為 CANCELLEDFAILED 狀態,新工作也會變為 CANCELLEDFAILED。如果您希望無論現有工作的狀態如何都執行新工作,那麼可以使用 APPEND_OR_REPLACEAPPEND_OR_REPLACE的作用是不管狀態變為 CANCELLEDFAILED 狀態,新工作仍會執行。

3.4 觀察任務狀態

在將任務加入到佇列後,我們可以根據 name、id 或與其關聯的 tag 在 WorkManager 中查詢任務的相關資訊,並且檢查它的狀態,涉及的方法有如下幾個。

// by id
workManager.getWorkInfoById(syncWorker.id); // ListenableFuture<WorkInfo>

// by name
workManager.getWorkInfosForUniqueWork("sync"); // ListenableFuture<List<WorkInfo>>

// by tag
workManager.getWorkInfosByTag("syncTag"); // ListenableFuture<List<WorkInfo>>

該查詢會返回 WorkInfo 物件的 ListenableFuture,主要包含工作的 id、其標記、其當前的 State 以及通過 Result.success(outputData) 設定的任何輸出資料。利用每個方法的 LiveData ,我們可以通過註冊監聽器來觀察 WorkInfo 的變化,如下所示。

workManager.getWorkInfoByIdLiveData(syncWorker.id)
        .observe(getViewLifecycleOwner(), workInfo -> {
    if (workInfo.getState() != null &&
            workInfo.getState() == WorkInfo.State.SUCCEEDED) {
        Snackbar.make(requireView(),
                    R.string.work_completed, Snackbar.LENGTH_SHORT)
                .show();
   }
});

並且,WorkManager 2.4.0 及更高版本還支援使用 WorkQuery 物件對已加入佇列的作業進行復雜查詢,WorkQuery 支援按工作的標記、狀態和唯一工作名稱的組合進行查詢,如下所示。

WorkQuery workQuery = WorkQuery.Builder
       .fromTags(Arrays.asList("syncTag"))
       .addStates(Arrays.asList(WorkInfo.State.FAILED, WorkInfo.State.CANCELLED))
       .addUniqueWorkNames(Arrays.asList("preProcess", "sync")
     )
    .build();

ListenableFuture<List<WorkInfo>> workInfos = workManager.getWorkInfos(workQuery);

上面程式碼的作用是查詢帶有“syncTag”標記、處於 FAILED 或 CANCELLED 狀態,且唯一工作名稱為“preProcess”或“sync”的所有任務。

3.5 取消和停止任務

3.5.1 取消任務

WorkManager支援取消對列中的任務,取消時按工作的 name、id 或與其關聯的 tag來進行取消,如下所示。

// by id
workManager.cancelWorkById(syncWorker.id);

// by name
workManager.cancelUniqueWork("sync");

// by tag
workManager.cancelAllWorkByTag("syncTag");

WorkManager 會在後臺檢查任務的當前State。如果工作已經完成,系統不會執行任何操作。否則工作的狀態會更改為 CANCELLED,之後就不會執行這個工作。

3.5.2 停止任務

正在執行的任務可能因為某些原因而停止執行,主要的原因有以下一些。

  • 明確要求取消它,可以呼叫WorkManager.cancelWorkById(UUID)方法。
  • 如果是唯一任務,將 ExistingWorkPolicy 為 REPLACE 的新 WorkRequest 加入到了佇列中時,舊的 WorkRequest 會立即被視為已取消。
  • 新增的任務約束條件不再適合。
  • 系統出於某種原因指示應用停止工作。

當任務停止後,WorkManager 會立即呼叫 ListenableWorker.onStopped()關閉可能保留的所有資源。

3.6 觀察任務的進度

WorkManager 2.3.0為設定和觀察任務的中間進度提供了支援,如果應用在前臺執行時,工作器保持執行狀態,那麼也可以使用WorkInfo 的 LiveData Api向使用者顯示此資訊。ListenableWorker 支援使用setProgressAsync() 方法來保留中間進度。ListenableWorker只有在執行時才能觀察到和更新進度資訊。

3.6.1 更新進度

對於Java 開發者來說,我們可以使用 ListenableWorker 或 Worker 的 setProgressAsync() 方法來更新非同步過程的進度。耳低於 Kotlin 開發者來說,則可以使用 CoroutineWorker 物件的 setProgress() 擴充套件函式來更新進度資訊。
,如下所示。
Java寫法:

import android.content.Context;
import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

public class ProgressWorker extends Worker {

    private static final String PROGRESS = "PROGRESS";
    private static final long DELAY = 1000L;

    public ProgressWorker(
        @NonNull Context context,
        @NonNull WorkerParameters parameters) {
        super(context, parameters);
        // Set initial progress to 0
        setProgressAsync(new Data.Builder().putInt(PROGRESS, 0).build());
    }

    @NonNull
    @Override
    public Result doWork() {
        try {
            // Doing work.
            Thread.sleep(DELAY);
        } catch (InterruptedException exception) {
            // ... handle exception
        }
        // Set progress to 100 after you are done doing your work.
        setProgressAsync(new Data.Builder().putInt(PROGRESS, 100).build());
        return Result.success();
    }
}

Kotlin寫法:

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()
    }
}

3.6.2 觀察進度

觀察進度可以使用 getWorkInfoBy…() 或 getWorkInfoBy…LiveData() 方法,此方法會返回 WorkInfo資訊,如下所示。

WorkManager.getInstance(getApplicationContext())
     // requestId is the WorkRequest id
     .getWorkInfoByIdLiveData(requestId)
     .observe(lifecycleOwner, new Observer<WorkInfo>() {
             @Override
             public void onChanged(@Nullable WorkInfo workInfo) {
                 if (workInfo != null) {
                     Data progress = workInfo.getProgress();
                     int value = progress.getInt(PROGRESS, 0)
                     // Do something with progress
             }
      }
});

參考:
Android Jetpack架構元件(六)之Room
Android Jetpack架構元件(五)之Navigation
Android Jetpack架構元件(四)之LiveData
Android Jetpack架構元件(三)之ViewModel
Android Jetpack架構元件(二)之Lifecycle
Android Jetpack架構元件(一)與AndroidX

相關文章