WorkManager :创建&提交&输入输出&取消

1,221 阅读10分钟

WorkManager的使用

导入库

将以下依赖项添加到应用的 build.gradle 文件中:


dependencies {
    def work_version = "2.5.0" //目前最新的稳定版
    // (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"

    // optional - Multiprocess support
    implementation "androidx.work:work-multiprocess:$work_version"
}

定义工作

工作使用 Worker 类定义。doWork() 方法在 WorkManager 提供的后台线程上异步运行。

如需为 WorkManager 创建一些要运行的工作,请扩展 Worker 类并替换 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
     XXXXXXXXXX

     // Indicate whether the work finished successfully with the Result
     return Result.success();
   }
}

doWork() 返回的 Result 会通知 WorkManager 服务工作是否成功,以及工作失败时是否应重试工作。

  • Result.success():工作成功完成。
  • Result.failure():工作失败。
  • Result.retry():工作失败,应根据其重试政策在其他时间尝试。

定义WorkRequest

定义工作后,必须使用 WorkManager 服务进行调度该工作才能运行。对于如何调度工作,WorkManager 提供了很大的灵活性。您可以将其安排为在某段时间内定期运行,也可以将其安排为仅运行一次

不论您选择以何种方式调度工作,请始终使用 WorkRequestWorker 定义工作单元,WorkRequest(及其子类)则定义工作运行方式和时间。

WorkRequest 本身是抽象基类。该类有两个派生实现,可用于创建 OneTimeWorkRequestPeriodicWorkRequest请求。顾名思义,OneTimeWorkRequest 适用于调度非重复性工作,而 PeriodicWorkRequest 则更适合调度以一定间隔重复执行的工作。

调度一次性工作

对于无需额外配置的简单工作,请使用静态方法 from

WorkRequest uploadWorkRequest = OneTimeWorkRequest.from(UploadWorker.class);

对于更复杂的工作,可以使用构建器。

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

调度定期工作

您的应用有时可能需要定期运行某些工作。例如,您可能要定期备份数据、定期下载应用中的新鲜内容或者定期上传日志到服务器。

使用 PeriodicWorkRequest 创建定期执行的 WorkRequest 对象的方法如下:

PeriodicWorkRequest saveRequest =
       new PeriodicWorkRequest.Builder(SaveImageToFileWorker.class, 1, TimeUnit.HOURS) //工作的运行时间间隔定为一小时
           // Constraints
           .build();

※注意:可以定义的最短重复间隔是 15 分钟。

如果您的工作的性质致使其对运行时间敏感,您可以将 PeriodicWorkRequest 配置为在每个时间间隔的灵活时间段内运行,如下图所示。

definework-flex-period.png 如需定义具有灵活时间段的定期工作,请在创建 PeriodicWorkRequest 时传递 flexInterval 以及 repeatInterval。灵活时间段从 repeatInterval - flexInterval 开始,一直到间隔结束。

以下是可在每小时的最后 15 分钟内运行的定期工作的示例。

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

※注意:可以定义的最短重复间隔是 15 分钟,灵活时间段必须大于或等于5分钟。

definework-flex-period.png

WorkRequest 提交给系统

最后,您需要使用 enqueue() 方法将 WorkRequest 提交到 WorkManager

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

上面的代码,我们再work的doWork()方法种添加一个Log输入语句,简单验证下。

public class UploadWorker extends Worker {
    public Result doWork() {
        Log.d("test", "i is " + i);
        Log.d("test", "j is " + j);
        i++;
        j++;
        return Result.success();
    }
}

WorkRequest uploadWorkRequest = OneTimeWorkRequest.from(UploadWorker.class);
WorkManager.getInstance(getBaseContext())
                .enqueue(uploadWorkRequest);

当使用OneTimeWorkRequest的时候,无论等多久,Log的确只输出一次。

而当使用PeriodicWorkRequest,并且把重复执行时间设置为15分钟。

public class UploadWorker extends Worker {
    public Result doWork() {
        Log.d("test", "i is " + i);
        Log.d("test", "j is " + j);
        i++;
        j++;
        return Result.success();
    }
}

WorkRequest uploadWorkRequest2 = new PeriodicWorkRequest.Builder(UploadWorker.class,
                15, TimeUnit.MINUTES)
                .build();
WorkManager.getInstance(getBaseContext())
                .enqueue(uploadWorkRequest2);

执行Log如下:

studio64_2021-07-08_16-32-40.png 通过Log的输出时间,可以发现UploadWorker的确是在第一次执行完毕之后,间隔15分钟之后再次执行。

并且通过上面的Log,我们还发现UploadWorker在每次执行的时候都是重新构建了一个UploadWorker对象。所以PeriodicWorkRequest是把任务重复执行了一次,而不是接着上一次任务再次继续执行。

同时也测试了当把程序从后台Kill掉之后,等间隔15分钟后,Log还可以再次输出。

WorkRequest的工作状态

把WorkRequest提交给系统了,那么WorkRequest在系统中是什么样的工作状态呢?

一次性工作的状态

对于one-time工作请求,工作的初始状态为ENQUEUED。

在ENQUEUED状态下,你的工作会在满足工作约束和工作延迟的要求后立即执行。然后工作会转为RUNNING状态,再根据工作的结果转为SUCCEEDED、FAILED状态;或者,如果结果是retry,它可能会回到ENQUEUED状态。在这个过程中,你可以随时取消工作,取消后工作将进入CANCELLED状态。

one-time-work-flow.png

SUCCEEDED、FAILED和CANCELLED都表示此工作的终止状态,并且WorkInfo.State.isFinished()都返回true。

定期工作的状态

对于PeriodicWorkRequest来说,只有一个终止状态CANCELLED。SUCCEEDED和FAILED仅仅适用于one-time工作。这是因为定期工作除了取消工作之外,永远都不会结束。每次运行结束之后,无论结果如何,系统都会重新对其调度。

periodic-work-states.png

WorkManager的输入和输出

上面介绍的WorkManager是一个很简单的例子,只是在Worker中输出一句Log,跟Activity上没有任何的交互。但是在实际使用中,我们是非常需要跟Activity进行交互,从而告知用户,这边有个耗时任务在运行等等。

在输入和输出中,输入值以键值对的形式存储在 Data 对象中,并且可以在工作请求中设置。WorkManager 会在执行工作时将输入 Data传递给工作。Worker 类可通过调用 Worker.getInputData() 访问输入参数。

        String[] strings = {"AAA","BBB","CCC","DDD"};
        Data data = new Data.Builder().putStringArray("STRING_ARRAY_KEY", strings).build();
        WorkRequest uploadWorkRequest = new OneTimeWorkRequest.Builder(UploadWorker.class)
                .setInputData(data)
                .build();
​
        WorkManager.getInstance(getBaseContext()).enqueue(uploadWorkRequest);
​
        WorkManager.getInstance(getBaseContext()).getWorkInfoByIdLiveData(uploadWorkRequest.getId())
                .observe(this, info -> {
                    if (info != null && info.getState() == WorkInfo.State.SUCCEEDED) {
                        String[] myResult = info.getOutputData().getStringArray("STRING_ARRAY_KEY_OUT");
                        // ... do something with the result ...
                        String string = "";
                        Log.d("test","myResult is " + myResult.toString());
                        for (int i = 0; i < myResult.length; i++) {
                            string = string + myResult[i] + "\n";
                        }
                        textView.setText(string);
                    }
                });

上面的代码是Worker的请求方的代码,在该代码中我们创建了一个字符数组形式的Data,然后定义了一个OneTimeWorkRequest,并且通过setInputData方法把Data传递给工作。同时也定义了一个返回监听(getWorkInfoByIdLiveData),在监听中,通过判断WorkInfo(info)的State状态是否是Successded的状态再次通过WorkInfo从工作中回去返回值(getOutputData())。

public class UploadWorker extends Worker {
    public UploadWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
        Log.d("test", "new UploadWorker");
    }
​
    @NonNull
    @Override
    public Result doWork() {
        String[] strings = getInputData().getStringArray("STRING_ARRAY_KEY");
        Log.d("test","strings is " + strings.toString());
        String[] strings1 = new String[strings.length];
        for(int i = 0; i < strings.length; i++) {
            strings1[i] = strings[i] + i;
        }
        Log.d("test","strings1 is " + strings1.toString());
        Data outData = new Data.Builder().putStringArray("STRING_ARRAY_KEY_OUT", strings1).build();
        return Result.success(outData);
    }
}

在UploadWorker中,通过getInputData方法获取从请求方传递过来的Data。同时再新创建一个Data,用来把数据传递给请求方。请求方通过返回监听和getOutputData方法获取返回的数据。

上面的示例代码执行结果如下:

Screenshot_20210611-193729.jpg

上面的示例代码使用的是OneTimeWorkRequest,那么我们是否可以使用相同的方式使用PeriodicWorkRequest呢?

UploadWorker的代码不动,请求方修改代码如下:

        String[] strings = {"AAA","BBB","CCC","DDD"};
        Data data = new Data.Builder().putStringArray("STRING_ARRAY_KEY", strings).build();
        WorkRequest uploadWorkRequest2 = new PeriodicWorkRequest.Builder(UploadWorker.class,
                15, TimeUnit.MINUTES)
                .setInputData(data)
                .build();
        WorkManager.getInstance(getBaseContext()).enqueue(uploadWorkRequest2);
​
        WorkManager.getInstance(getBaseContext()).getWorkInfoByIdLiveData(uploadWorkRequest2.getId())
                .observe(this, info -> {
                    if (info != null && info.getState() == WorkInfo.State.SUCCEEDED) {
                        ......
                        Log.d("test","myResult is " + myResult.toString());

执行结果发现界面上没有任何的变化,并且上面代码中的Log结果也没有输出。这说明判断条件中判断state是否是succeed的条件不符合,查看对于WorkInfo.State.SUCCEEDED的说明,发现PeriodicWorkRequest从来不会返回SUCCEEDED状态。

        /**
         * Used to indicate that the {@link WorkRequest} has completed in a successful state.  Note
         * that {@link PeriodicWorkRequest}s will never enter this state (they will simply go back
         * to {@link #ENQUEUED} and be eligible to run again).
         */
        SUCCEEDED,

根据上面的注释,我们把判断条件改为if (info != null && info.getState() == WorkInfo.State.ENQUEUED),再次执行代码,发现程序报错了。

06-11 19:43:03.002 30439 30439 D AndroidRuntime: Shutting down VM
06-11 19:43:03.005 30439 30439 E AndroidRuntime: FATAL EXCEPTION: main
06-11 19:43:03.005 30439 30439 E AndroidRuntime: Process: com.example.workmanagersimple, PID: 30439
06-11 19:43:03.005 30439 30439 E AndroidRuntime: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String java.lang.Object.toString()' on a null object reference
06-11 19:43:03.005 30439 30439 E AndroidRuntime:        at com.example.workmanagersimple.MainActivity.lambda$onCreate$0(MainActivity.java:44)
06-11 19:43:03.005 30439 30439 E AndroidRuntime:        at com.example.workmanagersimple.-$$Lambda$MainActivity$w1mLUdtnmzEu1slhnhK3ua3n61o.onChanged(Unknown Source:4)

通过上面的Error信息,可以知道两点:

1.判断条件有效,程序当前的状态就是ENQUEUED;

2.出错的语句是那句Log中的myResult为null。

为啥myResult会为null呢?明明返回值已经通过Result.success(outData)传递出来了。查看WorkInfo的说明。

Information about a particular WorkRequest containing the id of the WorkRequest, its current WorkInfo.State, output, tags, and run attempt count. Note that output is only available for the terminal states (WorkInfo.State.SUCCEEDED and WorkInfo.State.FAILED).

WorkInfo中明确说了output只有在WorkInfo.State.SUCCEEDED 和WorkInfo.State.FAILED状态才能传递,而PeriodicWorkRequest当前的状态是WorkInfo.State.ENQUEUED,所以并不能通过WorkInfo获取output。

那么PeriodicWorkRequest到底能不能获取到返回值,我查看网上说明。有的文章中说用如下的方法。

        WorkManager.getInstance().getStatusById(request.getId()).observe(this, new Observer<WorkStatus>() {
            @Override
            public void onChanged(@Nullable WorkStatus workStatus) {

可但是在WorkManager的函数中并不能发现getStatusById方法,也没有WorkStatus这个类。同时在AndroidStudio中把work-runtime的版本设置到可以设置的最低2.0.0,也没有WorkStatus类和getStatusById方法。

在这边我想虽然PeriodicWorkRequest不能直接获取到返回值,但是我们是不是可以通过间接的方法呢?比如在Worker的doWork方法中,把执行的结果写进一个文件或者数据库中,然后在请求方判断状态;当状态符合条件的时候,去文件或者数据库中读取数据。这种方式也是可以获取数据的。

WorkManager的取消

在上面的工作状态中,我们知道无论是一次工作还是定期工作都存在一个CANCELLED的状态。那么就是说CANCELLED是WorkManager中非常重要的。

在WorkManager中存在如下的四个cancel方法:cancelAllWork、cancelAllWorkByTag、cancelWorkById 和 cancelUniqueWork。

cancelAllWork()

该函数将cancel所有的没有finish的work

        Data data1 = new Data.Builder().putString("key", "1").build();
        WorkRequest uploadWorkRequest1 =
                new OneTimeWorkRequest.Builder(UploadWorker.class)
                        .setInitialDelay(10, TimeUnit.MINUTES)
                        .setInputData(data1)
                        // Additional configuration
                        .build();
​
        Data data2 = new Data.Builder().putString("key", "2").build();
        WorkRequest uploadWorkRequest2 =
                new OneTimeWorkRequest.Builder(UploadWorker.class)
                        .setInitialDelay(10, TimeUnit.MINUTES)
                        .setInputData(data2)
                        // Additional configuration
                        .build();
​
        WorkManager workManager = WorkManager.getInstance(MainActivity.this);
​
        workManager.enqueue(uploadWorkRequest1);
        workManager.getWorkInfoByIdLiveData(uploadWorkRequest1.getId()).observe(this, info -> {
            if (info != null && info.getState() == WorkInfo.State.CANCELLED) {
                Log.d(TAG, "WorkRequest 1 is cancelled");
            }
        });
​
        workManager.enqueue(uploadWorkRequest2);
        workManager.getWorkInfoByIdLiveData(uploadWorkRequest2.getId()).observe(this, info -> {
            if (info != null && info.getState() == WorkInfo.State.CANCELLED) {
                Log.d(TAG, "WorkRequest 2 is cancelled");
            }
        });
​
        Log.d(TAG, "WorkRequest 1 & 2 have submit to System");
​
        Handler handler = new Handler();
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                workManager.cancelAllWork();
            }
        }, 5000);

如上面的代码,我们定义了两个WorkRequest,并且设置一个延时,这样方便我们cancel这两个work,毕竟只能cancel未finish的work。然后我们再定义一个Handler,延时5秒执行cancel操作。打印出来的log如下:

2021-01-06 20:41:01.553 15367-15367/com.example.myapplication D/MainActivity: WorkRequest 1 & 2 have submit to System
2021-01-06 20:41:06.665 15367-15367/com.example.myapplication D/MainActivity: WorkRequest 1 is cancelled
2021-01-06 20:41:06.667 15367-15367/com.example.myapplication D/MainActivity: WorkRequest 2 is cancelled

两个work的确都被cancel了。

cancelAllWorkByTag(@NonNull String tag)

该函数将cancel所有的没有finish的并且tag符合给定的work

WorkRequest uploadWorkRequest1 =
        ......
        .addTag("work")
WorkRequest uploadWorkRequest2 =
        ......
        .addTag("work")
WorkRequest uploadWorkRequest3 =
        ......
        .addTag("work_work")
    
handler.postDelayed(new Runnable() {
    @Override
    public void run() {
        workManager.cancelAllWorkByTag("work");
    }
}, 5000);

我们改造下代码,给一开始的两个WorkRequest添加同一个Tag(work),同时添加一个新的WorkRequest3,添加另一个Tag(work_work),然后在handler中调用cancelAllWorkByTag,cancel掉Tag为work的工作。打印的Log如下:

2021-01-06 21:02:42.256 15888-15888/com.example.myapplication D/MainActivity: WorkRequest 1 & 2 & 3 have submit to System
2021-01-06 21:02:47.317 15888-15888/com.example.myapplication D/MainActivity: WorkRequest 1 is cancelled
2021-01-06 21:02:47.319 15888-15888/com.example.myapplication D/MainActivity: WorkRequest 2 is cancelled
2021-01-06 21:02:52.372 15888-15969/com.example.myapplication D/MainActivity: UploadWorker doWork and key is 3

Tag为work的WorkRequest1 & 2两者被cancel了,但是Tag为work_work的WorkRequest3正常执行完毕。

cancelWorkById(@NonNull UUID id)

该函数将cancel没有finish的并且id符合给定的work

        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                workManager.cancelWorkById(uploadWorkRequest3.getId());
            }
        }, 5000);

这次我们在Handler中调用cancelWorkById函数,并且给定是WorkRequest3的id。打印Log如下:

2021-01-06 21:18:04.358 16367-16367/com.example.myapplication D/MainActivity: WorkRequest 1 & 2 & 3 have submit to System
2021-01-06 21:18:09.451 16367-16367/com.example.myapplication D/MainActivity: WorkRequest 3 is cancelled
2021-01-06 21:18:14.415 16367-16421/com.example.myapplication D/MainActivity: UploadWorker doWork and key is 1
2021-01-06 21:18:14.426 16367-16422/com.example.myapplication D/MainActivity: UploadWorker doWork and key is 2

发现只有WorkRequest3被cancel了,WorkRequest1 & 2 正常执行完毕。

※注:由于每个WorkRequest在定义的时候,对应的id就给定了,所以上面的cancelWorkById函数并不能cancel多个work。

cancelUniqueWork(@NonNull String uniqueWorkName)

该函数将cancel没有finish的并且name符合给定的work

workManager.enqueueUniqueWork("work&work", ExistingWorkPolicy.KEEP, (OneTimeWorkRequest) uploadWorkRequest3);
​
handler.postDelayed(new Runnable() {
    @Override
    public void run() {
        workManager.cancelUniqueWork("work&work");
    }
}, 5000);

使用这个方法,我们需要改造提交work的地方,在前面的方法中,我们提交work使用的都是enquen()方法,而在这边我们需要使用enqueueUniqueWork()方法,该方法的第一个参数就是我们cancelUniqueWork方法需要的name。

参照文献: developer.android.google.cn/jetpack/and… developer.android.google.cn/topic/libra…