Android 后台任务队列管理 Android Priority Job Queue和WorkManager
一:前言
有人说“Android的开发,玩的就是多线程”从某个角度来说的确如此,现在的App被设计的越来越复杂,相信很多开发人员都因大量而又复杂的后台任务(background work)而焦头烂额:Async-Task和Activity的生命周期太过于耦合,虽然实现简单但是对于重要的后台任务还是不靠谱
ndroid-Priority-Job-Queue是一款专门为Android平台编写的,实现了Job Queue的后台任务队列类库,能够轻松的在后台执行定时任务,并且提高了用户体验和应用的稳定性。
二:Android Priority Job Queue(后台管理任务队列)
其使用框架也很简便直接:
- 构造一个任务管理器JobManager,为我们管理任务;
- 自定义Job类,来作为任务的载体;
- 在需要时,将自定义的Job类实例加入到JobManager中;
这样就OK了,JobManager会根据优先级、持久性、负载平衡、延迟,网络控制、分组等因素来管理任务的执行。由于是独立于各个Activity,JobManager为Job的执行提供了一个很好的生命周期,
第一步:添加依赖
dependencies {
implementation 'com.birbit:android-priority-jobqueue:3.0.0'
}
第二步:配置JobManager
JobManager是整个框架的核心。作为一个重型的对象,建议Application只构建一个JobManager实例供全局使用
public class MyApplication extends Application {
private JobManager jobManager;//任务队列的Job管理
private static MyApplication instance;
@Override
public void onCreate() {
super.onCreate();
instance = this;//1. Application的实例
configureJobManager();//2. 配置JobMananger
}
//私有构造器
private MyApplication (){
instance=this;
}
public JobManager getJobManager() {
return jobManager;
}
public static MyApplication getInstance() {
return instance;
}
private void configureJobManager() {
//3. JobManager的配置器,利用Builder模式
Configuration configuration = new Configuration.Builder(this)
.customLogger(new CustomLogger() {
private static final String TAG = "JOBS";
@Override
public boolean isDebugEnabled() {
return true;
}
@Override
public void d(String text, Object... args) {
Log.d(TAG, String.format(text, args));
}
@Override
public void e(Throwable t, String text, Object... args) {
Log.e(TAG, String.format(text, args), t);
}
@Override
public void e(String text, Object... args) {
Log.e(TAG, String.format(text, args));
}
})
.minConsumerCount(1)//最少的活跃线程(这里配置是1)
.maxConsumerCount(3)//最多的开启线程(这里配置是3)
.loadFactor(3)// 一个Thread设置多3个任务(也可以Integer.MAX_VALUE,int最大值)
.consumerKeepAlive(120)// 设置线程在没有任务的情况下保持存活的时长,以秒为单位
.build();
jobManager = new JobManager(configuration);
}
}
- CustomLogger:日志设置,便于用户查看任务队列的工作信息,在调试的过程中很有用,后面分析JobManager的任务调度时就会用到;
- minConsumerCount&maxConsumerCount: 最少消费者和最多消费者数量,所谓的消费者就是开启的线程,用来执行任务。任务队列实际上就是一个生产者和消费者问题,用户是生产者,提交任务(Job),开启的线程就是消费者来执行任务,任务被执行就是“消费”。这里所谓的最少和最大将会下面具体解释;
- loadFactor(int): 其意义是设置多少个任务为一组被分配个一个消费者(Thread),也就是一个Thread最多要“承包”几个任务来执行;
- consumerKeepAlive :设置消费者在没有任务的情况下保持存活的时长,以秒为单位,如果过了这个时长还没有任务,消费者线程就会被回收
第三步:Job
自定义Job类需要继承Android-Priority-Job-Queue提供的Job类
public abstract class BaseJob extends Job {
protected BaseJob(Params params) {
super(params);
}
//任务加入队列并被保存在硬盘上,定义此时要处理的逻辑
@Override
public void onAdded() {
LogUtils.i("onAdded: " + getClass().getSimpleName());
}
//任务开始执执行,在此定义任务的主题逻辑,当执行完毕后,任务将被从任务队列中删除
@Override
public void onRun() throws Throwable {
}
//任务取消的时候要执行的逻辑
@Override
protected void onCancel(int cancelReason, @Nullable Throwable throwable) {
//调用释放
release();
}
//当onRun()方法中抛出异常时,就会调用该函数,该函数返回Job类在执行发生异常时的应对策略,是重新执行还是取消,或者是一定时间之后再尝试。
@Override
protected RetryConstraint shouldReRunOnThrowable(
@NonNull Throwable throwable, int runCount, int maxRunCount) {
}
/**
* 开始做任务
*/
public abstract void doJob() throws Throwable;
/**
* 重写这个方法进行资源回收
*/
protected void release() {
}
}
第四步:Params类
在这里我们要特别说一下Params类,通过该类可以配置Job类的各种信息,同样是采用链式调用
//我这里继承刚才抽象类BaseJob
public class StopAppJob extends BaseJob{
public StopAppJob(String pkgName, boolean isFirst) {
//这里我们主要看一下父类调用,这里我们构造的Params类,new Params(7):是默认构造器传入任务优先级7;groupBy():设置组ID;delayInMs()::设置延迟时间,ms为单位
super(new Params(7).addTags(TAG).groupBy(SCRIPT_JOB).delayInMs(ONE_SECOND));
this.pkgName = pkgName;
this.isFirst = isFirst;
}
@Override
public void onRun() throws Throwable {
super.onRun();
doJob();
}
@Override
public void doJob() throws Throwable {
//doSomething
}
}
1.默认构造器传入的是int参数是该任务的优先级,优先级越高,越优先执行。
public Params(int priority) {
this.priority = priority;
}
2.requireNetwork(): 设置该任务要求访问网络
3.groupBy(String groupId):设置组ID,被设置相同组ID的任务,将会按照顺序执行
4.persist():设置任务为可持久化的,持久化要求Job类为序列化的,这一点并不意外,因为一个类的内容只有序列化之后才能变成字节模式保存在硬盘上
5.delayInMs(long delayMs):设置延迟时间,ms为单位,在该时间之后再放入任务队列中。
第五步:执行Job任务
MyApplication.getJobManager()
.addJobInBackground(
new StopAppJob(PackageInfoConfig.TAOBAO_PACKAGE_NAME, true));
这个库的github:https://github.com/yigit/andr...
但是:这个项目已经不维护了:它是在一个没有 JobScheduler、RxJava 不流行、Kotlin 甚至没有公开诞生的世界中设计的。基于Java语言的
现在大部分使用的是WorkManager,故我们需要学习一下WorkManager
三:WorkManager使用
WorkManager是Android Jetpack的一部分,是用于后台工作的架构组件,需要兼顾机会和有保证的执行。机会性执行意味着WorkManager将尽快完成您的后台工作。有保证的执行意味着即使在离开应用程序的情况下,WorkManager也会兼顾各种情况下开始逻辑工作。
WorkManager是一个简单但非常灵活的库,它具有许多其他优点:
- 支持异步一次性和定期任务
- 支持网络条件,存储空间和充电状态等约束
- 链接复杂的工作请求,包括并行运行工作
- 一个工作请求的输出用作下一个工作的输入
- 将API级别的兼容性处理回API级别14(请参阅注释)
- 可以使用或不使用Google Play服务
- 遵循系统健康最佳实践
- LiveData支持可轻松在UI中显示工作请求状态
WorkManager几个关键的类:
Worker:任务的执行类,是一个抽象类需要集成它来实现要执行的任务
WorkRequest:指定哪个Worker执行任务,可以向WorkRequest中添加细节,指定执行环境,执行顺序,ID等。WorkRequest是个抽象类,在代码中使用其子类OneTimeWorkRequest或者PeriodicWorkRequest
WorkManager:对WorkRequest进行排队和管理
WorkStatus:包含任务的状态和信息,以LiveData的形式提供给观察者
1.第一步添加依赖
Java
// Java
implementation "androidx.work:work-runtime:2.5.0"
Kotlin
// kotlin workmanager
implementation "androidx.work:work-runtime-ktx:2.5.0"
2.第二步:使用Worker类定义任务
public class SendLogsWorker extends Worker {
public SendLogsWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
public Result doWork() {
//耗时任务执行在doWork()中执行
Log.d("aa","日志");
return Result.success();
}
}
源码:
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);
}
。。。。。
}
doWork()有3种返回类型
执行成功 Result.success()
执行失败 Result.failure()
重新执行 Result.retry()
3.第三步:使用WorkRequest分配任务
WorkRequest是一个抽象类,它的两种实现方式:OneTimeWorkRequest和PeriodicWorkRequest分别对应的是一次性任务和周期性任务
OneTimeWorkRequesty一次请求任务
public class TwelveActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_twelve);
//触发条件
Constraints constraints=new Constraints.Builder()
.setRequiresCharging(true)//设置充电状态,默认是flase
.setRequiredNetworkType(NetworkType.UNMETERED)//Wifi
.setRequiresStorageNotLow(true)//内存低,不工作
.setRequiresBatteryNotLow(true)//电量低,不工作
.build();
//一次性任务
OneTimeWorkRequest oneTimeWorkRequest=new OneTimeWorkRequest.Builder(SendLogsWorker.class)
.setConstraints(constraints)
.setInitialDelay(10,TimeUnit.SECONDS)//设置延迟执行10s
.build();
WorkManager.getInstance(this).enqueueUniqueWork("sendLogs", ExistingWorkPolicy.KEEP,oneTimeWorkRequest);
}
PeriodicWorkRequest重复任务请求
PeriodicWorkRequest sendLogsWorkRequest=new PeriodicWorkRequest.Builder(SendLogsWorker.class,5, TimeUnit.SECONDS)//定义5s周期,是没有效果的
//PeriodicWorkRequest使用和OneTimeWorkRequest没有太大区别,需要注意的是,间隔时间不能少于 15 分钟.
.setConstraints(new Constraints.Builder().setRequiresCharging(true).build())
.build();
//结果日志,可以看出相差15分钟
2021-07-06 14:07:04.265 25585-25647/com.ruan.mygitignore D/aa: 日志
2021-07-06 14:22:04.398 25585-26000/com.ruan.mygitignore D/aa: 日志
2021-07-06 14:37:04.565 25585-26356/com.ruan.mygitignore D/aa: 日志
触发条件:
- setRequiredNetworkType(NetworkType.UNMETERED) //设置需要的网络条件Wifi(UNMETERED (Wifi),CONNECTED(任何网络),NOT_REQUIRED(没有要求),NOT_ROAMING(连接非漫游网络),METERED(连接按流量计费的网络))
- setRequiresStorageNotLow(true)//如果设置为 true,那么当用户设备上的存储空间不足时,工作不会运行。
- setRequiresBatteryNotLow(true)//如果设置为 true,那么当用户设备上的电量不足时,工作不会运行。
WorkRequest
new OneTimeWorkRequest.Builder(SendLogsWorker.class)
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.LINEAR,OneTimeWorkRequest.MIN_BACKOFF_MILLIS,TimeUnit.MICROSECONDS)//10s重试和退避政策
.setInitialDelay(10,TimeUnit.SECONDS)//延迟执行
.addTag("log")//添加Tag标记
.build();
- setInitialDelay(10,TimeUnit.SECONDS)//如果工作没有约束,或者当工作加入队列时所有约束都得到了满足,那么系统可能会选择立即运行该工作。如果您不希望工作立即运行,可以将工作指定为在经过一段最短初始延迟时间后再启动
- setBackoffCriteria(BackoffPolicy.LINEAR,OneTimeWorkRequest.MIN_BACKOFF_MILLIS,TimeUnit.MICROSECONDS)//10s重试和退避政策,最短退避延迟时间设置为允许的最小值,即 10 秒。由于政策为 LINEAR,每次尝试重试时,重试间隔都会增加约 10 秒。例如,第一次运行以 Result.retry() 结束并在 10 秒后重试;然后,如果工作在后续尝试后继续返回 Result.retry(),那么接下来会在 20 秒、30 秒、40 秒后重试,以此类推。如果退避政策设置为 EXPONENTIAL,那么重试时长序列将接近 20、40、80 秒
- addTag("log")//每个工作请求都有一个唯一标识符,该标识符可用于在以后标识该工作,以便取消工作或观察其进度,例如,WorkManager.cancelAllWorkByTag(String) 会取消带有特定标记的所有工作请求,WorkManager.getWorkInfosByTag(String) 会返回一个 WorkInfo 对象列表,该列表可用于确定当前工作状态。
工作状态
一次性工作的状态
定期工作的状态
调度工作
//一次性工作
WorkManager.getInstance(this).enqueueUniqueWork("sendLogs", ExistingWorkPolicy.REPLACE,oneTimeWorkRequest);
//重复工作
WorkManager.getInstance(this).enqueueUniquePeriodicWork("sendLogs", ExistingPeriodicWorkPolicy.KEEP,sendLogsWorkRequest);
WorkManager.enqueueUniqueWork()(用于一次性工作)
WorkManager.enqueueUniquePeriodicWork()(用于定期工作)
这两种方法都接受 3 个参数:
- uniqueWorkName - 用于唯一标识工作请求的 String。
- existingWorkPolicy - 此 enum 可告知 WorkManager:如果已有使用该名称且尚未完成的唯一工作链,应执行什么操作。如需了解详情,请参阅冲突解决政策。
- work - 要调度的 WorkRequest。
冲突解决政策
调度唯一工作时,您必须告知 WorkManager 在发生冲突时要执行的操作。您可以通过在将工作加入队列时传递一个枚举来实现此目的。
对于一次性工作,您需要提供一个 ExistingWorkPolicy,它支持用于处理冲突的 4 个选项。
- REPLACE:用新工作替换现有工作。此选项将取消现有工作。
- KEEP:保留现有工作,并忽略新工作。
- APPEND:将新工作附加到现有工作的末尾。此政策将导致您的新工作链接到现有工作,在现有工作完成后运行。
- 现有工作将成为新工作的先决条件。如果现有工作变为 CANCELLED 或 FAILED 状态,新工作也会变为 CANCELLED 或 FAILED。如果您希望无论现有工作的状态如何都运行新工作,请改用 APPEND_OR_REPLACE。
- APPEND_OR_REPLACE 函数类似于 APPEND,不过它并不依赖于先决条件工作状态。即使现有工作变为 CANCELLED 或 FAILED 状态,新工作仍会运行。
对于定期工作,您需要提供一个 ExistingPeriodicWorkPolicy,它支持 REPLACE 和 KEEP 这两个选项。这些选项的功能与其对应的 ExistingWorkPolicy 功能相同。
任务链
WorkManager 允许我们按照一定的顺序执行任务,比如我想 A、B、C 三个任务按先后顺序执行:
WorkManager.getInstance()
.beginWith(workA)
.then(workB)
.then(workC)
.enqueue();
再更复杂一点,我想 A 和 B 同时执行,它们都执行完之后,再执行 C:
WorkManager.getInstance()
.beginWith(workA,workB)
.then(workC)
.enqueue();
这样就需要先把 A、B 和 C、D 分别组成一条任务链,再进行联结:
//这里Kolin语言实现
val chain1 = WorkManager.getInstance()
.beginWith(workA)
.then(workB)
val chain2 = WorkManager.getInstance()
.beginWith(workC)
.then(workD)
val chain3 = WorkContinuation
.combine(chain1, chain2)
.then(workE)
chain3.enqueue()
结尾:一切美好的事物,终将毁于时间
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。