Android 架构组件之 WorkManager 的使用指南及项目实践

文章目录

  • 使用场景
  • 定义
  • 选择 WorkManager 的理由 Why?
  • 多线程任务如何选择
  • 推荐的解决方案:
    • 即时任务
    • 延期任务
    • 精确任务
  • 走进源码
    • Worker
    • WorkRequest
    • WorkManager
    • Data
    • WorkStatus
  • Demo 快速使用
  • 项目实践

首先放一个WorkManger介绍视频:中文官方介绍视频(主要是小姐姐好看~)

使用场景

WorkManager 适用于那些在应用退出之后任务还需要继续执行的需求(比如应用数据上报服务器的情况),对应那些在应用退出的之后任务也需要终止的情况就需要选择ThreadPool、AsyncTask来实现。

定义

官方介绍:

WorkManger是一个可兼容、灵活和简单的延迟后台任务。

到底是什么?

WorkManager 是一个 API,使您可以轻松调度那些即使在退出应用或重启设备时仍应运行的可延期异步任务。

简单点说:

WorkManager 是一个 异步任务封装的功能库。

选择 WorkManager 的理由 Why?

Android 中处理后台任务的选择挺多的,比如ServiceDownloadManagerAlarmManagerJobScheduler等,那么选择WorkManager的理由是什么呢?

  • 版本兼容性强,向后兼容至 API 14。

  • 可以指定约束条件,比如可以选择必须在有网络的条件下执行。

  • 可定时执行也可单次执行。

  • 监听和管理任务状态。

  • 多个任务可使用任务链。

  • 保证任务执行,如当前执行条件不满足或者 App 进程被杀死,它会等到下次条件满足或者 App 进程打开后执行。

  • 支持省电模式。

多线程任务如何选择

后台任务会消耗设备的系统资源,如若处理不当,可能会造成设备电量的急剧消耗,给用户带来糟糕的体验。所以,选择正确的后台处理方式是每个开发者应当注意的,如下是官方给的选择方式:
Android 架构组件之 WorkManager 的使用指南及项目实践_第1张图片

此决策树可帮助您确定哪个类别最适合您的后台任务。

推荐的解决方案:

下面几部分将介绍针对各个后台任务类型的推荐解决方案。

即时任务

对于应在用户离开特定作用域或完成某项互动时结束的任务,我们建议使用 Kotlin 协程。许多 Android KTX 库都包含适用于常见应用组件(如 ViewModel)和常见应用生命周期的现成可用的协程作用域。

如果您是 Java 编程语言用户,请参阅 Android 上的线程处理,了解推荐的选项。

对于应立即执行并需要继续处理的任务,即使用户将应用放在后台运行或重启设备,我们也建议使用 WorkManager 并利用其对长时间运行的任务的支持。

在特定情况下(例如使用媒体播放或主动导航功能时),您可能希望直接使用前台服务。

延期任务

凡是不直接与用户互动相关且日后可随时运行的任务,都可以延期执行。建议为延期任务使用 WorkManager 解决方案。

如果您希望某些可延期异步任务即使在应用退出或设备重启后仍能正常运行,使用 WorkManager 可以轻松地调度这些任务。如需了解如何调度这些类型的任务,请参阅 WorkManager 相关文档。

精确任务

需要在精确时间点执行的任务可以使用 AlarmManager

如需详细了解 AlarmManager,请参阅设置重复闹铃时间。

走进源码

WorkManager相关类介绍

Worker

Worker 用于指定需要执行的具体任务。任务的具体逻辑在 Worker 里面写。Worker 是个抽象类。所以我们需要继承并实现这个类在定义自己的任务。

public abstract class Worker extends ListenableWorker {
    // Package-private to avoid synthetic accessor.
    SettableFuture<Result> mFuture;
    @Keep
    @SuppressLint("BanKeepAnnotation")
    public Worker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }
 	 /**
     * 任务逻辑
     * @return 任务的执行情况,成功,失败,还是需要重新执行
     */
    @WorkerThread
    public abstract @NonNull Result doWork();

    @Override
    public final @NonNull ListenableFuture<Result> startWork() {
        mFuture = SettableFuture.create();
        getBackgroundExecutor().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result result = doWork();
                    mFuture.set(result);
                } catch (Throwable throwable) {
                    mFuture.setException(throwable);
                }

            }
        });
        return mFuture;
    }
}

在 父类 ListenableWorker中:

/**
     * 任务的输入数据,有的时候可能需要我们传递参数进去,比如下载文件我们需要传递文件路径进去,
     * 在 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()函数使用。

WorkRequest

WorkRequest 代表一个单独的任务,是对 Worker 任务的包装,一个 WorkRequest 对应一个 Worker 类。

我们可以通过 WorkRequest 来给 Worker 类添加约束细节,比如指定任务应该运行的环境,任务的输入参数,任务只有在有网的情况下执行等等。

The base class for specifying parameters for work that should be enqueued in WorkManager. There are two concrete implementations of this class: OneTimeWorkRequest and PeriodicWorkRequest.

WorkRequest 是一个抽象类,组件里面也给两个相应的子类:

  • OneTimeWorkRequest(任务只执行一遍)、

  • PeriodicWorkRequest(任务周期性的执行)。

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(每次重试时间指数增加)。

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()开始的队列里面的任务是不能重复的。

Data

Data 是用于来给 Worker 设置输入参数和输出参数的。

比如:我们需要去网络上下载图,那么需要给 Worker 传入下载地址(输入参数),在 Worker 执行成功之后我们又需要获取到图片在本地的保持路径(输出参数)。 这这个传入传出都是通过Data来实现的。

Data是一个轻量级的容器(不能超过 10KB),Data 通过 key-value 的形式来保存信息。

其他类 略……

WorkStatus

包含有任务的状态和任务的信息,以 LiveData 的形式提供给观察者。

Demo 快速使用

  1. 添加依赖

    def versions = "2.2.0"
    implementation "androidx.work:work-runtime:$versions"
    
  2. 定义 Worker

    我们定义 MainWorker 继承 Worker,发现需要重写 doWork 方法,并且需要返回任务的状态 WorkerResult:

    class MainWorker : Worker() {
        override fun doWork(): WorkerResult {
            // 要执行的任务
            return WorkerResult.SUCCESS
        }
    }
    
  3. 定义 WorkRequest

    val request = OneTimeWorkRequest.Builder(MainWorker::class.java).build()
    
  4. 加入任务队列

    WorkManager.getInstance(context).enqueue(request)
    

加入任务队列后,任务会马上得到执行。是否真正执行,还得取决于环境是否有满足约束条件(如是否联网等)。

其他场景:

如链式调用:

WorkManager.getInstance(context)
        .beginWith(workA)
        .then(workB)
        .then(workC)
        .enqueue()

环境约束:

val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)  // 网络状态
        .setRequiresBatteryNotLow(true)                 // 不在电量不足时执行
        .setRequiresCharging(true)                      // 在充电时执行
        .setRequiresStorageNotLow(true)                 // 不在存储容量不足时执行
        .setRequiresDeviceIdle(true)                    // 在待机状态下执行,需要 API 23
        .build()

val request = OneTimeWorkRequest.Builder(MainWorker::class.java)
        .setConstraints(constraints)
        .build()

周期任务:

val request = PeriodicWorkRequest
        .Builder(MainWorker::class.java, 15, TimeUnit.MINUTES)
        .setConstraints(constraints)
        .setInputData(data)
        .build()

使用测试:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val constraints = Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build()

        val dateFormat = SimpleDateFormat("hh:mm:ss", Locale.getDefault())
        val data = Data.Builder()
                .putString("date", dateFormat.format(Date()))
                .build()

        val request = OneTimeWorkRequest
                .Builder(MainWorker::class.java)
                .setConstraints(constraints)
                .setInputData(data)
                .build()

        WorkManager.getInstance(context).enqueue(request)

        WorkManager.getInstance(context)
                .getStatusById(request.id)
                .observe(this, Observer<WorkStatus> { workStatus ->
                    if (workStatus != null && workStatus.state.isFinished) {
                        Log.d("MainActivity",
                                workStatus.outputData.getString("name", ""))
                    }
                })

    }
}

打开应用之前,先把网络关闭,打开后发现 Worker 并没有打印时间,这时候再把网连上,就会看到打印出时间了。

这也是为什么前面说 WorkManager.getInstance(context).enqueue(request) 是将任务加入任务队列,并不代表马上执行任务,因为任务可能需要等到满足环境条件的情况才会执行。

项目实践

需求背景:在收到服务端发出的指令后,设备端上传相关性能日志服务器,方便在服务端分析设备的性能。

伪代码实现:

class RequestLogCommand : IPushActionCommand {
		// 收到系统发来的相关指令后,开始在后台执行日志相关操作并上传到服务器
    override fun onMessageArrived(context: Context, entity: MqttPushEntity) {
        val workManager: WorkManager = WorkManager.getInstance(AppContext.getContext())
        val uploadSystemLog: OneTimeWorkRequest = OneTimeWorkRequest.Builder(UploadWorker::class.java).build()
        workManager.enqueue(uploadSystemLog)
    }

    class UploadWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
        override fun doWork(): Result {
            return try {
              // do something
                Log.i(TAG, "UploadWorker - success")
                Result.success()
            } catch (e: Exception) {
                e.printStackTrace()
                Log.e(TAG, "UploadWorker failure", e)
                Result.failure()
            }  finally {
              // do something
            }
        }

    }

    companion object {
        private const val TAG = "RequestLogCommand"
    }
}

其中的该类的调用时机,根据项目需要而实现,项目中是在收到消息后执行。本代码重在介绍几个重要的类的使用特点。

其他场景,后期再完善~

参考:

后台处理指南

WorkManager basics

你可能感兴趣的:(Android,源码解读,android,架构,kotlin,WorkManager)