AlarmManager提供对系统闹钟服务的访问。应用将自己的意图传递给系统闹钟服务,当闹钟响起时,系统会向应用发送广播。如果应用尚未执行,则会自动启动该应用程序。当设备属于睡眠状态时,注册的闹钟会被保留(如果此期间,闹钟关闭,可以选择唤醒设备),但如果系统关闭并重新启动,则会被清除。
一、定时任务的实现方案
(1)使用Timer实现
Timer.schedule 可以实现延迟执行某个任务,也可以重复执行某个任务。
Timer常常用于时间较短的延迟处理,或者循环次数较少的重复任务,比如30秒倒计时,倒计时结束之后需要cache任务;
Timer底层封装了一个线程,和TimerTask队列,这个队列按照一定的方式将任务排队处理。当Timer实例被创建的时候,线程被启动,从而执行线程的run方法。
但是,Timer却有一个致命的缺点,如果CPU一旦进入休眠状态(熄掉屏幕等待一段时间),线程将会失去CPU时间片而阻塞,从而造成定时任务失效。
实际上,熄掉屏幕等待一段时间之后,定时任务将不再定时,而是比预定的时间延迟一段时间才会执行,从而造成定时任务的不准确性,
当CPU休眠之后,定时任务将被完全阻塞。
如果再次唤醒屏幕,打开应用,定时任务将会被从新唤起,继续执行任务(非重新执行任务,而是继续执行任务,除非应用进程被系统杀死);
Timer定时器解决方案:如果能做到让CPU不休眠,那么就可以保证定时任务的准确性,使用WakeLock可以让CPU保持唤醒状态。
WakeLock一般我们不会使用它,因为让CPU保持唤醒是一个比较耗电的方式,用耗电的方式解决Timer定时器不准确的问题是得不偿失的。
(2)使用Handler实现
Handler 的 postDelayed 的方法可以实现延时任务,假如CPU进入休眠状态,延迟任务会失效。
这个现象和Timer一致。
(3)使用AlarmManager实现
AlarmManager是本章的重点,它是定时任务的重要方式,下文会有详细介绍。
二、AlarmManager接口介绍
(1)获取实例
// 获取系统闹钟服务管理对象
AlarmManager manager = (AlarmManager) getSystemService(Service.ALARM_SERVICE);
getSystemService方法可以获取系统的Manager Service实例,是应用和系统通信的主要方式。
AlarmManager是系统闹钟服务的管理对象。
(2)设置闹钟
public void set(@AlarmType int type, long triggerAtMillis, PendingIntent operation)
以上是设置闹钟的一个重要方法,但是从 Android 4.4(API 19) 开始,将变得不再精准,为了能够闹钟精准,Google提供了另外一个接口:
public void setExact(@AlarmType int type, long triggerAtMillis, PendingIntent operation)
第一个参数type是闹钟类型
,它有四种选项,分别是
AlarmManager.RTC_WAKEUP:让定时任务的触发时间从1970年1月1日0点开始算起,但会唤醒CPU
AlarmManager.RTC:让定时任务的触发时间从1970年1月1日0点开始算起,但不唤醒CPU
AlarmManager.ELAPSED_REALTIME_WAKEUP:让定时任务的触发时间从系统开机开始算起,但会唤醒CPU
AlarmManager.ELAPSED_REALTIME:让定时任务的触发时间从系统开机开始算起,但不唤醒CPU
第二个参数triggerAtMillis是闹钟任务的触发时间,这里要说到两种时间的概念
相对时间:设备boot后到当前经历的时间,SystemClock.elapsedRealtime()获取到的是相对时间。
绝对时间:1970年1月1日到当前经历的时间,System.currentTimeMillis()和Calendar.getTimeInMillis()获取到的都是绝对时间。
如果是相对时间,那么计算triggerAtMillis就需要使用SystemClock.elapsedRealtime();
如果是绝对时间,那么计算triggerAtMillis就需要使用System.currentTimeMillis()或者calendar.getTimeInMillis();
绝对时间与 AlarmManager.RTC_WAKEUP 和 AlarmManager.RTC 对应;
相对时间与 AlarmManager.ELAPSED_REALTIME_WAKEUP 和 AlarmManager.ELAPSED_REALTIME 对应;
为了让闹钟精准,下面简单做下适配
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// Android 4.4 (API 19)被添加
// 设置精准的闹钟
manager.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
// 或者
// manager.setWindow(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), 5*1000, pendingIntent);
} else {
// Android 4.4 之前为精准闹钟,之后不是精准闹钟
manager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
}
在 Android 5.0 (API 21)时,新增了另一个精准闹钟方法
public void setAlarmClock(AlarmClockInfo info, PendingIntent operation)
setAlarmClock 的默认闹钟类型是RTC_WAKEUP,代码如下:
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(System.currentTimeMillis() + 5*1000, pendingIntent);
// Android 5.0 (API 21)被添加
// 设置精确的闹钟
// 类似于 setExact 方法,但是 setAlarmClock 默认的闹钟类型是 RTC_WAKEUP
manager.setAlarmClock(alarmClockInfo, pendingIntent);
}
Android 6.0(API 23) 中引入了低电耗模式(Doze)和应用待机模式(standby)。
由于低电耗模式(Doze)的限制,AlarmManager闹钟将推迟到下一个维护期,如果您希望在设备处于低电耗模式(Doze)下也能触发闹钟,那么请使用
manager.setAndAllowWhileIdle(...);
或者
manager.setExactAndAllowWhileIdle(...);
常用的闹钟接受消息是用 BroadcastReceiver
或者 Service
,从Android 7.0(API 24)开始,新增了另一种接收方式,使用OnAlarmListener
监听的方式接收:
public void set(@AlarmType int type, long triggerAtMillis, String tag, OnAlarmListener listener,
Handler targetHandler)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
manager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, "MyTAG", new AlarmManager.OnAlarmListener() {
@Override
public void onAlarm() {
}
}, null);
}
参数说明如下:
tag:listener的TAG,在缓存中,肯能存在多个listener,为了方便管理,指定tag,为listener烙印上唯一标识;
listener:取代 BroadcastReceiver 和 Service;
targetHandler:一般设置为null即可,一旦设置为null,onAlarm方法必然在主线程执行,如果targetHandler不为null,那么需要注意:
不能在子线程中创建Handler对象,不确定是主线程还是子线程,那么需要在Handler的构造方法中执行主线程,把null替换成
new Handler(Looper.getMainLooper())
即可。
manager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, "AA", new AlarmManager.OnAlarmListener() {
@Override
public void onAlarm() {
}
}, new Handler(Looper.getMainLooper()));
或者执行Looper.prepare()
Looper.prepare();
manager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, "AA", new AlarmManager.OnAlarmListener() {
@Override
public void onAlarm() {
}
}, new Handler());
同样,对应的 setExact 和 setWindow 也在Android 7.0(API 24)的时候新增了OnAlarmListener的支持。
下面开始介绍,如何设置重复闹钟:
manager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), 1000, pendingIntent);
和
manager.setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), 1000, pendingIntent);
setInexactRepeating:设置不准确的重复闹钟,可以当作定时器
setRepeating:在Android 4.4之前(Android 19之前,不包括 19)是准确的,但是Android 4.4之后变为不准确;
(3)设置系统时间
// 设置系统时间
// 需要添加权限 android.Manifest.permission#SET_TIME
manager.setTime(xxx);
需要在AndroidManifest文件中添加权限:android.permission.SET_TIME
(4)设置系统时区
// 设置时区
// 需要添加权限 android.Manifest.permission#SET_TIME_ZONE
manager.setTimeZone(xxx);
需要在AndroidManifest文件中添加权限:android.permission.SET_TIME_ZONE
(5)取消闹钟
manager.cancel(pendingIntent);
或者
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
manager.cancel(alarmListener);
}
(6)是否可以安排精确闹钟
if (Build.VERSION.SDK_INT >= 31) {
manager.canScheduleExactAlarms();
}
canScheduleExactAlarms添加于Android 12(API 31),用于获取是否可以安排精确闹钟。
在大多数情况下,应用应该使用非精确闹钟 (inexact alarms),这样可以减少电池消耗。
Android 系统可以通过低电耗模式 (Doze) 和应用待机模式 (App Standby) 等机制管理这些闹钟,从而最大限度地减少设备唤醒和电池消耗。
对于那些需要精确闹钟的情况,例如闹铃应用和定时器,您仍然可以使用精确闹钟 (exact alarms)。
精确闹钟功能非常方便可靠,但也会加大电量消耗。
适配策略:尽可能调整为不需精确闹钟,针对 Android 12 的应用如果想要使用精确闹钟,现在需要申请一个新的权限: SCHEDULE_EXACT_ALARM。
这是一个一般权限,所以只要您的应用在清单中进行了声明,就会在第一次启动时被自动授予该权限。但用户仍可拒绝授予或撤销权限。
使用canScheduleExactAlarms(),可用来检查应用的权限状态。
三、简单代码例子
public class AlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
}
}
<receiver android:name=".AlarmReceiver">
<intent-filter>
<action android:name="com.test.alarm"/>
</intent-filter>
</receiver>
// PendingIntent
Intent intent = new Intent(MainActivity.this, AlarmReceiver.class);
intent.setAction("com.test.alarm");
intent.putExtra("name", "value");
PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// Android 4.4 (API 19)被添加
// 设置精准的闹钟
manager.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
} else {
// Android 4.4 之前为精准闹钟,之后不是精准闹钟
manager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
}
四、总结
(1)设置系统时间需要添加权限:android.permission.SET_TIME;
<uses-permission android:name="android.permission.SET_TIME" />
(2)设置系统时区需要添加权限:android.permission.SET_TIME_ZONE
<uses-permission android:name="android.permission.SET_TIME_ZONE" />
(3)如果需要在Android 12(API 31)上使用精准闹钟,必须设置权限:SCHEDULE_EXACT_ALARM
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
(4)重复闹钟只有 setRepeating 和 setInexactRepeating,其它均为一次性闹钟
(5)单次闹钟适配可以是这样的
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
manager.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
} else {
manager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
}
但是,如果需要在低功耗空闲状态下,闹钟也能生效,那么需要修改代码:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
manager.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
} else {
manager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
}
setExact效果等同于setWindow。
(6)重复闹钟适配可以是这样的
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
manager.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
} else {
manager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), 5*1000, pendingIntent);
}
由于setExactAndAllowWhileIdle和setExact是单次闹钟,当接收到闹钟时,再重新设置一下闹钟即可。
setRepeating本身就是重复闹钟,所以不需要重新设置;
(7)接收闹钟可以是BroadcastReceiver,也可以是Service,还可以是OnAlarmListener,只不过OnAlarmListener是Android 7.0新增的,需要做版本判断;