Android实现定时任务的几种方式
相比Android倒计时的常用场景,定时任务相对来说使用的场景相对没那么多,除非一些特殊的设备或者一些特殊的场景我们会用到。
关于定时任务可以分为两种作用范围,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机制。
话说回来,AlarmManage有一个 AlarmManagerService ,该服务程序主要维护 app 注册下来的各类Alarm, 并且一直监听 Alarm 设备, 一旦有 Alarm 触发,或者是 Alarm 事件发生,AlarmManagerService 就会遍历 Alarm 列表,找到相应的注册 Alarm 并发出广播. 首先, Alarm 是基于 RTC 实时时钟计时, 而不是CPU计时; 其次, Alarm 会维持一个 CPU 的 wake lock, 确保 Alarm 广播能被处理。
他们之间的区别是,AlarmManager 最终是操作硬件,设备开机通电和关机就会丢失Alarm任务,而 JobSchedule 是系统级别的任务,就算重启设备也会继续执行。并且相较来说 AlarmManager 可以做到精准度可以比 JobSchedule 更加好点。
而 WorkManager 则是对JobSchedule的封装与兼容处理,6.0以上版本内部实现JobSchedule,一下的版本提供 AlarmManager 。提供的统一的Api实现相同的功能。
所以在2022年的今天,系统级别的定时任务就只推荐用 AlarmManager(短时间) 或者 WorkManager(长时间)了。
2.2 AlarmManager实现定时任务
由于各版本的不同使用的方式不同 API > 19的时候不能设置为循环 需要设置为单次的发送 然后在广播中再次设置单次的发送。
当API >23 当前手机版本为6.0的时候有待机模式的省点优化 需要重新设置。
当设备为Android 12,如果使用到了AlarmManager来设置定时任务,并且设置的是精准的闹钟(使用了setAlarmClock()、setExact()、setExactAndAllowWhileIdle()这几种方法),则需要确保SCHEDULE_EXACT_ALARM权限声明且打开,否则App将崩溃。
需要在AndroidManifest.xml清单文件中声明 SCHEDULE_EXACT_ALARM 权限
最终我们兼容所有的做法是,只开启一个定时任务,然后触发到广播,然后再广播中再次启动一个定时任务,依次循环。
例如我们设置一个 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)
}
}
}
打印日志。
2.3 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();
}
}
那除此之外我们一些Log的上传,图片的更新,资源或插件的下载等,我们都可以通过WorkManager来实现一些后台的操作,使用起来也是很简单。
总结
这里我直接给出了一些特定的场景应该使用哪一种定时任务,如果大家的应用场景适合App内部的定时任务,应该优先选择内部的定时任务。
App外的定时任务,都是系统服务的定时任务,不一定保险,毕竟是和厂商(特别是国内的厂商)作对,厂商会想方设法杀死我们的定时任务,毕竟有风险。