Android Workmanager 爬坑

5,068 阅读8分钟
原文链接: www.jianshu.com

目录

  • google后台任务推荐方案
  • doze简介
  • 从jobservice 到android-job 到workmanager
  • 遇到的一些坑
  • 总结

先看一下google推荐的后台任务的解决方案

image.png

如图后台任务可以从三个维度去考虑:

  • 是否需要立即执行
  • 是否需要执行完毕
  • 是否需要监听系统的状态 如网络状态等

然后给出了对应的具体的推荐方案

线程池、广播、前台任务这里暂时不提,主要看一下新出的workmanager,从图中可以看出workmanager的使用条件任务可以适当延迟的,即task没有闹钟那种强时效性。

workmanager是今年google io提出来的,目前版本还是1.0.0-alpha01,看见alpha就有点慌,后面果然出现bug了,这个后面再细说。

背景

为什么要使用workmanager,是alrammanager不好用吗?
其实这两者的作用并不完全一样;alarmmanager适用于类似闹钟那样必须准时唤醒的任务,不管是否处于doze(低耗电和应用待机模式);不过在6.0后的doze里面,alarmmanager需要调用setAndAllowWhileIdle()
setExactAndAllowWhileIdle()
如果必须要准时执行的任务比如股票每日开市提醒只能强行唤醒系统,但是也有的是不是要求时间这么准确的,比如每隔2天弹一个notification提示打开app啥的,这种实效性不那么高的就可以换成jobservice。

简单介绍一下doze

相关文档

image.png
这个是android6.0后出来的,6.0+的设备上都有这个,目的是为了省电。简单的说就是手机系统发现一段时间用户都没有在用手机了,就会进入省电模式,期间不会处理alarm、网络状态等,等省电了一段时间后再给一小段时间让所有应用统一处理alram等task,然后又进入省电状态。如果中途一直没有亮屏、充电等表示用户开始用手机了的操作,doze的省电周期会越来越长,如上图。
老实说,按照国人的习惯,白天手机想进入doze,基本上上不可能的。没有谁会好几个小时不碰手机吧。所以基本上都是晚上进入doze,这也说明了为啥早上亮屏的瞬间手机好多app会在同一时间弹出notification(亮屏就退出了doze,被延迟的alram马上开始工作)。

那么使用jobservice的优势在哪儿,我为什么不全部用alarmManager?

使用jobservice的优势就是doze下,不会唤醒系统,耗电量会减小。耗电量减小到一定程度,才能达到googleplay的推荐要求;在国内,两种类型差不多的app,用户通过电量消耗统计软件发现耗电量低的那个app,应该会对它更有好感吧(卸载也是先卸载竞争对手的app 哈哈)。

从jobservice 到android-job 到workmanager

前面说的时效性不是很强的任务可以用jobservice(JobScheduler),但是这个是android5.0+才出来的,要兼容5.0之前的,额外再用alramager;而且国外的还可以使用googleservice(Firebase JobDispatcher)实现。这就需要开发者自己来兼容;
后面发现印象笔记的android-job已经实现了类似的功能;
github上印象笔记的文档

androidjob的相关api

任务调度类型

  • 普通任务
    setExecutionWindow(start,end)
  • 特定时间执行的任务
    setExact(time)
  • 周期任务
    setPeriodic(long intervalMs, long flexMs)

任务的执行时间范围public Builder setExecutionWindow(long startInMs, long endInMs)

  • mStartMs Earliest point from which your task is eligible to run.

  • endInMs
    Latest point at which your task must be run. eligible to run.
    这两个是时间窗口的起始时间,意思是System.currentTimeMillis() + startInMs到System.currentTimeMillis() + endInMs之间执行,endInMs对应后面的deadline。
    在setPeriodic(long)和setExact(long)时不起作用

和周期任务相关的参数Builder setPeriodic(long intervalMs, long flexMs)

  • long mIntervalMs 这个是周期执行有关的参数,每隔多久执行一次,最低是15分钟。

  • long mFlexMs 这个是周期执行有关的参数,离周期末尾多久这个job应该被执行,最低5分钟。

    JobManager.instance().getConfig().setAllowSmallerIntervalsForMarshmallow(true); // Don't use this in production

    debug模式下设置这个,可以在api<24的机子上周期减小到60s,倒数减小到30s;正式环境,最低周期15分钟,倒数5分钟。

和任务执行失败,重新调度有关的参数public Builder setBackoffCriteria(long backoffMs, @NonNull BackoffPolicy backoffPolicy)

  • backoffMs
    非周期性的任务,执行失败后被重新调度需要等待的时间,配合着下面的策略使用。
    默认开始30s,逐渐增加最多5小时
  • backoffPolicy
    枚举,只有线性和指数增长两种方式。

一些二级条件吧,如果没有强制要求,会在deadline时忽略这些条件执行job

  • boolean mRequiresCharging
    是否需要插入设备(是指充电吧)
    默认false,但是如果之前的endInMS到了&&mRequirementsEnforced=false,就会忽略这个条件执行job

  • boolean mRequiresDeviceIdle 是否需要等到设备空闲。同上面一样,当deadline到来&&没有mRequirementsEnforced=false时,就会忽略这个条件执行Job

  • boolean mRequiresBatteryNotLow 是否禁止低电量。同上,deadline&&有mRequirementsEnforced=false会忽略。

  • boolean mRequiresStorageNotLow 是否禁止低存储空间。这个只有在android o才有效果

  • NetworkType mNetworkType
    需要等待的网络状态,是netWorkType的枚举,有好几种,类似wify 、流量等,默认是不需要关心网络状态。
    同上条件。

  • boolean mRequirementsEnforced 这个就是前面提到的那个参数,如果设为false,当job到deadline还没执行的时候,就可以抢救一下。

传参相关

  • PersistableBundleCompat mExtras
    带的额外参数,必须是这种类型,类似bundle。可以在onRunJob方法那里通过传过去的param取出来。

可以说android-job很良心了,暴露的api简单易用,功能强大。

workmanager的使用

本来已经使用android-job了,但是后面在github文档的后面看到了官方的说明:由于google出了workmanager,Android-job后面可能不再更新。然后又换到workmanager。

developer上workmanager的官方文档
使用起来还是很方便的,workmanager.enque(workRequest)就可以了,看起来类似google之前出的volley的风格,不知道是不是同一批程序员开发的哈哈。

public class DemoWorker extends Worker {
  public static final String TAG = "work_demo_tag";
  @NonNull @Override public WorkerResult doWork() {
    Data data = getInputData();
    int requestCode = data.getInt(AlarmMgr.ALARM_REQUEST_CODE, -1);
    String strData = data.getString(AlarmMgr.ALARM_NOTIFICATION_DATA, "");
    boolean bNotiClick = data.getBoolean(AlarmMgr.ALARM_NOTIFICATION_CLICK, false);
    Context context = getApplicationContext();
  //your job to do

  //可以再这里调用下一个一次性任务,形成周期循环。
 //现在默认执行成功,有需要再添加retry back-off相关逻辑
    return WorkerResult.SUCCESS;
  }
/**
   * @param requestCode requestCode
   * @param strData strData
   * @param bNotiClick bNotiClick
   */
  public static void schedule(final int requestCode, String strData, boolean bNotiClick) {
    WorkManager manager = WorkManager.getInstance();
   //下一次任务开始时,取消上一次相关tag类型的任务(避免网络状态不太好反复触发网络切换广播的情况)
   //这里如果queue里面存在100个以上job会crash,所以需要处理一下
    manager.cancelAllWorkByTag(String.valueOf(requestCode));
    OneTimeWorkRequest oneTimeWorkRequest =
        new OneTimeWorkRequest.Builder(AlarmReceiverWorker.class)
            .addTag(String.valueOf(requestCode))
            .setInputData(new Data.Builder()
                .putInt(AlarmMgr.ALARM_REQUEST_CODE, requestCode)
                .putString(AlarmMgr.ALARM_NOTIFICATION_DATA, strData)
                .putBoolean(AlarmMgr.ALARM_NOTIFICATION_CLICK, bNotiClick)
                .build())
            .build();
    manager.enqueue(oneTimeWorkRequest);
  }
}

需要注意的是队列里面任务(还在等待调度 未执行的那种)不能超过100个,不然会crash,这是workmanager代码的限制
workRequest子类有oneTime和periodic,对应一次性任务和周期性任务;因为基本上同样是对jobservice操作,所以方法很类似android-job,可以对照上面anroid-job用法或者查看developer文档workmanger简介
列一下常见用法

  • 多任务调度顺序
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();
  • 设置任务执行的约束条件
// 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();
  • 取消任务除了前面提到的cancleByTag,也可以用uniquework
 OneTimeWorkRequest oneTimeWorkRequest =
          new OneTimeWorkRequest.Builder(AlarmReceiverWorker.class).build();
      WorkManager.getInstance()
          .beginUniqueWork("downloadQueue", ExistingWorkPolicy.REPLACE, oneTimeWorkRequest);

说说坑吧

workermanager的周期任务,有时不能取消掉之前放到队列的任务。通过cancleByTag也不行,这个在stackoverflow和github上面的issue也看到有人提过,而且确实回复是个bug,应该是alpha版还是有些问题的。
既然周期任务有坑,我们也可以采用一次性任务开启下一个一次性任务,像链表那样。而且可以避开周期任务里面源码对周期设置最小15分钟的限制,不过一般也没那么流氓要隔几分钟就唤醒吧。
但是使用一次性任务循环触发,发现在小米上测试,打开app退到后台时,alarmmanager android-job workmanager正常,杀掉app,workmanager就不行了;使小米达到doze的条件,再亮屏,anroid-job也不行了。在小米上暂时只有alramanager适用(如果有哪位朋友发现小米上用workmanager有方法可以避开这个坑,请分享一下)。

还有就是前面提到的,要避开queue里面出现100个待调度的job的case。

总结

最后的做法定时任务使用alrammanager,等收到alram广播后交给worker处理;网络状态监听任务直接再接收到广播后交给worker处理。保证定时,但是收到广播放到队列里面不像之前那样接收到广播就串行马上执行了,等系统决定统一处理。

等后面workmanager版本号把alpha去掉就更好了;可以先用android-job,等workamanager出稳定版本再替换成worker,因为api太像了,替换的成本也不大。

这篇文章有些细节需要后面再进行补充。