Android架構元件WorkManager詳解

weixin_33860722發表於2018-07-07

       WorkManager架構元件是用來管理後臺工作任務。這個時候你可能會奇怪了Android不是已經 有很多管理後臺任務的類了麼,比如JobScheduler, AlarmManger、在比如AsyncTask, ThreadPool。WorkManager。WorkManager的優勢在哪裡,我們為啥要使用WorkManager。我們從兩個方面來說明WorkManager的優勢

  • WorkManager對比JobScheduler, AlarmManger的優勢:我們要知道雖然AlarmManager是一直存在但是JobScheduler是Android 5.x之後才有的。WorkManager的底層實現,會根據你的裝置API的情況,自動選用JobScheduler, 或是AlarmManager來實現後臺任務。
  • WorkManager對比AsyncTask, ThreadPool的優勢:WorkManager裡面的任務在應用退出之後還可以繼續執行。AsyncTask, ThreadPool裡面的任務在應用退出之後不會執行。

       WorkManager適用於那些在應用退出之後任務還需要繼續執行的需求(比如應用資料上報伺服器的情況),對應那些在應用退出的之後任務也需要終止的情況就需要選擇ThreadPool、AsyncTask來實現。

一、WorkManager相關類介紹

       想使用WorkManager元件庫,第一步我們們得先了解下WorkManager裡面相關的幾個類。

1.1、Worker

       Worker用於指定需要執行的具體任務。任務的具體邏輯在Worker裡面寫。
Worker是個抽象類。所以我們需要繼承並實現這個類在定義自己的任務。

       Worker類裡面幾個比較關鍵的函式:任務邏輯實現函式,任務輸入資料的獲取函式,任務輸出資料的設定函式。

    /**
     * 任務邏輯
     * @return 任務的執行情況,成功,失敗,還是需要重新執行
     */
    @WorkerThread
    public abstract @NonNull Worker.Result doWork();

    /**
     * 任務的輸入資料,有的時候可能需要我們傳遞引數進去,比如下載檔案我們需要傳遞檔案路基進去,
     * 在doWork()函式中通過getInputData()獲取到我們傳遞進來的引數
     * @return Data引數
     */
    public final @NonNull Data getInputData() {
        return mExtras.getInputData();
    }

    /**
     * 設定我們任務輸出結果
     * @param outputData 結果
     */
    public final void setOutputData(@NonNull Data outputData) {
        mOutputData = outputData;
    }

       doWork()函式的返回值:

  • Worker.Result.SUCCESS:任務執行成功。
  • Worker.Result.FAILURE:任務執行失敗。
  • Worker.Result.RETRY:任務需要重新執行,需要配合WorkRequest.Builder裡面的setBackoffCriteria()函式使用。

1.2、WorkRequest

       WorkRequest代表一個單獨的任務,是對Worker任務的包裝,一個WorkRequest對應一個Worker類。我們可以通過WorkRequest來給Worker類新增約束細節,比如指定任務應該執行的環境,任務的輸入引數,任務只有在有網的情況下執行等等。WorkRequest是一個抽象類,元件裡面也給兩個相應的子類:OneTimeWorkRequest(任務只執行一遍)、PeriodicWorkRequest(任務週期性的執行)。

  • WorkRequest.Builder: 建立WorkRequest物件的幫助類。
  • Constraints:指定任務執行的限制條件(例如,"僅當連線到網路時")。使用Constraint.Builder來建立Constraints,並在建立WorkRequest之前把Constraints傳給WorkRequest.Builder的setConstraints()函式。

WorkRequest裡面常用函式介紹

    /**
     * 獲取 WorkRequest對應的UUID
     */
    public @NonNull UUID getId();

    /**
     * 獲取 WorkRequest對應的UUID string
     */
    public @NonNull String getStringId();

    /**
     * 獲取WorkRequest對應的WorkSpec(包含了任務的一些詳細資訊)
     */
    public @NonNull WorkSpec getWorkSpec();

    /**
     * 獲取 WorkRequest對應的tag
     */
    public @NonNull Set<String> getTags();

    public abstract static class Builder<B extends WorkRequest.Builder, W extends WorkRequest> {
        ...

        /**
         * 設定任務的退避/重試策略。比如我們在Worker類的doWork()函式返回Result.RETRY,讓該任務又重新入隊。
         */
        public @NonNull B setBackoffCriteria(
            @NonNull BackoffPolicy backoffPolicy,
            long backoffDelay,
            @NonNull TimeUnit timeUnit);


        /**
         * 設定任務的執行的限制條件,比如有網的時候執行任務,不是低電量的時候執行任務
         */
        public @NonNull B setConstraints(@NonNull Constraints constraints);

        /**
         * 設定任務的輸入引數
         */
        public @NonNull B setInputData(@NonNull Data inputData);

        /**
         * 設定任務的tag
         */
        public @NonNull B addTag(@NonNull String tag);

        /**
         * 設定任務結果儲存時間
         */
        public @NonNull B keepResultsForAtLeast(long duration, @NonNull TimeUnit timeUnit);
        @RequiresApi(26)
        public @NonNull B keepResultsForAtLeast(@NonNull Duration duration);
        ...
    }

       這裡要稍微提下Builder的setBackoffCriteria()函式的使用場景,比較常用,一般當我們任務執行失敗的時候任務需要重試的時候會用到這個函式,在任務執行失敗的時候Worker類的doWork()函式返回Result.RETRY告訴這個任務要重試。那重試的策略就是通過setBackoffCriteria()函式來設定的。BackoffPolicy有兩個值LINEAR(每次重試的時間線性增加,比如第一次10分鐘,第二次就是20分鐘)、EXPONENTIAL(每次重試時間指數增加)。

1.3、WorkManager

       管理任務請求和任務佇列,我們需要把WorkRequest物件傳給WorkManager以便將任務編入佇列。通過WorkManager來排程任務,以分散系統資源的負載。

WorkManager常用函式介紹

    /**
     * 任務入隊
     */
    public final void enqueue(@NonNull WorkRequest... workRequests);
    public abstract void enqueue(@NonNull List<? extends WorkRequest> workRequests);

    /**
     * 鏈式結構的時候使用,從哪些任務開始。
     * 比如我們有A,B,C三個任務,我們需要順序執行。那我們就可以WorkManager.getInstance().beginWith(A).then(B).then(C).enqueue();
     */
    public final @NonNull WorkContinuation beginWith(@NonNull OneTimeWorkRequest...work);
    public abstract @NonNull WorkContinuation beginWith(@NonNull List<OneTimeWorkRequest> work);


    /**
     * 建立一個唯一的工作佇列,唯一工作佇列裡面的任務不能重複新增
     */
    public final @NonNull WorkContinuation beginUniqueWork(
        @NonNull String uniqueWorkName,
        @NonNull ExistingWorkPolicy existingWorkPolicy,
        @NonNull OneTimeWorkRequest... work);
    public abstract @NonNull WorkContinuation beginUniqueWork(
        @NonNull String uniqueWorkName,
        @NonNull ExistingWorkPolicy existingWorkPolicy,
        @NonNull List<OneTimeWorkRequest> work);

    /**
     * 允許將一個PeriodicWorkRequest任務放到唯一的工作序列裡面去,但是當佇列裡面有這個任務的時候你的提供替換的策略。
     */
    public abstract void enqueueUniquePeriodicWork(
        @NonNull String uniqueWorkName,
        @NonNull ExistingPeriodicWorkPolicy existingPeriodicWorkPolicy,
        @NonNull PeriodicWorkRequest periodicWork);

    /**
     * 通過UUID取消任務
     */
    public abstract void cancelWorkById(@NonNull UUID id);

    /**
     * 通過tag取消任務
     */
    public abstract void cancelAllWorkByTag(@NonNull String tag);

    /**
     * 取消唯一佇列裡面所有的任務(beginUniqueWork)
     */
    public abstract void cancelUniqueWork(@NonNull String uniqueWorkName);

    /**
     * 取消所有的任務
     */
    public abstract void cancelAllWork();

    /**
     * 獲取任務的WorkStatus。一般會通過WorkStatus來獲取返回值,LiveData是可以感知WorkStatus資料變化的
     */
    public abstract @NonNull LiveData<WorkStatus> getStatusById(@NonNull UUID id);
    public abstract @NonNull LiveData<List<WorkStatus>> getStatusesByTag(@NonNull String tag);

    /**
     * 獲取唯一佇列裡面所有的任務(beginUniqueWork)的WorkStatus
     */
    public abstract @NonNull LiveData<List<WorkStatus>> getStatusesForUniqueWork(@NonNull String uniqueWorkName);

       beginWith(),beginUniqueWork()兩個函式開啟的佇列的唯一區別在於,佇列裡面的任務能不能重複。beginWith()開始的佇列裡面的任務是可以重複的,beginUniqueWork()開始的佇列裡面的任務是不能重複的。

1.4、WorkStatus

       包含任務的資訊。WorkManager為每個WorkRequest物件提供一個LiveData(WorkManager通過getStatusById、getStatusesByTag、getStatusesForUniqueWork函式來獲取)。LiveData持有一個WorkStatus物件。LiveData是可以感知資料變化的。通過觀察這個LiveData,我們可以確定任務的當前狀態,並在任務完成後獲得返回值。WorkStatus裡面就包含的東西不多就任務的id、tag、狀態、返回值。

通過如下方式來監聽任務的狀態

// 獲取到LiveData然後監聽資料變化
        WorkManager.getInstance().getStatusById(request.getId()).observe(this, new Observer<WorkStatus>() {
            @Override
            public void onChanged(@Nullable WorkStatus workStatus) {
                if (workStatus == null) {
                    return;
                }
                if (workStatus.getState() == State.ENQUEUED) {
                    mTextOut.setText("任務入隊");
                }
                if (workStatus.getState() == State.RUNNING) {
                    mTextOut.setText("任務正在執行");
                }
                if (workStatus.getState().isFinished()) {
                    Data data = workStatus.getOutputData();
                    mTextOut.setText("任務完成" + "-結果:" + data.getString("key_name", "null"));
                }
            }
        });

1.5、Data

       Data是用於來給Worker設定輸入引數和輸出引數的。舉個例子,比如我們需要去網路上下載圖,那麼需要給Worker傳入下載地址(輸入引數),在Worker執行成功之後我們又需要獲取到圖片在本地的保持路徑(輸出引數)。這這個傳入傳出都是通過Data來實現的。Data是一個輕量級的容器(不能超過10KB),Data通過key-value的形式來儲存資訊。

二、WorkManager使用

       前面講了WorkManager裡面常用的一些類,接下來就是WorkManager的使用了。

       我們把WorkManager的使用分為幾個步驟:

  • 繼承Worker,處理任務的具體邏輯。
  • OneTimeWorkRequest或者PeriodicWorkRequest包裝Worker,設定Worker的一些約束新增,或者Worker的輸入引數。
  • 任務入隊執行(如果是多個任務可以形成任務鏈在入隊執行)。
  • 監聽任務的輸出(LiveData的使用)。

2.1、任務的輸入輸出

       有些時候,一個任務的執行,我們可能需要從外部傳入引數,在任務結束的時候需要把任務的結果告訴外部。比如我們去網路上下載一個圖片,那我們們需要url地址(輸入),在圖片下載成功之後獲取圖片在本地的儲存路徑(輸出)。

  • 輸入引數:想要給任務傳遞輸入引數需要在WorkRequest包裝Worker的通過WorkRequest.Builder的setInputData()函式設定輸入引數。之後任務在執行過程中可以通過getInputData()獲取到傳入的引數。
  • 輸出引數:任務執行過程中如果想要結果傳遞給外界,需要在Worker中通過setOutputData()設定輸出引數。之後如果想獲取任務結果需要通過WorkManager.getInstance().getStatusById()或者WorkManager.getInstance().getStatusesByTag()先獲取到LiveData。然後通過LiveData來感知任務資料的變化。

       我們通過一個簡單的實力來看下任務有輸入輸出的情況應該怎麼處理。

定義一個任務

public class InputOutputWorker extends Worker {

    @NonNull
    @Override
    public Result doWork() {

        try {
            //模擬耗時任務
            Thread.sleep(3000);
            Data inputData = getInputData();
            //獲取到輸入的引數,我們又把輸入的引數給outputData
            Data outputData = new Data.Builder().putString("key_name", inputData.getString("key_name", "no data")).build();
            setOutputData(outputData);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return Result.SUCCESS;
    }
}

OneTimeWorkRequest設定任務的輸入,執行任務,任務的執行過程中設定輸出,LiveData感知任務結果

    private void startWorker() {
        // 定義一個OneTimeWorkRequest,並且關聯InputOutputWorker。設定輸入引數
        Data inputData = new Data.Builder().putString("key_name", "江西高安").build();
        OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(InputOutputWorker.class)
                                                           .setInputData(inputData)
                                                           .build();
        // 任務入隊,WorkManager排程執行
        WorkManager.getInstance().enqueue(request);
        // 獲取到LiveData然後監聽資料變化
        WorkManager.getInstance().getStatusById(request.getId()).observe(this, new Observer<WorkStatus>() {
            @Override
            public void onChanged(@Nullable WorkStatus workStatus) {
                if (workStatus == null) {
                    return;
                }
                if (workStatus.getState() == State.ENQUEUED) {
                    mTextOut.setText("任務入隊");
                }
                if (workStatus.getState() == State.RUNNING) {
                    mTextOut.setText("任務正在執行");
                }
                if (workStatus.getState().isFinished()) {
                    Data data = workStatus.getOutputData();
                    mTextOut.setText("任務完成" + "-結果:" + data.getString("key_name", "null"));
                }
            }
        });
    }

2.2、週期任務

       WorkManager元件庫裡面提供了一個專門做週期性任務的類PeriodicWorkRequest。但是PeriodicWorkRequest類有一個限制條件最小的週期時間是15分鐘。

    private void startWorker() {
        // 定義一個PeriodicWorkRequest,並且關聯PeriodicWorker。任務15m迴圈(原始碼裡面已經規定了最小時間間隔15分鐘)
        PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(PeriodicWorker.class, 15, TimeUnit.MINUTES).build();
        // 任務入隊,WorkManager排程執行
        WorkManager.getInstance().enqueue(request);
    }

2.3、任務新增約束

       有些情況下,有些任務可能需要新增額外的約束,比如只能在聯網的情況下才能執行,只有在裝置空閒的情況下才能執行等等。WorkManager裡面所有的約束條件都是通過Constraints來實現的,Constraints也是通過Constraints.Builder()來實現的。

       關於任務的約束要注意,可能我們任務加入的那個時刻候沒有滿足約束的條件,任務沒有執行。但是過後一旦約束條件滿足之後任務會自動執行的。

Constraints常用函式-可以新增的限制如下

    /**
     * 是否在充電狀態下執行任務
     */
    public @NonNull Constraints.Builder setRequiresCharging(boolean requiresCharging);


    /**
     * 是否在裝置空閒的時候執行
     */
    @RequiresApi(23)
    public @NonNull Constraints.Builder setRequiresDeviceIdle(boolean requiresDeviceIdle);


    /**
     * 指定網路狀態執行任務
     * NetworkType.NOT_REQUIRED:對網路沒有要求
     * NetworkType.CONNECTED:網路連線的時候執行
     * NetworkType.UNMETERED:不計費的網路比如WIFI下執行
     * NetworkType.NOT_ROAMING:非漫遊網路狀態
     * NetworkType.METERED:計費網路比如3G,4G下執行。
     */
    public @NonNull Constraints.Builder setRequiredNetworkType(@NonNull NetworkType networkType);


    /**
     * 在電量不足的是否是否可以執行任務
     */
    public @NonNull Constraints.Builder setRequiresBatteryNotLow(boolean requiresBatteryNotLow);


    /**
     * 在儲存容量不足時是否可以執行
     */
    public @NonNull Constraints.Builder setRequiresStorageNotLow(boolean requiresStorageNotLow);

    /**
     * 當Uri有更新的時候是否執行任務
     */
    @RequiresApi(24)
    public @NonNull Constraints.Builder addContentUriTrigger(Uri uri, boolean triggerForDescendants);

       我們舉一個簡單的例子,比如我們限制任務只有在wifi的狀態下才能執行。

    /**
     * 啟動約束任務
     */
    private void startWorker() {
        // 設定只有在wifi狀態下才能執行
        Constraints constraints = new Constraints.Builder().setRequiredNetworkType(NetworkType.UNMETERED).build();
        // 設定約束條件
        OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(ConstraintsWorker.class).setConstraints(constraints).build();
        // 任務入隊,WorkManager排程執行
        WorkManager.getInstance().enqueue(request);
    }

2.3、任務取消

       每個任務都有自己獨特的UUID,我們可以通過任務的UUID找到任務,然後取消他。除了UUID的方式,我們還可以給任務新增tag,然後通過tag來取消任務(可以給多個任務新增同一個tag,同時取消)。

       我們簡單的實現一個通過tag取消任務的例子

    /**
     * 給任務設定tag
     */
    private void startWorker() {
        // 給任務設定tag->cancel
        OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(ConstraintsWorker.class).addTag("cancel").build();
        // 任務入隊,WorkManager排程執行
        WorkManager.getInstance().enqueue(request);
    }

    /**
     * 通過tag取消任務
     */
    private void cancelWorker() {
        WorkManager.getInstance().cancelAllWorkByTag("cancel");
    }

2.4、鏈式任務

       有時候我們想讓某些按照特定的順序行來執行。WorkManager允許我們建立和排隊多個任務的工作序列,以及它們應該以什麼順序執行。這個就是鏈式任務了。

       任務鏈裡面的任何一個任務返回WorkerResult.FAILURE,則整個任務鏈終止。

       鏈式任務的關鍵在WorkContinuation,通過WorkContinuation來整理好佇列(是順序執行,還是組合執行)然後入隊執行。

WorkContinuation裡面常用函式介紹


    /**
     * 順序執行任務,
     * 如果then的引數指定了多個任務那麼這些任務的執行順序是沒有規律的,但是一定要等這個then函式引數裡面所有任務都執行完了才會去執行下一個then的任務
     * 任務任務返回Worker.WorkerResult.FAILURE整個則整個任務鏈結束
     */
    public final @NonNull WorkContinuation then(@NonNull OneTimeWorkRequest... work);
    public abstract @NonNull WorkContinuation then(@NonNull List<OneTimeWorkRequest> work);


    /**
     * 這些都是static函式哦,用於組合任務
     */
    public static @NonNull WorkContinuation combine(@NonNull WorkContinuation... continuations);
    public static @NonNull WorkContinuation combine(@NonNull List<WorkContinuation> continuations);
    public static @NonNull WorkContinuation combine(@NonNull OneTimeWorkRequest work,
        @NonNull WorkContinuation... continuations);
    public static @NonNull WorkContinuation combine(@NonNull OneTimeWorkRequest work,
        @NonNull List<WorkContinuation> continuations);

    /**
     * 獲取任務鏈中所有任務的LiveData,用於監聽任務鏈裡面任務的結果
     */
    public abstract @NonNull LiveData<List<WorkStatus>> getStatuses();
    
    /**
     * 任務鏈中的任務入隊,開始執行。
     */
    public abstract void enqueue();

2.4.1、任務順序執行

       任務順序執行,WorkContinuation的then()函式的使用。假設我們有A,B,C三個任務需要按順序執行。

    /**
     * A,B,C三個任務順序執行
     */
    private void startWorker() {
        // A
        OneTimeWorkRequest requestA = new OneTimeWorkRequest.Builder(OrderWorkerA.class).build();
        // B
        OneTimeWorkRequest requestB = new OneTimeWorkRequest.Builder(OrderWorkerB.class).build();
        // C
        OneTimeWorkRequest requestC = new OneTimeWorkRequest.Builder(OrderWorkerC.class).build();
        // 任務入隊,WorkManager排程執行
        WorkManager.getInstance().beginWith(requestA).then(requestB).then(requestC).enqueue();
    }

       為了把順序任務說的更加徹底,我們在來一個例子。

WorkManager.getInstance()
    // First, run all the A tasks (in parallel):
    .beginWith(workA1, workA2, workA3)
    // ...when all A tasks are finished, run the single B task:
    .then(workB1, workB2)
    // ...then run the C tasks (in any order):
    .then(workC1, workC2)
    .enqueue();

       上訴程式碼中beginWith函式裡面的workA1, workA2, workA3三個任務是平行(同時)執行的,而且要等workA1, workA2, workA3都執行完才能做下一步the裡的任務。then(workB1, workB2)裡面的workB1,workB2的執行順序是沒有規律的,但是一定要等到workB1,workB2都執行玩才能執行下一步的then裡面的任務。workC1, workC2的執行順序也是沒有規律的。

2.4.2、組合任務

       想要組合任務,就需要用到WorkContinuation的combine()函式了。我們用一個非常見到的任務來說明組合任務的執行。比如我們想要實現如下圖所示的鏈試效果。

9182331-8cd991abbeb673de.png
combine.png

上圖對應程式碼如下

    /**
     * 組合任務
     */
    private void startWorker() {
        OneTimeWorkRequest requestA = new OneTimeWorkRequest.Builder(ConbineWorkerA.class).build();
        OneTimeWorkRequest requestB = new OneTimeWorkRequest.Builder(ConbineWorkerB.class).build();
        OneTimeWorkRequest requestC = new OneTimeWorkRequest.Builder(ConbineWorkerC.class).build();
        OneTimeWorkRequest requestD = new OneTimeWorkRequest.Builder(ConbineWorkerD.class).build();
        OneTimeWorkRequest requestE = new OneTimeWorkRequest.Builder(ConbineWorkerE.class).build();
        //A,B任務鏈
        WorkContinuation continuationAB = WorkManager.getInstance().beginWith(requestA).then(requestB);
        //C,D任務鏈
        WorkContinuation continuationCD = WorkManager.getInstance().beginWith(requestC).then(requestD);
        //合併上面兩個任務鏈,在接入requestE任務,入隊執行
        WorkContinuation.combine(continuationAB, continuationCD).then(requestE).enqueue();
    }

2.4.3、任務鏈中任務資料流(每個任務的輸入輸出)

       在任務鏈中,我們可能會有這樣的需求,任務之間的資料是相互依賴的,下一個任務需要上一個任務的輸出資料。這種情況我們就稱之為任務鏈中任務的資料流。其實強大的WorkManager已經幫我們設計好了。WorkManager會把上一個任務的輸出自動作為下一個人任務的輸入。

2.4.3.1、順序任務的資料流

       因為WorkManager設計的時候已經幫我們設計好了上一任務的輸出會自動作為下一個任務的輸入。所以順序任務的資料流是非常好處理的。上一個任務呼叫setOutputData()返回其結果,下一個任務呼叫getInputData()來獲取上一個任務的結果。我們用一個簡單的例項來說明。A,B,C三個順序任務。A任務輸出10,B任務得到A任務的值再乘以10,最後把結果給到C任務。我們來看下這種情況下的程式碼應該怎麼寫。

A任務

/**
 * A任務輸出10
 */
public class StreamThenWorkerA extends Worker {

    @NonNull
    @Override
    public Result doWork() {
        Data data = new Data.Builder().putInt("a_out", 10).build();
        setOutputData(data);
        return Result.SUCCESS;
    }
}

B任務

/**
 * 得到A任務的輸出在乘以10,做為輸出
 */
public class StreamThenWorkerB extends Worker {

    @NonNull
    @Override
    public Result doWork() {
        //先得到A任務的輸出值
        Data inputData = getInputData();
        int a_out = inputData.getInt("a_out", 0);
        //把A任務的輸出×10在給到C任務
        Data data = new Data.Builder().putInt("b_out", a_out * 10).build();
        setOutputData(data);
        return Result.SUCCESS;
    }
}

C任務

/**
 * 只是做一個簡單的列印
 */
public class StreamThenWorkerC extends Worker{

    @NonNull
    @Override
    public Result doWork() {
        Data inputData = getInputData();
        int b_out = inputData.getInt("b_out", 0);
        //獲取到B任務的輸出,我們只是做一個簡單的輸出。
        Log.d("tuacy", "value = " + b_out);
        return Result.SUCCESS;
    }
}

執行任務

    /**
     * 順序任務的資料流
     * A,B,C三個任務。A,輸出10,B任務得到A任務的值×10,最後給到C任務。
     */
    private void startThenWorker() {
        OneTimeWorkRequest requestA = new OneTimeWorkRequest.Builder(StreamThenWorkerA.class).build();
        OneTimeWorkRequest requestB = new OneTimeWorkRequest.Builder(StreamThenWorkerB.class).build();
        OneTimeWorkRequest requestC = new OneTimeWorkRequest.Builder(StreamThenWorkerC.class).build();
        WorkManager.getInstance().beginWith(requestA).then(requestB).then(requestC).enqueue();
    }

2.4.3.2、組合任務的資料流

       組合任務的資料流稍稍複雜一點,因為涉及到多個任務的輸出同時作為一個任務的輸入,這個時候多個任務的輸出需要合併一個輸入。這個時候就會有合併規則一說了(不同的任務中有相同的key應該怎麼處理),WorkManager通過OneTimeWorkRequest.Builder類的setInputMerger()函式來指定多個任務輸入流的合併規則。引數是繼承自InputMerger的類。InputMerger是一個抽象類,WorkManager也給我們提供了兩種合併規則:ArrayCreatingInputMerger、OverwritingInputMerger。

  • ArrayCreatingInputMerger:所有key對應的value都會放到陣列裡面,有相同的key的話,陣列慢慢擴大。比如有A、B兩個任務的輸出需要組合到一起。A任務輸出裡面有一個key:a_key->100。B任務裡面有兩個key(有個key和A任務是相同的):b_key->100、a_key->200。最後通過ArrayCreatingInputMerger規則組合的結果是:a_key對應一個陣列,陣列裡面有兩個元素100和200、b_key也對應一個陣列,裡面只有一個元素100。這個時候在下一個任務中想要獲取合併之後的輸入必須使用getIntArray(),因為現在key對應的value是一個陣列了。
  • OverwritingInputMerger:如果有相同的key,直接覆蓋。我通過測試發現OverwritingInputMerger沒效果,表現形式和ArrayCreatingInputMerger一樣

       我們還是用一個簡單的例子來說明組合任務的資料流,我們有A,B,C三個任務。A,B任務合併再執行C任務。在C任務中獲取A,B兩個任務的輸出。

A任務的輸出中只有一個key: a_key -> 100

/**
 * A任務的輸出中只有一個key: a_key -> 100
 */
public class StreamCombineWorkerA extends Worker {

    @NonNull
    @Override
    public Result doWork() {
        Data data = new Data.Builder().putInt("a_key", 100).build();
        setOutputData(data);
        return Result.SUCCESS;
    }
}

B任務的輸出中有兩個key:b_key -> 100、a_key -> 200,有個key在A任務中也出現了

/**
 * B任務的輸出中有兩個key:b_key -> 100、a_key -> 200
 * 有個key在A任務中也出現了
 */
public class StreamCombineWorkerB extends Worker {

    @NonNull
    @Override
    public Result doWork() {
        Data data = new Data.Builder().putInt("b_key", 100).putInt("a_key", 200).build();
        setOutputData(data);
        return Result.SUCCESS;
    }
}

C任務只是簡單的獲取A,B任務的輸出

/**
 * 在C任務中獲取到A,B任務的輸出。
 *
 */
public class StreamCombineWorkerC extends Worker {

    @NonNull
    @Override
    public Result doWork() {
        Data data = getInputData();

        // 注意;這裡我用的是getIntArray
        int[] aKeyValueList = data.getIntArray("a_key");
        int[] bKeyValueList = data.getIntArray("b_key");
        Log.d("tuacy", "a_key = " + aKeyValueList[0]);
        Log.d("tuacy", "b_key = " + bKeyValueList[0]);

        return Result.SUCCESS;
    }
}

啟動組合任務,呼叫setInputMerger(
OverwritingInputMerger.class)來設定合併規則

    private void startCombineWorker() {
        OneTimeWorkRequest requestA = new OneTimeWorkRequest.Builder(StreamCombineWorkerA.class).build();
        OneTimeWorkRequest requestB = new OneTimeWorkRequest.Builder(StreamCombineWorkerB.class).build();
        // 設定合併規則OverwritingInputMerger
        OneTimeWorkRequest requestC = new OneTimeWorkRequest.Builder(StreamCombineWorkerC.class).setInputMerger(
            OverwritingInputMerger.class).build();
        //A任務鏈
        WorkContinuation continuationA = WorkManager.getInstance().beginWith(requestA);
        //B任務鏈
        WorkContinuation continuationB = WorkManager.getInstance().beginWith(requestB);
        //合併上面兩個任務鏈,在接入requestE任務,入隊執行
        WorkContinuation continuation = WorkContinuation.combine(continuationA, continuationB).then(requestC);
        continuation.enqueue();
    }

2.4.4、唯一工作佇列

       我們上面例子中所有的鏈試任務的佇列都是通過WorkManager.getInstance().beginWith()來建立的。這種方式建立的鏈試任務沒啥限制條件,任務隨便怎麼入隊。要是某些場景我們需要同一個任務不能重複入隊怎麼辦。這個時候就需要唯一工作佇列了。

       WorkManager允許我們建立一個唯一的工作佇列。唯一工作佇列指的是這個佇列中任務不能重複入隊。WorkManager中通過beginUniqueWork()來建一個唯一佇列。每個唯一工作佇列建立的時候都必須指定一個佇列名字,同時還得指定ExistingWorkPolicy當WorkManager裡面已經有一個相同的唯一佇列時候的處理方式。ExistingWorkPolicy有三個值:REPLACE(取消現有的序列並將其替換為新序列)、KEEP(保持現有順序並忽略新請求)、APPEND(將新序列附加到現有序列,在現有序列的最後一個任務完成後執行新序列的第一個任務)。

       如果在唯一工作佇列中多次加入同一個任務,程式會異常退出。

    /**
     * A,B,C三個任務加入到唯一工作佇列中去
     */
    private void startWorker() {
        // A
        OneTimeWorkRequest requestA = new OneTimeWorkRequest.Builder(OrderWorkerA.class).build();
        // B
        OneTimeWorkRequest requestB = new OneTimeWorkRequest.Builder(OrderWorkerB.class).build();
        // C
        OneTimeWorkRequest requestC = new OneTimeWorkRequest.Builder(OrderWorkerC.class).build();
        // 任務入隊,WorkManager排程執行
        WorkManager.getInstance().beginUniqueWork("unique", ExistingWorkPolicy.KEEP, requestA)
                   .then(requestB)
                   .then(requestC)
                   .enqueue();
    }

       關於WorkManager的任務就講這麼寫,如果大家在使用過程中有什麼疑問,歡迎留言。最後給出本文涉及到的例項下載地址https://github.com/tuacy/WorkManagerDev

相關文章