前言
一看标题,使用定时器,学Java基础就有,使用Timer类啊,不是这样的。
比如,在充电时、有网络时、低电量时、低存储时、手机空闲时等等,我们需要在这些其中的一个或多个条件满足后的某个时间内才执行的一些任务,要怎么实现?
注:JobService有这样的问题,当我们的手机使用专网的sim卡时,网络是不通互联网的,则在设置有网才执行的条件时,条件会不满足,导致任务不会被执行,可能WorkManager也有这样的问题,我懒得去实验了。感觉最好用的还是自己写个定时器吧,时间到了自己检测一下网络就好了,另外自己再监听一下网络,监听到有网络了,立马让定时器可以马上执行。
早期用:AlarmManager + BroadcastReceiver
后来用:JobScheduler
现在用:WorkManager
注:WorkManager不是用来处理所有后台任务的,什么样的后台任务用什么API,官方文档在此。
使用早期的方式比较麻烦,后来就有了JobScheduler,但是JobScheduler是Api 21才出来的,有兼容性问题,所以后来有了WorkManager,可以兼容到Android4.0版本。
我是发现公司的一个项目上使用到了JobScheduler开始学习的,学懂一点点没多久就发现又有了WorkManager,罪过,学Android咋这么苦啊,东西多的学不过来,这篇文章只能记录下一些文章连接了,没这么多时间了现在,工作上又好多东西要做的,只能先记录一下了。
这儿有一篇好文章:https://segmentfault.com/a/1190000006101221
官方,后台任务处理:https://developer.android.com/guide/background#ap
官方推荐的4种最佳后台工作方式:
WorkManager
前台服务
AlarmManager
DownloadManager
2019年11月19日官方博客:在Android上统一后台任务计划
WorkManager官网Demo
WorkManager官网Demo2
WorkManager一个重要的说明:
WorkManager用于可延迟的工作(即,不需要立即运行),并且即使应用程序退出或设备重新启动也需要可靠地运行。例如:
- 将日志或分析发送到后端服务
- 定期将应用程序数据与服务器同步
WorkManager不适用于进行中的后台工作,如果应用程序进程消失,该后台工作可以安全终止,或者用于需要立即执行的工作。请查看[后台处理指南](https://developer.android.google.cn/guide/background),以了解哪种解决方案可以满足您的需求。
JobScheduler的使用
官方的一篇博客:https://medium.com/google-developers/scheduling-jobs-like-a-pro-with-jobscheduler-286ef8510129
已经过时了,但是今天学了一点,过时归过时,还是记录一下吧。
定时执行一些任务,任务一般是耗时的吧,所以要用Service,定义一个类继承JobService,如下:
class MyJobService: JobService() {
override fun onStartJob(params: JobParameters?): Boolean {
// 当条件满足时就会执行这里的代码,比如有网络的情况下,5秒后执行这里,如果5秒内又没网了,则不会执行。如果等到有网了之后会立即执行(已经过了最短),不会再重新计算5秒。
Log.i(TAG, "${getCurrentTime()}:onStartJob")
return true
}
override fun onStopJob(params: JobParameters?): Boolean {
Log.i(TAG, "${getCurrentTime()}:onStopJob")
return true
}
companion object {
fun getCurrentTime() = DateFormat.format("kk:mm:ss", System.currentTimeMillis())
}
}
在onStartJob方法中执行自己的具体任务逻辑,需要开子线,因为Service是运行在主线程的,而这个Service什么时候执行呢?就需要使用到JobScheduler,它就可以定义需要满足什么条件,延迟多久执行任务等一些条件设置,代码如下:
class MyJobService: JobService() {
companion object {
val TAG: String = "MyJobService"
var jobScheduler: JobScheduler? = null
fun getCurrentTime() = DateFormat.format("kk:mm:ss", System.currentTimeMillis())
fun startScheduler(context: Context) {
jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
// 注:JobInfo可以声明为成员变量,只创建一次,在重复执行任务时可以复用。
val job: JobInfo = JobInfo.Builder(1, ComponentName(context, MyJobService::class.java))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
// 指定任务的延迟时间,即5秒后才开始此任务,即调用了jobScheduler.schedule()方法之后等5秒后再执行任务,任务就是判断如果有网络,则执行JobService的onStartJob方法
// 如果5秒后任务条件没有满足,等到满足的时候就会立马执行JobService的onStartJob方法
.setMinimumLatency(TimeUnit.SECONDS.toMillis(5)) // 最小为5秒,设置更小将使用5秒
//.setOverrideDeadline(TimeUnit.SECONDS.toMillis(10)) // 设置最大延迟时间为10秒,则调用jobScheduler.schedule()方法之后如果10秒后条件还不满足也执行JobService的onStartJob方法
// .setPeriodic(TimeUnit.SECONDS.toMillis(3)) // 最小间隔为15分钟(定义在JobInfo.getMinPeriodMillis()),不能比这个小,如果比这个小估计就会使用15分钟
.build()
val result = jobScheduler?.schedule(job)
Log.i(TAG, "${getCurrentTime()}:startScheldur:${if (result == 0) "RESULT_FAILURE" else "RESULT_SUCCESS"}")
}
fun cancelScheldur() {
jobScheduler?.cancelAll()
}
}
}
这里设置了条件,5秒后执行任务,任务的条件是有网络才执行MyJobService中的onStartJob方法。
关于onStartJob方法:JobParameters参数,由系统创建,如果想放里面存放一些内容,在JobInfo.Builder中可以设置。方法返回值,true代表任务还在执行,当自己的任务执行完成后调用jobFinished来告诉系统,此时系统会释放唤醒锁,不知道这个锁是什么东西。如果返回false,表示任务结束,系统直接释放唤醒锁。
onStopJob方法,并不是说任务结束了就会调用此方法,比如调用了jobFinished后此方法也不会执行。此方法执行的话,表示之前条件已经满足了,onStartJob方法已经执行了,但是此方法返回true,而且没有调用jobFinished,则系统并没有释放唤醒锁,如果此时条件突然不满足了则会回调此方法,比如你需要有网络才执行一个下载任务,当有网络时onStartJob开始执行,然后突然网络断开,则onStopJob开始执行,意思就是告诉你,网络没有了,你快停止下载任务吧,而且此时系统会释放唤醒锁,又或者你还没下载完,我们调用了jobScheduler.cancelAll()方法,表示取消任务,则onStopJob会执行,总之,onStopJob执行时,肯定是已经执行过了onStartJob的,onStopJob执行是告诉你条件突然不满足了,快停止你正在进行的任务。如果在onStartJob调用后,当你的任务完成时,你应该调用jobFinished方法来告诉系统你的任务完成了,此时系统释放唤醒锁,如果此时突然条件不满足了,系统也不会调用onStopJob了,因为你的任务已经完成了,即使你没完成,但是你调用了jobFinished就表示已经完成了,所以系统就没必要调用onStopJob来通知你停止任务了。 返回值:false表示工作完全结束,true表示你的工作没有结束,但你也必须停止你的任务,系统会根据你创建任务时提供的重试条件重新安排任务,即在下次条件满足时再来执行你的任务。再细说一下返回true的细节,如果d onStartJob中返回true,onStopJob中也返回true,则当条件满足后执行onStartJob,此后当条件不满足时执行onStopJob,如果设置了重试,则当条件再次满足时会再执行onStartJob,然后条件不满足时又执行onStopJob,即如果我们不调用jobFinished方法,在条件满足和不满足时就会一直执行onStartJob和onStopJob方法,实现只要条件一达到就执行一次任务。而如果onStopJob返回false,则只要onStopJob被调用一次,则表示任务已经结束了,之后 条件满足也不再执行onStartJob。 重试是通过JobInfo.Builder的setBackoffCriteria(long initialBackoffMillis, @BackoffPolicy int backoffPolicy)方法进行设置,第一个参数是设置延迟多少时间后再开始任务,开始任务不是说立马执行onStartJob,而是说指定的延迟时间之后 ,如果条件满足了则立即执行onStartJob,如果不满足则等到满足了才会执行onStartJob,第二参数是一个策略,如果指定为线性,假如延迟时间为10秒,则第一次重试延迟10秒,第二次重试时延迟20秒,第三次延迟30秒。。。
总结就是:一般onStartJob和onStopJob都返回true,在onStartJob中完成任务时再调用jobFinished,这样的话,在任务没完成时如果条件突然不满足了,则任务肯定要暂停了,则我们可以通过重试策略在将来条件再次满足时再继续我们的任务。
学完这些知识点就可以做这样一个需求了,完成一个类似定时器的功能,每5秒钟执行一次,而且是有网络的情况下才执行,如果没有网络就等到有网络才执行(如果实现定时向服务器发送心跳包),代码如下:
class MyJobService: JobService() {
override fun onStartJob(params: JobParameters?): Boolean {
Log.i(TAG, "${getCurrentTime()}:onStartJob")
startScheduler(this)
return false
}
override fun onStopJob(params: JobParameters?) = false
companion object {
val TAG: String = "MyJobService"
var jobScheduler: JobScheduler? = null
var job: JobInfo? = null
fun getCurrentTime() = DateFormat.format("kk:mm:ss", System.currentTimeMillis())
fun startScheduler(context: Context) {
jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
cancelScheduler()
if (job == null) {
job = JobInfo.Builder(1, ComponentName(context, MyJobService::class.java))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.setMinimumLatency(TimeUnit.SECONDS.toMillis(5)) // 最小为5秒,设置更小将使用5秒
.build()
}
val result = jobScheduler?.schedule(job!!)
Log.i(TAG, "${getCurrentTime()}:startScheduler:${if (result == 0) "RESULT_FAILURE" else "RESULT_SUCCESS"}")
}
fun cancelScheduler() {
jobScheduler?.cancelAll()
}
}
}
可以看到这里onStartJob和onStopJob都返回了false,只要onStartJob一执行就会释放唤醒锁,所以onStopJob永远也不会被调用,onStopJob返回什么都没有差异了,而onStartJob中立马又开始一个新的任务,所以系统又会为新任务分配一个唤醒锁,当条件再次满足时再次执行onStartJob。
对于说应用关掉,只要条件满足这安排的任务也会被执行,或者重启后如果条件满足了即使程序没运行,任务也会被执行,这个目前也没时间实验了,如果真的是这样,那就得注意如果程序都没启动的话,直接运行任务代码要写严谨,不能出Bug,即不要用到一些应用启动才初始化的一些实例变量。
还有一个细节点,就是我们指定的条件满足后,只是系统会尽快安排执行onStartJob,并不代表会立即执行的,还要根据系统的相关设置变化的,比如一些高版本的系统,当设置了省电模式时,可能我们的任务就不执行了,比如我们的应用后台运行了,虽然我们需要的条件满足了,但是设置了省电模式可能就不执行了,系统就限制了后台应用的任务执行了,为了省电嘛!只有等到用户关闭省电模式,并且那时我们的条件还满足的话才执行onsTartJob。这个我也没有实验过,在此记录一下有这个知识点。
这里面用到的类不多,也就这几个,查看系统Api就能理解其中的许多功能了:
- JobService:用于执行任务的服务
- JobInfo:用于设置需要满足的执行条件
- JobInfo.Builder:用于创建JobInfo对象
- JobScheduler:用于执行指定JobInfo描述的任务
这里也只是用到了最基本的功能,也只能记录到此了,没时间深学了,看到一篇有深入源码学习的文章:
https://www.jianshu.com/p/aa957774a965
贴官方Demo
WorkManager
WorkManager
依赖于若干 API,例如 JobScheduler
和 AlarmManager
。WorkManager
会根据用户设备 API 级别等条件选择使用适合的 API。
在官网上直接搜索WorkManager就会有很多的文章链接了,如下:
这些搜索结果一个个点开看应该足够自学就能学会了。
官网教程:https://developer.android.com/topic/libraries/architecture/workmanager?hl=zh-cn
官网博客:https://medium.com/androiddevelopers/introducing-workmanager-2083bcfc4712
Demo:
- https://developer.android.com/codelabs/android-workmanager?hl=zh-cn#0
- https://developer.android.com/codelabs/android-adv-workmanager?hl=zh-cn#0
视频:
- https://www.youtube.com/watch?v=Bz0z694SrEE
- https://www.youtube.com/watch?v=NtpgWjiXEfg&list=PLWz5rJ2EKKc_J88-h0PhCO_aV0HIAs9Qk
示例Demo:
实现功能:任务开始后3秒种后执行第一次,以后每15分钟执行一次,代码实现如下:
class UploadWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) {
override fun doWork(): Result {
i++
val currentTime = System.currentTimeMillis()
val useTime = currentTime - startTime
startTime = currentTime
val result = when (i) {
4, 5, 6, 7 -> Result.retry()
11, 12, 13 -> Result.failure()
else -> Result.success()
}
Timber.fi("$i ${DateUtil.formatMillis(useTime)} doWork()结果:${result.javaClass.simpleName} ${Thread.currentThread().name}")
// 返回Result.success()和Result.failure(),则指定的repeatInterval后会再次重复执行,如果返回Result.retry(),则按setBackoffCriteria()函数指定的时间进行重试。
return result
}
override fun onStopped() {
Timber.fi("工作被停止了!, 线程:${Thread.currentThread()}")
}
companion object {
private const val uniqueWorkName = "uploadPicture"
private var i = 0
private var startTime = 0L
fun startWork() {
Timber.fi("startWork()")
i = 0
// Worker约束条件:要有网络连接
val constrains = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
// 定期工作请求,每10分钟执行一次
val repeatInterval: Long = 15 // 重复间隔最少要设置为15分钟,如果小于15分则实际执行时按15分来执行。
val periodicWorkRequest = PeriodicWorkRequest.Builder(UploadWorker::class.java, repeatInterval, TimeUnit.MINUTES)
// 3秒后开始执行第一次,第一次之后按照repeatInterval指定的时间重复执行。与repeatInterval不同,InitialDelay没有最小时间限制,如果不设置,则启动任务后立马执行doWork()。
.setInitialDelay(3, TimeUnit.SECONDS)
.setConstraints(constrains)
// 失败重试策略为线性(BackoffPolicy.LINEAR), 重试时间为最小值(OneTimeWorkRequest.MIN_BACKOFF_MILLIS 10秒),
// 每次重试的等待时间为:第一次重试为10 x 1秒,第二次重试为10 x 2秒,第三次为10 x 3秒,第四次为10 x 4秒,可以看到:1、2、3、4为线性增长,每次加1
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance(ContextHolder.getContext()).enqueueUniquePeriodicWork(
uniqueWorkName, // 指定工作的名称
ExistingPeriodicWorkPolicy.KEEP, // 如果指定名称的工作存了,则保持原来的工作,并且不添加新工作
periodicWorkRequest
)
startTime = System.currentTimeMillis()
}
fun stopWork() {
Timber.fi("stopWork()")
WorkManager.getInstance(ContextHolder.getContext()).cancelUniqueWork(uniqueWorkName)
}
}
}
执行结果如下:
startWork()
1 00:03 doWork()结果:Success androidx.work-1
2 15:43 doWork()结果:Success androidx.work-2
3 15:34 doWork()结果:Success androidx.work-3
4 15:08 doWork()结果:Retry androidx.work-4
5 00:10 doWork()结果:Retry androidx.work-1
6 00:20 doWork()结果:Retry androidx.work-2
7 00:36 doWork()结果:Retry androidx.work-3
8 00:59 doWork()结果:Success androidx.work-4
9 15:01 doWork()结果:Success androidx.work-1
10 16:20 doWork()结果:Success androidx.work-2
11 15:08 doWork()结果:Failure androidx.work-3
12 15:00 doWork()结果:Failure androidx.work-4
13 15:00 doWork()结果:Failure androidx.work-1
14 15:00 doWork()结果:Success androidx.work-2
15 15:00 doWork()结果:Success androidx.work-3
16 15:00 doWork()结果:Success androidx.work-4
17 15:00 doWork()结果:Success androidx.work-1
可以看到,第一次是3秒后就执行了,第2、3、4是15分钟多一点执行,第4次时指定了Retry ,所以10后重试,共有3个Retry,理论上是按10、20、30这样的间隔来重试的,我们发现第三次是用了36秒,再往后有一个时间间隔16分多的,即使我们指定的重复执行间隔是15分,但是我们是有条件的,即要有网络连接满足了才会执行,而且也受系统限制,如果系统认为不应该执行的时候也会往后延迟,比如系统有更重要的事要做的时候,又或者比如设置了超级省电模式可能就需要等到用户设置回变通模式时才会执行。