Android Jetpack之WorkManager保活福音

作者: Android架构 | 来源:发表于2019-01-24 20:34 被阅读18次

    WorkManager 初探

    WorkManager API可以很容易的指定一个可延迟的异步任务何时运行。这些api允许你创建一个任务并将其交给WorkManager以立即或在适当的时间运行。比如说,一个APP可能需要时不时的从服务器下载新的资源,使用这些类,我们可以创建一个任务,为它选择合适的运行环境(比如“只有当设备在充电和在线的时候”),然后交给WorkManager使其能在满足条件时运行。即使APP强制退出或设备重新启动,任务仍然保证能够运行。

    WorkManager用于那些需要保证即使APP退出了系统依然可以运行的任务,比如将应用数据上传到服务器。不要用于如果APP被杀进程,可以安全终止的后台任务;对于这种情况,建议使用ThreadPools。

    WorkManager会根据设备的系统版本和APP的状态等因素选择适当的方式来运行任务。如果APP正在运行,WorkManager会在APP进程中起一个新线程来运行任务;如果APP没有运行,WorkManager会选择一个合适的方式来调度后台任务--根据系统级别和APP状态,WorkManager可能会使用JobScheduler,FireBase JobDispatcher或者AlarmManager。我们不需要编写逻辑代码来确定设备具备什么功能选择什么样的API,WorkManager会自动选择最佳方案。

    此外,WorkManager还提供了几个高级特性。例如,你可以设置一个任务链,当一个任务结束之后,WorkManager会自动执行下一个在链中排队的任务。我们还可以通过观察任务的LiveData来获取它的状态和返回值,从而可以设置一个显示任务状态的UI。

    本文概述了最重要的WorkManager特性。当然,还有好多可用的特性,若想了解全部的细节,可以查看WorkManager reference documentation

    关于如何将WorkManager库导入到Android项目中,可以查看 Adding Components to your Project这篇文章

    类和概念

    WorkManager API使用几个不同的类。在某些情况下,您需要对其中一个API类进行子类化。
    比较重要的类有:

    • Worker
      指定需要执行的任务。WorkManager api包含一个抽象的Worker类。我们需要继承并实现这个类
    • WorkRequest
      表示一个独立的任务。一个WorkRequest对象需要至少指定一个执行该任务的Worker类。当然我们也可以添加更多的细节,比如指定任务应该运行的环境等。每一个WorkRequest都有一个自动生成唯一ID,我们可以使用这个ID来执行诸如取消排队任务或者获取任务状态等操作。WorkRequest是一个抽象类,我们可以使用系统提供的子类-OneTimeWorkRequestPeriodicWorkRequest
      • WorkRequest.Builder:
        创建WorkRequest对象的帮助类。同样,我们也需要用系统提供的子类:OneTimeWorkRequest.Builder 或者 PeriodicWorkRequest.Builder。
      • Constraints
        指定任务运行的限制条件(例如,“仅当连接到网络时”)。使用Constraint.Builder来创建Constraints,并在创建WorkRequest之前把Constraints传给WorkRequest.Builder。
    • WorkManager
      对工作请求进行管理。我们需要把WorkRequest对象传给WorkManager以便将任务编入队列。WorkManager以这样的方式调度任务,以分散系统资源的负载,同时满足我们指定的约束条件。
    • WorkStatus
      包含特定任务的信息。WorkManager为每个WorkRequest对象提供一个LiveData。LiveData持有一个WorkStatus对象;通过观察这个LiveData,我们可以确定任务的当前状态,并在任务完成后获得返回值。

    典型的工作流程

    假设我们正在编写一个图片库的APP,该APP需要定期压缩存储的图像。我们使用WorkManager来调度图像压缩的任务。在这种情况下,我们并不关心压缩任务发生的时间,我们只需要设置一个任务,然后其他都不关心了。
    首先,需要定义一个Worker类并重写doWork()方法。worker类指定了如何执行操作,但是没有任何关于任务应该何时运行的信息。

    public class CompressWorker extends Worker {
    @Override
    public Worker.WorkerResult doWork() {
    
        // Do the work here--in this case, compress the stored images.
        // In this example no parameters are passed; the task is
        // assumed to be "compress the whole library."
        myCompress();
    
        // Indicate success or failure with your return value:
        return WorkerResult.SUCCESS;
    
        // (Returning RETRY tells WorkManager to try this task again
        // later; FAILURE says not to try again.)
      }
    }
    

    接下来,基于Worker创建一个OneTimeWorkRequest对象,然后使用WorkManager将任务放入队列中:

    OneTimeWorkRequest compressionWork =
        new OneTimeWorkRequest.Builder(CompressWorker.class)
    .build();
    WorkManager.getInstance().enqueue(compressionWork);
    

    WorkManager选择适当的时间来运行任务,平衡诸如系统上的负载、设备是否正在充电等方面的考虑。在大多数情况下,如果不指定任何约束,WorkManager会立即运行任务。如果您需要获取任务状态,您可以通过获取适当的LiveData<WorkStatus>来获得一个WorkStatus对象。例如,如果您想检查任务是否完成,可以使用如下代码:

    WorkManager.getInstance().getStatusById(compressionWork.getId())
    .observe(lifecycleOwner, workStatus -> {
        // Do something with the status
        if (workStatus != null && workStatus.getState().isFinished()) {
            // ...
        }
    });
    

    任务约束

    我们可以通过约束条件来指定任务何时运行。例如,我们可能希望指定该任务只应在设备空闲并连接电源时运行。在这种情况下,我们需要去创建OneTimeWorkRequest.Builder对象,然后使用这个Builder去创建OneTimeWorkRequest:

    // Create a Constraints that defines when the task should run
    Constraints myConstraints = new Constraints.Builder()
    .setRequiresDeviceIdle(true)
    .setRequiresCharging(true)
    // Many other constraints are available, see the
    // Constraints.Builder reference
     .build();
    
    // ...then create a OneTimeWorkRequest that uses those constraints
    OneTimeWorkRequest compressionWork =
                new OneTimeWorkRequest.Builder(CompressWorker.class)
     .setConstraints(myConstraints)
     .build();
    

    然后把这个OneTimeWorkRequest对象传给WorkManager.enqueue(),WorkManager在找到运行任务的时间时会考虑这个约束的。

    取消任务

    UUID compressionWorkId = compressionWork.getId();
    WorkManager.getInstance().cancelWorkById(compressionWorkId);
    

    WorkManager会尽最大努力的取消任务,但这并不靠谱——当我们试图取消任务时,任务可能已经在运行中或者已经完成了。WorkManager还提供了方法来取消唯一工作序列中的所有任务,或使用指定标记的所有任务,当然同样不靠谱。

    高级特性

    WorkManager API的核心功能能够创建简单的、即发即忘的任务。除此之外,API还提供了一些高级特性,来设置更详细的请求。

    循环任务

    我们都会碰到需要重复执行的任务。比如,一个照片管理应用不会只压缩一次图片,更有可能的是,它会时不时的检查一下共享的照片,看看是否有新的或者修改过的图片需要压缩。我们可以选择一个重复执行的任务来压缩图片,当然,也可以启动一个新任务。

    new PeriodicWorkRequest.Builder photoCheckBuilder =
        new PeriodicWorkRequest.Builder(PhotoCheckWorker.class, 12,
                                        TimeUnit.HOURS);
    // ...if you want, you can apply constraints to the builder here...
    
    // Create the actual work object:
    PeriodicWorkRequest photoCheckWork = photoCheckBuilder.build();
    // Then enqueue the recurring task:
    WorkManager.getInstance().enqueue(photoCheckWork);
    

    WorkManager会试图在请求的时间间隔运行该任务,这取决于我们强加的约束和它的其他需求了。

    任务链

    应用程序可能需要按特定的顺序运行多个任务。WorkManager支持创建一个工作序列,该序列指定多个任务以及它们应该运行的顺序。

    WorkManager.getInstance()
    .beginWith(workA)
        // Note: WorkManager.beginWith() returns a
        // WorkContinuation object; the following calls are
        // to WorkContinuation methods
    .then(workB)    // FYI, then() returns a new WorkContinuation instance
    .then(workC)
    .enqueue();
    

    WorkManager根据每个任务指定的约束,按所请求的顺序执行任务。如果有任意任务返回“Worker.WorkerResult.FAILURE”,则整个工作序列都将结束。

    我们还可以将多个OneTimeWorkRequest对象传递给beginWith()和then()调用。如果我们将多个OneTimeWorkRequest对象传递给单个方法调用,那么WorkManager在运行其余的序列之前就会运行所有这些任务(并行)。比如:

    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(workB)
    // ...then run the C tasks (in any order):
    .then(workC1, workC2)
    .enqueue();
    

    通过将多个链与WorkContinuation.combine()方法连接起来,可以创建更复杂的序列。
    打个比方,假设我们现在想运行这样一个工作序列

    WorkContinuation chain1 = WorkManager.getInstance()
      .beginWith(workA)
      .then(workB);
    WorkContinuation chain2 = WorkManager.getInstance()
      .beginWith(workC)
      .then(workD);
    WorkContinuation chain3 = WorkContinuation
      .combine(chain1, chain2)
      .then(workE);
    chain3.enqueue();
    

    在这种情况下,WorkManager在workB之前运行workA。它在workD之前运行workc。在WordB和workD结束后,WorkManager运行workE。

    注意看黑板!
    虽然WorkManager按顺序运行每个子链,但是chain1中的任务与chain2中的任务顺序是不相关的。
    例如,workB可能在workC之前或之后运行,或者它们可能同时运行。
    唯一可以保证的是,每个子链中的任务将按顺序运行;也就是说,workB会等到workA完成后才开始。

    WorkContinuation还有许多变体,为一些特定情况提供了现成的方法,具体可以参考WorkContinuation

    唯一的工作序列

    用beginUniqueWork()代替beginWith()就可以创建一个唯一的工作序列。每一个唯一工作序列都有一个名字;WorkManager每次只允许有一个使用该名称的工作序列。当我们创建一个新的惟一的工作序列时,如果已经有一个同名的未完成序列,可以指定WorkManager应该做什么:

    • 取消原有的序列并用新的来代替它
    • 保留原有的序列并忽略新的请求
    • 把新的工作序列拼到原有序列的后边,当原有的序列的最后一个任务执行完之后,接着执行新的序列的第一个任务。

    如果有一个不应该多次排队的任务,那么唯一的工作序列就很有用了。例如,如果应用程序需要将其数据同步到网络中,我们可以将一个名为“sync”的序列编入队列,并指定如果已经有一个具有该名称的序列,则应该忽略新任务。如果需要逐步构建一个长长的任务链,那么唯一的工作序列也很有用。例如,一个照片编辑应用程序可以让用户撤消一系列的操作。每个撤销操作都可能需要一段时间,但是它们必须按照正确的顺序执行。在这种情况下,应用程序可以创建一个“撤消”链,并根据需要将每个“撤消”操作附加到链上。

    任务标签

    可以通过为WorkRequest对象分配一个标签来对任务进行分组。

    OneTimeWorkRequest cacheCleanupTask =
        new OneTimeWorkRequest.Builder(MyCacheCleanupWorker.class)
    .setConstraints(myConstraints)
    .addTag("cleanup")
    .build();
    

    WorkManager类提供了一些实用方法,可以使用特定的标记对所有任务进行操作。例如,WorkManager.cancelAllWorkByTag()用一个特定的标记取消所有任务,而WorkManager.getStatusesByTag()返回一个列表,其中列出了所有带有该标记的任务的所有工作状态。

    入参和返回值

    为了更好的灵活性,我们还可以向任务传递参数并让任务返回结果,传递的值和返回的值是键值对形式。要将一个参数传递给一个任务,需要在创建WorkRequest对象之前调用WorkRequest.Builder.setInputData()方法,该方法接收一个Data.Builder创建的Data对象。Worker类可以通过调用Worker.getInputData()访问这些参数。任务需要调用Worker.setOutputData()来输出返回值,同样接收一个Data对象,我们可以通过观察任务的LiveData<WorkStatus>来获得返回值。

    假设我们有一个执行耗时操作的Worker:

     // Define the Worker class:
    public class MathWorker extends Worker {
    
      // Define the parameter keys:
      public static final String KEY_X_ARG = "X";
      public static final String KEY_Y_ARG = "Y";
      public static final String KEY_Z_ARG = "Z";
      // ...and the result key:
      public static final String KEY_RESULT = "result";
    
      @Override
      public Worker.WorkerResult doWork() {
    
          // Fetch the arguments (and specify default values):
          int x = getInputData().getInt(KEY_X_ARG, 0);
          int y = getInputData().getInt(KEY_Y_ARG, 0);
          int z = getInputData().getInt(KEY_Z_ARG, 0);
    
          // ...do the math...
          int result = myCrazyMathFunction(x, y, z);
    
          //...set the output, and we're done!
          Data output = new Data.Builder()
              .putInt(KEY_RESULT, result)
              .build();
          setOutputData(output);
          return WorkerResult.SUCCESS;
      }
    }
    

    创建一个任务并传递参数:

    // Create the Data object:
    Data myData = new Data.Builder()
        // We need to pass three integers: X, Y, and Z
        .putInt(KEY_X_ARG, 42)
        .putInt(KEY_Y_ARG, 421)
        .putInt(KEY_Z_ARG, 8675309)
        // ... and build the actual Data object:
        .build();
    
    // ...then create and enqueue a OneTimeWorkRequest that uses those arguments
    OneTimeWorkRequest mathWork = new OneTimeWorkRequest.Builder(MathWorker.class)
            .setInputData(myData)
            .build();
    WorkManager.getInstance().enqueue(mathWork);
    

    通过WorkStatus获取返回值:

    WorkManager.getInstance().getStatusById(mathWork.getId())
    .observe(lifecycleOwner, status -> {
         if (status != null && status.getState().isFinished()) {
           int myResult = status.getOutputData().getInt(KEY_RESULT,
                  myDefaultValue));
           // ... do something with the result ...
         }
    });
    

    如果是一个任务链,那么一个任务的返回值可以作为下一个任务的参数。如果是一个简单的任务链,一个OneTimeWorkRequest后面跟着另一个OneTimeWorkRequest,第一个任务通过调用setOutputData()返回结果,下一个任务通过调用getInputData()来获取结果。如果是一个更复杂的任务链,比如说,有几个任务都将返回值传递给下一个任务,我们可以通过OneTimeWorkRequest.Builder上定义一个InputMerger,指定如果不同的任务返回一个具有相同键的输出时应该怎么做。

    【附录】

    资料图

    需要资料的朋友可以加入Android架构交流QQ群聊:513088520

    点击链接加入群聊【Android移动架构总群】:加入群聊

    获取免费学习视频,学习大纲另外还有像高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)等Android高阶开发资料免费分享。

    相关文章

      网友评论

        本文标题:Android Jetpack之WorkManager保活福音

        本文链接:https://www.haomeiwen.com/subject/rnwujqtx.html