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为单位,在该时间之后再放入任务队列中。
image.png
第五步:执行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: 日志

image.png
触发条件:

  • 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 对象列表,该列表可用于确定当前工作状态。
    工作状态
    一次性工作的状态
    image.png

定期工作的状态
image.png
调度工作

//一次性工作
 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 三个任务按先后顺序执行:
image.png

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

再更复杂一点,我想 A 和 B 同时执行,它们都执行完之后,再执行 C:
image.png

WorkManager.getInstance()
        .beginWith(workA,workB)
        .then(workC)
        .enqueue();

image.png
这样就需要先把 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()

结尾:一切美好的事物,终将毁于时间


Rocky_ruan
57 声望5 粉丝

不积跬步,无以至千里