WorkManager完全解析+重構輪詢系統

Hanking發表於2019-01-20

花兩個週末寫完的,創作不容易啊,轉載請標明出處哈:

csdn:blog.csdn.net/u013309870/… 掘金:juejin.im/post/5c4472…

前言

之前用IntentService寫了一個輪詢框架,但是並不是很好,後面一直想找個其他方式來改寫一下,找了好多資料發現了WorkManager,WorkManager是google提供的一個非常優秀的後臺任務管理框架,對於提交給WorkManager的任務可以立即執行也可以在適當的時候執行,可以執行一次也可以根據條件迴圈執行多次,並且對於多個任務WorkManager可以管理任務的執行路徑先後執行順序等。本文先介紹WorkManager的用法,然後使用WorkManager來重構之前的輪詢框架。

什麼是Workmanager

WorkManager是google提供的非同步執行任務的管理框架,會根據手機的API版本和應用程式的狀態來選擇適當的方式執行任務。當應用在執行的時候會在應用的程式中開一條執行緒來執行任務,當退出應用時,WorkManager會選擇根據裝置的API版本使用適合的演算法呼叫JobScheduler或者Firebase JobDispatcher,或者AlarmManager來執行任務。如下圖:

在這裡插入圖片描述
由上圖可以看出WorkManager管理任務執行時底層還是呼叫了JobScheduler,JobDispatcher,AlarmManager,不過WorkManager會根據Android系統的API和應用的執行狀態來選擇合適的方式執行,並不用我們自己去考慮應用複雜的執行狀態來進行選擇使用JobScheduler還是JobDispatcher或者AlarmManager呼叫規則如下圖:
在這裡插入圖片描述

WorkManager在專案中配置

使用WorkManager需要gradle依賴,進行一下簡單配置就可以使用了。找到專案中的app/build.gradle目錄下在上面加上下面的依賴。

dependencies {
    // 其他依賴配置
    def work_version = "1.0.0-beta02"
    implementation "android.arch.work:work-runtime:$work_version"
}
複製程式碼

可以在developer.android.com/topic/libra…獲取當前的work-runtime版本並且設定正確的版本號。

WorkManager主要類及使用

如下圖給出了WorkManager中主要的類以及關係圖,黃色區域是最主要的三個類,構成了WorkManager的基本框架,紅色部分和綠色部分是關聯的黃色部分的具體實現或者類裡面包含一些規則或資料。

在這裡插入圖片描述
1、Worker處理要執行的任務的具體邏輯。

2、WorkerRequest代表一個獨立的可以執行的任務,以及任務執行時的條件和規則,比如說任務執行一次還是多次以及任務的觸發條件是什麼任務有什麼約束等。

3、WorkManager提供佇列將要執行的WorkerRequest放到佇列中管理和執行。 如下圖,三個主要類的關係:

在這裡插入圖片描述
下面分別介紹三個類的作用和使用方法。

Worker

Worker是一個抽象類,當有一個要執行的任務的時候可以繼承Worker類,重寫doWork()方法在doWork()方法中實現具體任務的邏輯。

public class MyWorker extends Worker {
    public MyWorker(
            @NonNull Context appContext,
            @NonNull WorkerParameters workerParams) {
        super(appContext, workerParams);
    }
    @NonNull
    @Override
    public Worker.Result doWork() {

        Context applicationContext = getApplicationContext();

        try {

            Bitmap picture = BitmapFactory.decodeResource(
                    applicationContext.getResources(),
                    R.drawable.test);
                    
            return Worker.Result.SUCCESS;
            
        } catch (Throwable throwable) {
        
            return Worker.Result.FAILURE;
            
        }
    }
}
複製程式碼

在上面的MyWorker例項中,繼承了Worker 並且重寫了doWork()方法,需要注意的是doWork()方法是有返回值Worker.Result的,可以在任務執行成功是返回Worker.Result.SUCCESS,在任務執行出現異常時返回Worker.Result.FAILURE doWork()方法的返回值主要有三種 1、Worker.Result.SUCCESS 表示任務執行成功

2、Worker.Result.FAILURE 表示任務執行失敗

3、Worker.Result.RETRY 通知WorkManager之後再嘗試執行該任務

WorkRequest

WorkRequest要指定執行任務的Worker,也可以給WorkRequest加一些規則,比如說什麼時候執行任務,任務執行一次還是多次,每一個WorkRequest都有一個自動產生的唯一ID,可以根據唯一ID獲取對應任務的狀態以及是否取消對應的任務。如下圖WorkRequest有兩個實現類如下圖:

在這裡插入圖片描述
1、OneTimeWorkRequest 任務只執行一次

OneTimeWorkRequest myWorkRequest =
        new OneTimeWorkRequest.Builder(MyWorker.class)
    .build();
    //將上面定義的MyWorker加入到OneTimeRequest.Builder方法中
WorkManager.getInstance().enqueue(myWorkRequest);//獲取WorkManager例項並將WorkRequest進隊
複製程式碼

2、PeriodicWorkRequest PeriodicWorkRequest重複執行任務,直到被取消才停止。首次執行是任務提交後立即執行或者滿足所給的 Constraints條件。以後執行都會根據所給的時間間隔來執行。注意任務的執行可能會有延時,因為WorkManager會根據OS的電量進行優化。 假如設定的Periodic Work是24小時執行一次,有可能根據電池優化策略執行的過程如下:

     1     | Jan 01, 06:00 AM
     2     | Jan 02, 06:24 AM
     3     | Jan 03, 07:15 AM
     4     | Jan 04, 08:00 AM
     5     | Jan 05, 08:00 AM
     6     | Jan 06, 08:02 AM
複製程式碼

由上面的執行時間可以看出,PeriodicWorkRequest並不是準確的按照24小時來執行,會有一定的時間延遲。因此如果需要準確的間隔時間來執行任務的話不能使用PeriodicWorkRequest。

Constraints constraints = new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build();
PeriodicWorkRequest build = new PeriodicWorkRequest.Builder(MyWorker.class, 25, TimeUnit.MILLISECONDS)
           .addTag(TAG)
           .setConstraints(constraints)
           .build();

WorkManager instance = WorkManager.getInstance();
if (instance != null) {
          instance.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, build);
}
複製程式碼

Constraints

可以給任務加一些執行的Constraints條件,比如說當裝置空閒時或者正在充電或者連線WiFi時執行任務。

Constraints myConstraints = new Constraints.Builder()
    .setRequiresDeviceIdle(true)
    .setRequiresCharging(true)
    // Many other constraints are available, see the
    // Constraints.Builder reference
     .build();
OneTimeWorkRequest myWork =
                new OneTimeWorkRequest.Builder(CompressWorker.class)
     .setConstraints(myConstraints)
     .build();
複製程式碼

WorkManager

WorkManager管理WorkRequest佇列。並根據裝置和其他條件選擇執行的具體方式。在大部分情況在如果沒有給佇列加Contraints,WorkManager會立即執行任務。

WorkManager.getInstance().enqueue(myWork);
複製程式碼

如果要檢查任務的執行狀態可以通過獲取WorkInfo,WorkInfo在WorkManager裡面的LiveData<WorkInfo>中。下面是判斷任務是否結束的方式。

WorkManager.getInstance().getWorkInfoByIdLiveData(myWork.getId())
    .observe(lifecycleOwner, workInfo -> {
        // Do something with the status
        if (workInfo != null && workInfo.getState().isFinished()) {
            // ...
        }
    });
複製程式碼

取消任務執行

通過任務的ID可以獲取任務從而取消任務。任務ID可以從WorkRequest中獲取。

UUID compressionWorkId = compressionWork.getId();
WorkManager.getInstance().cancelWorkById(compressionWorkId);
複製程式碼

注意並不是所有的任務都可以取消,當任務正在執行時是不能取消的,當然任務執行完成了,取消也是意義的,也就是說當任務加入到ManagerWork的佇列中但是還沒有執行時才可以取消。

WorkManager多工排程

有時候可能有很多工需要執行,並且這些任務之前可能有先後順序或者某些依賴關係,WorkManager提供了很好的方式。 1、先後順序執行單個任務 比如說有三個任務workA,workB,workC,並且執行順序只能時workA---->workB---->workC可以用如下的方式處理。

WorkManager.getInstance()
    .beginWith(workA)
    .then(workB)  instance
    .then(workC)
    .enqueue();
複製程式碼

上面的workA,workB,workC,都是WorkRequest的子類實現物件。WorkManager會根據上面的先後順序來執行workA,workB,workC,,但是如果執行過程中三個任務中有一個失敗,整個執行都會結束。並且返回Result.failure()2、先後順序執行多個任務列 有時候可能要先執行一組任務,然後再執行下一組任務,可以使用下面的方式來完成。

WorkManager.getInstance()
    // First, run all the A tasks (in parallel):
    .beginWith(Arrays.asList(workA1, workA2, workA3))
    // ...when all A tasks are finished, run the single B task:
    .then(workB)
    // ...then run the C tasks (in any order):
    .then(Arrays.asList(workC1, workC2))
    .enqueue();
複製程式碼

3、多路徑先後執行 上面兩種方式都是單路徑執行,可以實現更加複雜的多路徑執行方式,使用WorkContinuation.combine(List<OneTimeWorkRequest>)如下圖要實現的執行方式:

在這裡插入圖片描述

WorkContinuation chain1 = WorkManager.getInstance()
    .beginWith(workA)
    .then(workB);
WorkContinuation chain2 = WorkManager.getInstance()
    .beginWith(workC)
    .then(workD);
WorkContinuation chain3 = WorkContinuation
    .combine(Arrays.asList(chain1, chain2))
    .then(workE);
chain3.enqueue();
複製程式碼

使用WorkManager遇到的問題

1、使用PeriodicWorkRequest只執行一次,並不重複執行。

     WorkManager instance= new PeriodicWorkRequest.Builder(PollingWorker.class, 10, TimeUnit.MINUTES)
                .build();
複製程式碼

原因:PeriodicWorkRequest預設的時間間隔是15分鐘如果設定的時間小於15分鐘,就會出現問題。

解決方法:設定的預設時間必須大於或等於15分鐘。另外要注意,就算設定的時間為15分鐘也不一定間隔15分鐘就執行。所以要精確的間隔時間執行,一般不用WorkManager,可以使用AlarmManager來實現。

2、在doWork()方法中更新UI導致崩潰。 原因:doWork()方法是在WorkManager管理的後臺執行緒中執行的,更新UI操作只能在主執行緒中進行。

解決方法:當doWork()耗時方法執行完之後,將更新UI操作拋到主線中執行,可以用handle來實現,如下:

/**
 * Created time 15:32.
 *
 * @author huhanjun
 * @since 2019/1/23
 */
public class PollingWorker extends Worker {
    public static final String TAG = "PollingWorker";

    @NonNull
    @Override
    public Result doWork() {
        Log.d(TAG, "doWork");
        try {
            polling();
            runOnUIThread(new Runnable() {
                @Override
                public void run() {
                    //更新UI操作
                }
            });
            return Result.SUCCESS;
        } catch (Exception e) {
            Log.d(TAG, "failure");
            return Result.FAILURE;
        }
    }

    private void polling() {
        Log.d(TAG, "Polling");
    }
//拋到主執行緒中執行
    private void runOnUIThread(Runnable runnable) {
        new Handler(Looper.getMainLooper()).post(runnable);
    }
}
複製程式碼

在執行完Polling方法後,將更新操作拋給了runOnUIThread()方法,這樣就可以在上面程式碼註釋的地方執行更新操作。 未完待續........下週末來重構之前的輪詢框架:juejin.im/post/5c2e0e…

參考文獻

1、developer.android.com/topic/libra…

2、codelabs.developers.google.com/codelabs/an…

我的csdn地址

blog.csdn.net/u013309870

相關文章