Android Jetpack架構組件(七)之WorkManager

1、WorkManager概述

1.1 WorkManager簡介

在Android應用開發中,或多或少的會有後臺任務的需求,根據需求場景的不一樣,Android爲後臺任務提供了多種不一樣的解決方案,如Service、Loader、JobScheduler和AlarmManger等。後臺任務一般用在不須要用戶感知的功能,而且後臺任務執行完成後須要即時關閉任務回收資源,若是沒有合理的使用這些API就會形成電量的大量消耗。爲了解決Android電量大量消耗的問題,Android官方作了各類優化嘗試,從Doze到app Standby,經過添加各類限制和管理應用程序進程來包裝應用程序不會大量的消耗電量。android

爲了解決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,即便應用推出或者設備重啓也不須要擔憂任務被丟失。app

兼容性廣

WorkManager可以兼容API 14,而且不須要你的設備安裝Google Play Services,所以不用擔憂出現兼容性問題。異步

除此以外,WorkManager 還具有許多其餘關鍵優點。ide

工做約束

使用工做約束明肯定義工做運行的最佳條件。例如,僅在設備採用 Wi-Fi 網絡鏈接時、當設備處於空閒狀態或者有足夠的存儲空間時再運行。函數

強大的調度

WorkManager 容許開發者使用靈活的調度窗口調度工做,以運行一次性或重複工做。還能夠對工做進行標記或命名,以便調度惟一的、可替換的工做以及監控或取消工做組。已調度的工做存儲在內部託管的 SQLite 數據庫中,由 WorkManager 負責確保該工做持續進行,並在設備從新啓動後從新調度。此外,WorkManager 遵循低電耗模式等省電功能和最佳作法,所以開發者無需考慮電量消耗的問題。flex

靈活的重試政策

有時任務執行會出現失敗,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、基本使用

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、方法指南

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

相關文章
相關標籤/搜索