创建定时任务—Timer与Alarm

时间:2021-02-27 08:03:31

  Android中的定时任务一般有两种实现方式,一种是使用Java API中提供的 Timer 类,一种是使用Android的 Alarm 机制。
  这两种方式在多数情况下都能实现类似的效果,但是 Timer 有一个明显的短板,就是不太是用于那些需要长期在后台运行的定时任务。我们都知道,为了能让电池更加耐用,每种手机都会有自己的休眠策略,Android手机就会在长时间不操作的情况下自动让CPU进入到睡眠状态,这就有可能导致Timer中的定时任务无法正常运行。而 Alarm 则具有唤醒CPU的功能,它可以保证在大多数情况下需要执行定时任务的时候CPU都能正常工作。
  

1. Alarm机制

通过Context的getSystemService()方法获取AlarmManger实例:

AlarmManager manger = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

调用AlarmManager 的set()方法设置一个定时任务,比如设定一个任务在10秒后执行:

long triggerAtTime = SystemClock.elapsedRealtime() + 10 * 1000;
manger.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pendingIntent);

set方法中的三个参数:

set(int type, long triggerAtMillis, PendingIntent operation)

第一个参数:int

参数值 作用
ELAPSED_REALTIME 表示让定时任务的触发时间从系统开机开始算起,但不会唤醒CPU
ELAPSED_REALTIME_WAKEUP 同上,会唤醒CPU
RTC 表示让定时任务的触发时间从1970年1月1日0点开始算起,但不会唤醒CPU
RTC_WAKEUP 同上,会唤醒CPU


第二个参数:long

定时任务触发的时间,以毫秒为单位。如果第一个参数使用ELAPSED_REALTIME或ELAPSED_REALTIME_WAKEUP,则这里传入开机至今的时间再加上延迟执行的时间。如果第一个参数使用RTC或RTC_WAKEUP,则这里传入1970年1月1日0点至今的时间再加上延迟执行的时间。

SystemClock.elapsedRealtime() 可以获取到系统开机至今所经历时间的毫秒数
System.currentTimeMillis() 可以获取到1970年1月1日0点至今所经历的时间毫秒数

第三个参数:PendingIntent

PendingIntent从名字上看起来和Intent有些类似,它们之间也确实存在着不少共同特点。比如它们都可以去指明一个“意图”用于启动活动、服务或者发送广播等。不同的是Intent更加倾向于立即去执行某个动作,而PendingIntent更加倾向于在某个合适的时机去执行某个动作。
PendingIntent用法很简单,主要提供了几个静态方法用于获取实例,可以根据需求选择使用 getActivity()方法、getBroadcast()方法,还是getService()方法。三个方法接收的参数都是相同的:
1. 第一个参数是Context上下文。
2. 第二个参数是 int requestCode 一般用不到,通常都是传入 0 即可。
3. 第三个参数是一个 Intent 对象,我们可以通过这个对象构建出 PendingIntent 的“意图”。
4. 第四个参数用于确定PendingIntent的行为,有以下四种值可选:

参数值 作用
FLAG_ONE_SHOT 相同的PendingIntent只能使用一次,且遇到相同的PendingIntent时不会去更新PendingIntent中封装的Intent的extra部分的内容
FLAG_NO_CREATE 如果要创建的PendingIntent尚未存在,则不创建新的PendingIntent,直接返回null
FLAG_CANCEL_CURRENT 如果要创建的PendingIntent已经存在了,那么在创建新的PendingIntent之前,原先已经存在的PendingIntent中的intent将不能使用
FLAG_UPDATE_CURRENT 如果要创建的PendingIntent已经存在了,那么在保留原先PendingIntent的同时,将原先PendingIntent封装的Intent中的extra部分替换为现在新创建的PendingIntent的intent中extra的内容


当然也可以传入 0 ,就意味着你不打算使用任何一种flag来控制PendingIntent的创建。
例如:

Intent intent = new Intent(this, NotificationActivity.class);
PendingIntent pIntent = PendingIntent.getActivity(this, 0, intent, 0);

注意:
从Android 4.4 系统开始,Alarm任务的触发时间变得不准确,有可能会延迟一段时间后任务才能得到执行。这并不是bug,而是系统在耗电性方面进行的优化。系统会自动检测目前有多少Alarm任务存在,然后将触发时间相近的几个任务放在一起执行,这可以大幅度的减少CPU被唤醒的次数,从而有效延长电池的使用时间。
当然,如果你要求Alarm任务的执行时间必须准确无误,Android仍然提供了解决方案。使用AlarmManager的 setExact() 方法来替代 set() 方法,就基本上可以保证任务能够准时执行了。

2. Doze模式

虽然Android的每个系统版本都在手机电量方面努力进行优化,不过一直没能解决后台服务泛滥、手机电量消耗过快的问题。于是在Android 6.0系统中,谷歌加入了一个全新的Doze模式,从而可以极大幅度的延长电池的使用寿命。
当用户的设备是Android 6.0 或以上系统时,如果该设备未插接电源,处于静止状态(Android 7.0中删除了该条件),且屏幕关闭了一段时间之后,就会进入到Doze模式。在该模式下,系统会对CPU、网络、Alarm等活动进行限制,从而延长了电池的使用寿命。
当然,系统并不会一直处于Doze模式,而是会间歇性地退出Doze模式一小段时间,在这段时间中,应用就可以去完成它们的同步操作、Alarm任务等。

创建定时任务—Timer与Alarm

可以看到,随着设备进入Doze模式的时间越长,间歇性地退出Doze模式的时间间隔也会越长。因为如果设备长时间不适用的话,是没必要频繁退出Doze模式来执行同步等操作,Android在这些细节上的把控使得电池寿命进一步得到了延长。

**重点内容**Doze模式下可能受限制的功能:
- 网络访问被禁止
- 系统忽略唤醒CPU或者屏幕操作
- 系统不再执行WIFI扫描
- 系统不再执行同步服务
- Alarm任务将会在下次退出Doze模式的时候执行

注意最后一条,也就是说在Doze模式下Alarm任务将会变得不准时,当然在大多数情况下是合理的,因为只有当用户长时间不使用手机时才会进入Doze模式,通常在这种情况下对Alarm任务的准时性要求并没有那么高。
不过如果你真的有非常特殊的需求,要求Alarm任务即使在Doze模式下也必须正常执行,Android同样也提供了解决方案。调用AlarmManger的setAndAllowWhileIdle()或setExactAndAllowWhileIdle()方法就能让定时任务即使在Doze模式下也能正常执行了,这两个方法之间的区别和set()、setExact()方法之间的区别是一样的。