WorkManager 的使用指南及项目实践

首先放一个WorkManger介绍视频:​ ​Android Jetpack WorkManager | Android 中文教学视频_哔哩哔哩_bilibili中文官方介绍视频​​(主要是小姐姐好看~)

使用场景


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

定义
官方介绍:

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

到底是什么?

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

简单点说:

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

选择 WorkManager 的理由 Why?
Android 中处理后台任务的选择挺多的,比如​​Service​​​、​​DownloadManager​​​、​​AlarmManager​​​、​​JobScheduler​​​等,那么选择​​WorkManager​​的理由是什么呢?

版本兼容性强,向后兼容至 API 14。
可以指定约束条件,比如可以选择必须在有网络的条件下执行。
可定时执行也可单次执行。
监听和管理任务状态。
多个任务可使用任务链。
保证任务执行,如当前执行条件不满足或者 App 进程被杀死,它会等到下次条件满足或者 App 进程打开后执行。
支持省电模式。
多线程任务如何选择
后台任务会消耗设备的系统资源,如若处理不当,可能会造成设备电量的急剧消耗,给用户带来糟糕的体验。所以,选择正确的后台处理方式是每个开发者应当注意的,如下是官方给的选择方式:

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

 

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

即时任务
对于应在用户离开特定作用域或完成某项互动时结束的任务,我们建议使用 ​ https://developer.android.com/kotlin/coroutines​Kotlin 协程​​​。许多 ​ ​Android KTX​​​ 库都包含适用于常见应用组件(如 ​ ​https://developer.android.com/topic/libraries/architecture/coroutines#viewmodelscopeViewModel​​​)和常见应用​ ​生命周期​​的现成可用的协程作用域。

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

对于应立即执行并需要继续处理的任务,即使用户将应用放在后台运行或重启设备,我们也建议使用 ​https://developer.android.com/topic/libraries/architecture/workmanager ​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 快速使用
添加依赖
def versions = "2.2.0"
implementation "androidx.work:work-runtime:$versions"
定义 Worker
我们定义 MainWorker 继承 Worker,发现需要重写 doWork 方法,并且需要返回任务的状态 WorkerResult:
class MainWorker : Worker() {
    override fun doWork(): WorkerResult {
        // 要执行的任务
        return WorkerResult.SUCCESS
    }
}
定义 WorkRequest
val request = OneTimeWorkRequest.Builder(MainWorker::class.java).build()
加入任务队列
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​
-----------------------------------
©著作权归作者所有:来自51CTO博客作者小羊子说的原创作品,请联系作者获取转载授权,否则将追究法律责任
Android 架构组件之 WorkManager 的使用指南及项目实践
https://blog.51cto.com/jun5753/5262689

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值