注册

Android实现定时任务的几种方案汇总

前言


相比Android倒计时的常用场景,定时任务相对来说使用的场景相对没那么多,除非一些特殊的设备或者一些特殊的场景我们会用到。


关于定时任务其实是分为2中作用范围,App内部范围和App外部范围,也就是说你是否需要App杀死了还能执行定时任务,需求不同实现的方式也不同,我们来看看都如何实现。


一、App内部范围


其实App内部范围的定时任务,我们可以使用倒计时的方案,Handler天然就支持。其实我们并不是需要每一次都使用一些系统服务让App外部范围生效。


比如场景如下,机器放在公司前台常亮并且一直运行在前台的,我需要没间隔60秒去查询当前设备是否在线,顺便更新一下当前的时间,显示早上好,中午好,下午好。


这样的场景我不需要使用一些系统服务,使用App内部范围的一些定时任务即可,因为就算App崩溃了,就算有系统级别的定时任务,我App不在了也没有用了,所以使用内部范围的定时任务即可,杀鸡焉用牛刀。


之前的倒计时方案改造一番几乎都能实现这样的定时任务,例如:


    private var mThread: Thread = Thread(this)
private var mflag = false
private var mThreadNum = 60

override fun run() {
while (mflag && mThreadNum >= 0) {
try {
Thread.sleep(1000 * 60)
} catch (e: InterruptedException) {
e.printStackTrace()
}

val message = Message.obtain()
message.what = 1
message.arg1 = mThreadNum
handler.sendMessage(message)

mThreadNum--
}
}

private val handler = Handler(Looper.getMainLooper()) { msg ->

if (msg.what == 1) {
val num = msg.arg1
//由于需要主线程显示UI,这里使用Handler通信
YYLogUtils.w("当时计数:" + num)
}

true
}

//定时任务
fun backgroundTask() {

if (!mThread.isAlive) {

mflag = true

if (mThread.state == Thread.State.TERMINATED) {
mThread = Thread(this@DemoCountDwonActivity)
if (mThreadNum == -1) mThreadNum = 60
mThread.start()
} else {
mThread.start()
}
} else {

mflag = false

}

}


这样每60秒就能执行一次任务,并且不受到系统的限制,简单明了。(只能在App范围内使用)


倒计时的一些的一些方案我们都能改造为定时任务的逻辑,比如上面的Handler,还有Timer的方式,Thread的方式等。


除了倒计时的一些方案,我们额外的还能使用Java的线程池Api也能快速的实现定时任务,周期性的执行逻辑,例如:



val executorService: ScheduledExecutorService = Executors.newScheduledThreadPool(3)
val command = Runnable {
//dosth
}
executorService.scheduleAtFixedRate(command, 0, 3, TimeUnit.SECONDS)

// executorService.shutdown() //如果想取消可以随时停止线程池

定时执行任务在平常的开发中相对不是那么多,比如特殊场景下,我们需要轮询请求。


比如我做过一款应用放在公司前台,我就需要轮询请求每180秒调用服务器接口,告诉服务器当前设备是否在线。便于后台统计,这个是当前App内部生命周期执行的,用在这里刚刚好。


又比如我们使用DownloadManager来下载文件,因为不能直接回调进度,需要我们手动的调用Query去查询当前下载的消息,和文件的总大小,计算当前的下载进度,我们就可以使用轮询的方案,每一秒钟调用一次Query获取进度,模拟了下载进度的展示。


二、App外部范围


有内部范围的定时任务了,那么哪一种情况下我们需要使用外部范围的定时任务呢?又如何使用外部范围的定时任务呢?


还是上面的场景,机器放在公司前台常亮并且一直运行在前台的,这个App我们需要自动更新,并且检查是否崩溃了或者在前台,不管App是否存在我们都需要自行的定时任务,超过App的生命周期了,我们需要使用系统服务的定时任务来做这些事情。


都有哪些系统服务可以完成这样的功能呢?


2.1 系统服务的简单对比与原理

AlarmManager JobSchedule WorkManager !三者又有哪些不同呢?


AlarmManager 和 JobSchedule 虽然都是系统服务,但是方向又不同,AlarmManager 是通过 AlarmManagerService 控制RTC芯片。


说起Alar就需要说到RTC,说到RTC就需要讲到WakeLock机制。


都是一些比较底层的原理,我不会具体展开,大家有兴趣可以自行搜索,或者参考


Android对RTC时间的操作流程


话说回来,AlarmManage有一个 AlarmManagerService ,该服务程序主要维护 app 注册下来的各类Alarm, 并且一直监听 Alarm 设备, 一旦有 Alarm 触发,或者是 Alarm 事件发生,AlarmManagerService 就会遍历 Alarm 列表,找到相应的注册 Alarm 并发出广播. 首先, Alarm 是基于 RTC 实时时钟计时, 而不是CPU计时; 其次, Alarm 会维持一个 CPU 的 wake lock, 确保 Alarm 广播能被处理。


JobSchedule则是完全Android系统级别的定时任务,如有感兴趣的可以参考文章


Android之JobScheduler运行机制源码分析


他们之间的区别是,AlarmManager 最终是操作硬件,设备开机通电和关机就会丢失Alarm任务,而 JobSchedule 是系统级别的任务,就算重启设备也会继续执行。并且相较来说 AlarmManager 可以做到精准度可以比 JobSchedule 更加好点。


而 WorkManager 则是对JobSchedule的封装与兼容处理,6.0以上版本内部实现JobSchedule,一下的版本提供 AlarmManager 。提供的统一的Api实现相同的功能。


所以在2022年的今天,系统级别的定时任务就只推荐用 AlarmManager(短时间) 或者 WorkManager(长时间)了。


2.2 AlarmManager实现定时任务

由于不是基础教程,如果要这里要讲一下基本使用,我估计这一篇文章都讲不完,如果想看AlarmManager的使用教程,可以看这里


Android中的AlarmManager的使用


由于各版本的不同使用的方式不同
API > 19的时候不能设置为循环 需要设置为单次的发送 然后在广播中再次设置单次的发送。


当API >23 当前手机版本为6.0的时候有待机模式的省点优化 需要重新设置。


当设备为Android 12,如果使用到了AlarmManager来设置定时任务,并且设置的是精准的闹钟(使用了setAlarmClock()、setExact()、setExactAndAllowWhileIdle()这几种方法),则需要确保SCHEDULE_EXACT_ALARM权限声明且打开,否则App将崩溃。


需要在AndroidManifest.xml清单文件中声明 SCHEDULE_EXACT_ALARM 权限


最终我们兼容所有的做法是,只开启一个定时任务,然后触发到广播,然后再广播中再次启动一个定时任务,依次循环,嗯,很有Handler的味道。


例如我们设置一个 AlarmManager ,每180秒检查一下 App 是否存活,如果 App 不在了就拉起 App 跳转MainActivity。(需求是当App杀死了也能启动首页,所以不适用于App内的定时执行方案)


    //定时任务
fun backgroundTask() {

//开启3分钟的闹钟广播服务,检测是否运行了首页,如果退出了应用,那么重启应用
val alarmManager = applicationContext.getSystemService(ALARM_SERVICE) as AlarmManager

val intent1 = Intent(CommUtils.getContext(), AlarmReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(CommUtils.getContext(), 0, intent1, PendingIntent.FLAG_UPDATE_CURRENT)

//先取消一次
alarmManager.cancel(pendingIntent)

//再次启动,这里不延时,直接发送
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), pendingIntent)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), pendingIntent)
} else {
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), 18000, pendingIntent)
}

YYLogUtils.w("点击按钮-开启 alarmManager 定时任务啦")

}

点击按钮就发送一个立即生效的闹钟,逻辑走到广播中,然后再广播中再次开启闹钟。


class AlarmReceiver : BroadcastReceiver() {

override fun onReceive(context: Context, intent: Intent?) {

val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager

//执行的任务
val intent1 = Intent(CommUtils.getContext(), AlarmReceiver::class.java)
val pendingIntent: PendingIntent = PendingIntent.getBroadcast(context, 0, intent1, PendingIntent.FLAG_UPDATE_CURRENT)

// 重复定时任务,延时180秒发送
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 180000, pendingIntent)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 180000, pendingIntent)
} else {
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), 180000, pendingIntent);
}

YYLogUtils.w("AlarmReceiver接受广播事件 ====> 开启循环动作")

//检测Activity栈里面是否有MainActivity
if (ActivityManage.getActivityStack() == null || ActivityManage.getActivityStack().size == 0) {
//重启首页
// context.gotoActivity<DemoMainActivity>()
} else {
YYLogUtils.w("不需要重启,已经有栈在运行了 Size:" + ActivityManage.getActivityStack().size)
}
}

}

打印日志,由于间隔时间太长,我手机放者,直接把Log保持到Log文件,导出出来,截图如下:



最后是杀死App之后收到的广播。


杀死App手机放了一会,我去个洗手间回来,看看打印日志情况。



2.3 WorkManager实现定时任务

同样的如果不清楚WorkManager的基础使用,推荐大家看看教程


Android架构组件WorkManager详解


WorkManager的使用相对来说也比较简单, WorkManager组件库里面提供了一个专门做周期性任务的类PeriodicWorkRequest。但是PeriodicWorkRequest类有一个限制条件最小的周期时间是15分钟。


WorkManager 比较适合一些比较长时间的任务。还能设置一些约束条件,比如我们每24小时,在设备充电的时候我们就上传这一整天的Log文件到服务器,比如我们每隔12小时就检查应用是否需要更新,如果需要更新则自动下载安装(需要指定Root设备)。


场景如下,还是那个放在公司前台常亮并且一直运行在前台的平板,我们每12小时就检查自动更新,并自动安装,由于之前写了 AlarmManager 所以安装成功之后App会自动打开。


伪代码如下:


        Data inputData2 = new Data.Builder().putString("version", "1.0.0").build();
PeriodicWorkRequest checkVersionRequest =
new PeriodicWorkRequest.Builder(CheckVersionWork.class, 12, TimeUnit.HOURS)
.setInputData(inputData2).build();

WorkManager.getInstance().enqueue(checkVersionRequest);
WorkManager.getInstance().getWorkInfoByIdLiveData(checkVersionRequest.getId()).observe(this, workInfo -> {
assert workInfo != null;
WorkInfo.State state = workInfo.getState();

Data data = workInfo.getOutputData();
String url = data.getString("download_url", "");
//去下载并静默安装Apk
downLoadingApkInstall(url)
});

/**
* 间隔12个小时的定时任务,检测版本的更新
*/
public class CheckVersionWork extends Worker {

public CheckVersionWork(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}

@Override
public void onStopped() {
super.onStopped();
}

@NonNull
@Override
public Result doWork() {
Data inputData = getInputData();
String version = inputData.getString("version");

//接口获取当前最新的信息

//对比当前版本与服务器版本,是否需要更新

//如果需要更新,返回更新下载的url

Data outputData = new Data.Builder().putString("key_name", inputData.getString("download_url", "xxxxxx")).build();
//设置输出数据
setOutputData(outputData);

return Result.success();
}
}

这个时间太长了不好测试,不过是我之前自用的代码,没什么问题,哪天有时间做个Demo把日志文件导出来看看才能看出效果。


那除此之外我们一些Log的上传,图片的更新,资源或插件的下载等,我们都可以通过WorkManager来实现一些后台的操作,使用起来也是很简单。


总结


这里我直接给出了一些特定的场景应该使用哪一种定时任务,如果大家的应用场景适合App内部的定时任务,应该优先选择内部的定时任务。


App外的定时任务,都是系统服务的定时任务,不一定保险,毕竟是和厂商(特别是国内的厂商)作对,厂商会想方设法杀死我们的定时任务,毕竟有风险。


关于系统服务的定时任务我感觉自己讲的不是很好,好在给出了一些方案和一些文章,大家如果对一些基础的使用或者底层原理感兴趣,可以自行了解一下。


关于系统服务的周期任务的使用如果有错误,或者版本兼容的问题,又或者有更多或更好的方法,也可以在评论区交流讨论。


如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。


Ok,这一期就此完结。



作者:newki
链接:https://juejin.cn/post/7130793502933254180
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册