闹钟(基于AlarmManager类)给你提供一种跳出app生命周期之外的一种操作基于时间的行为的一种方式。例如,你可以用闹钟来初始化一个长时间运行的操作,如每天启动一个service
来下载天气预报数据。
闹钟有如下特性:
- 闹钟帮你在特定的时间或者特定的时间间隔后启动
Intent
- 你可以把闹钟和
Broadcast Receiver
结合起来启动Service
,并执行其他操作 - 闹钟的操作是游离于你的app之外的,因此你可以在app没有启动、甚至在设备睡眠时,通过闹钟来触发你app中的事件或这动作
- 闹钟可以最小化app对于系统资源的需求。你可以在不依赖
timer
或者持续运行的后台service
来执行一些计划任务
NOte: 对于确定发生在app的生命周期内的定时操作,尽量考虑使用
Handler
类结合Timer
和Thread
来实现。这种行为赋予Android对于系统资源更好的控制。
Understand the Trade-offs – 理解得失
一个重复的闹钟是一种具有有限灵活性的简单机制。它或许不是你app的最好选择,特别是你需要触发网络操作的时候。一个设计不当的闹钟会导致电量剧烈消耗,以及显著增加服务器端负载。
一个常见的需要在你的app的生命周期之外触发的操作,是从服务器同步数据。这个时候,你应该使用重复闹钟来实现。不过,如果你要同步数据的服务器是你自己持有的,Google Cloud Messaging(GCM)配合sync adapter
是比AlarmManager
更好的一种解决方案。sync adapter
可以为你提供AlarmManager
所有的功能,且更加灵活。例如,一个同步操作可能是由一个“要求”更新数据的消息触发的,这个消息可能来自服务器,或者来自设备本身:如用户操作Activity,设备上的时间等等(see Running a Sync Adapter for details)。
Best practices – 最佳实践
你在设计你的重复闹钟时做的每一个决定,都会影响到你的app如何使用、甚至是滥用系统资源。例如,一款流行应用需要和服务器同步数据。如果这个同步操作是基于闹钟来触发的,并且每个用户都在晚上11点同步数据,那么这个时候,服务器的负载就会很高,甚至导致服务器宕机。下面是使用重复闹钟时的一些最近实践:
对于由重复闹钟触发的网络请求,请在时间上的添加一些随机性或者抖动。
- 被重复闹钟触发后,做一些本地操作。
本地操作
是指无需访问服务器或从服务器获取数据的操作。 - 与此同时,对于包含网络请求的重复闹钟,请在重复时间上添加一些随机变量。
- 被重复闹钟触发后,做一些本地操作。
把你闹钟的频率降到最低
只在最需要的时候唤醒设备(这个行为是闹钟的type属性决定的,在Choose an alarm type有描述)
如非必要,无需把你的闹钟间隔设置的那么精确。
使用
setInexactRepeating()
来代替setRepeating()
。当你使用setInexactRepeating()
时,Android系统会同步多个应用中的重复闹钟,在一个统一的时候欢喜这些闹钟。这样会减少系统必要的唤醒次数,降低电池电量的消耗。在Android4.4(API 19)中,所有的重复闹钟都是不精确的。注意一点,尽管setInexactRepeating()
相对于setRepeating()
来说是一个提升,但是每个用户上的app在同一个时间点附近集中访问服务器,同样会压垮服务器。因此,对于涉及网络请求的重复闹钟,在时间上增加随机性,就像上面提到的。避免使用时钟时间来设置重复闹钟
基于精确时间的闹钟工作并不良好。尽可能用
ELAPSED_REALTIME
。不同的闹钟类型会在下面详细描述。
Set a Repeating Alarm – 设置一个重复闹钟
就像上面说的,在重复且有规律的执行事件或获取数据时,重复闹钟是一个很好的选择。一个重复闹钟有如下几个特性:
闹钟类型。更多的讨论请看Choose an alarm type
触发时间。如果你设定的触发时间是在过去,那个这个闹钟会被立即触发。
时间间隔。例如每天、每小时、每5秒,等等。
一个在闹钟触发后触发的
Pending Intent
。如果你用相同的Pending Intent
设置了第二个闹钟,则前一个就会被取代。
Choose an alarm type – 选择闹钟类型 Choose an alarm type
使用重复闹钟时,第一个需要考虑到的问题就是它的类型。
闹钟大致有两种时钟类型:elapsed real time
and real time clock (RTC)
。Elapsed real time
把自从系统启动后的时间作为参考,而RTC用UTC时间。这意味着elapsed real time
适用于设置一个基于过去多少时间的闹钟(例如,每隔30s唤醒一次的闹钟),它不受时区和位置的影响。而RTC适用于基于当前时区或位置的闹钟。
这两张类型都有一个带“唤醒”的版本,也就是说在屏幕熄灭后依然可以唤醒CPU。这样确保了闹钟会在预定的事件被触发。这对于你的app有一个依赖时间的操作很有用–例如你的app有一个有限的窗口来执行以下特定操作。如果你没有使用可唤醒的闹钟类型,则所有的重复闹钟会在下次你的设备被唤醒的时候,一次性全部被触发。
如果你只是需要一个需要在特定时间间隔后唤醒的闹钟(例如,每隔半小时),请使用elapsed real time
类型的闹钟。通常状况下,这是一个比较好的选择。
如果你需要你的闹钟在一天中的特定时间唤醒,请使用RTC类型的闹钟。记住,无论如何,这里有一些问题–app无法在切换到其他时区是良好运行,假如用户改变了设备的时间设置,就会在app中引起不可预知的行为。使用RTC类型的闹钟就像上面讨论的那样,同样无法适应的很好。我们强烈建议你在可能的情况下尽量使用elapsed real time
类型的闹钟。
下面列出了所有的类型:
ELAPSED_REALTIME
—在一段时间后,触发pending intent,但是不会唤醒设备。这个时间会把设备休眠的时间也统计进来。ELAPSED_REALTIME_WAKEUP
—在设备启动后过去了多长时间后,唤醒设备并发出pending intent。RTC
—在特定的时间触发pending intent,但是不会唤醒设备。RTC_WAKEUP
—在特定时间,唤醒设备并触发pending intent。
ELAPSED_REALTIME_WAKEUP examples
这有一些使用ELAPSED_REALTIME_WAKEUP
的例子。
30分钟内唤醒设备并触发闹钟,之后每隔30分钟触发一次闹钟:
// Hopefully your alarm will have a lower frequency than this!
alarmMgr.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
AlarmManager.INTERVAL_HALF_HOUR,
AlarmManager.INTERVAL_HALF_HOUR, alarmIntent);
一分钟内唤醒设备并触发一个一次性的闹钟:
private AlarmManager alarmMgr;
private PendingIntent alarmIntent;
...
alarmMgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, AlarmReceiver.class);
alarmIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() +
60 * 1000, alarmIntent);
RTC examples
这里有一些使用RTC_WAKEUP
的例子。
在大约下午2点的时候,唤醒设备并触发这个闹钟,之后每天在同样的时间重复触发一次这个闹钟:
// Set the alarm to start at approximately 2:00 p.m.
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.HOUR_OF_DAY, 14);
// With setInexactRepeating(), you have to use one of the AlarmManager interval
// constants--in this case, AlarmManager.INTERVAL_DAY.
alarmMgr.setInexactRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
AlarmManager.INTERVAL_DAY, alarmIntent);
在上午8点20分的时候准时唤醒设备,之后每隔20分钟重复一次:
private AlarmManager alarmMgr;
private PendingIntent alarmIntent;
...
alarmMgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, AlarmReceiver.class);
alarmIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
// Set the alarm to start at 8:30 a.m.
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.HOUR_OF_DAY, 8);
calendar.set(Calendar.MINUTE, 30);
// setRepeating() lets you specify a precise custom interval--in this case,
// 20 minutes.
alarmMgr.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
1000 * 60 * 20, alarmIntent);
Decide how precise your alarm needs to be – 确定你的闹钟的精确度
如上所述,创建闹钟的第一步是选择闹钟类型。更近一步则是选择闹钟的精确度。对于大多数应用来说,setInexactRepeating()
是一个正确选择。当你使用这个方法时,Android系统会同步多个不精确的重复闹钟,并在一个统一的时间触发他们。这样会减少电池电量的消耗。
对于少数有精确时间要求的app来说–例如,闹钟需要在上午8点20准时被触发,并且在之后每个小时重复一次–请使用setRepeating()
。不过,你应该在可能的情况下尽量少使用精确闹钟。
使用setInexactRepeating()
的时候,你无法像使用setRepeating()
一样设置自定义的时间间隔。你只能使用一些时间间隔常量,如INTERVAL_FIFTEEN_MINUTES
, INTERVAL_DAY
,等等。如下表所示:
类型 | 名字 | 作用 |
---|---|---|
String | ACTION_NEXT_ALARM_CLOCK_CHANGED | Broadcast Action: Sent after the value returned by getNextAlarmClock() has changed. |
int | ELAPSED_REALTIME | Alarm time in SystemClock.elapsedRealtime() (time since boot, including sleep). |
int | ELAPSED_REALTIME_WAKEUP | Alarm time in SystemClock.elapsedRealtime() (time since boot, including sleep), which will wake up the device when it goes off. |
long | INTERVAL_DAY | Available inexact recurrence interval recognized by setInexactRepeating(int, long, long, PendingIntent) when running on Android prior to API 19. |
long | INTERVAL_FIFTEEN_MINUTES | Available inexact recurrence interval recognized by setInexactRepeating(int, long, long, PendingIntent) when running on Android prior to API 19. |
long | INTERVAL_HALF_DAY | Available inexact recurrence interval recognized by setInexactRepeating(int, long, long, PendingIntent) when running on Android prior to API 19. |
long | INTERVAL_HALF_HOUR | Available inexact recurrence interval recognized by setInexactRepeating(int, long, long, PendingIntent) when running on Android prior to API 19. |
long | INTERVAL_HOUR | Available inexact recurrence interval recognized by setInexactRepeating(int, long, long, PendingIntent) when running on Android prior to API 19. |
int | RTC | Alarm time in System.currentTimeMillis() (wall clock time in UTC). |
int | RTC_WAKEUP | Alarm time in System.currentTimeMillis() (wall clock time in UTC), which will wake up the device when it goes off. |
Cancel an Alarm – 取消一个闹钟
在你的app中,你也许想加上取消闹钟的功能。为了取消一个闹钟,你需要使用AlarmManager
调用cancel()
方法,并把你不想再被触发的PendingIntent
实例传到这个方法里。如下:
// If the alarm has been set, cancel it.
if (alarmMgr!= null) {
alarmMgr.cancel(alarmIntent);
}
Start an Alarm When the Device Boots – 在设备启动的时候开启一个闹钟
默认情况下,当设备关闭的时候,所有的闹钟都会被关闭。为了防止这种情况发生,你需要让你的app在用户重启设备后自动重启重复闹钟。这样可以确保AlarmManager
在无需用户手动重启闹钟的情况下,继续执行任务。
下面是具体步骤:
1. 在清单文件中,设置RECEIVE_BOOT_COMPLETED
权限。这样可以确保你的app可以收到系统在完成启动时发出的ACTION_BOOT_COMPLETED
广播(这个机制只会在用户已经启动过应用的情况下才能正常工作)。
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
- 实现一个
BroadcastReceiver
来接收开机广播。
public class SampleBootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) {
// Set the alarm here.
}
}
}
- 把这个receiver添加到app的清单文件中,并给这个receiver添加一个intent过滤器来过滤
ACTION_BOOT_COMPLETED
动作:
<receiver android:name=".SampleBootReceiver"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
</intent-filter>
</receiver>
- 请注意一点,在清单文件中,这个启动receiver的属性
android:enabled="false"
。这意味着这个receiver只有在app中显式的打开它,它才能收到开机广播。这样可以避免这个receiver被不必要的调用。你可以像下面一样打开一个receiver(例子中,假如用户设置了一个闹钟):
ComponentName receiver = new ComponentName(context, SampleBootReceiver.class);
PackageManager pm = context.getPackageManager();
pm.setComponentEnabledSetting(receiver,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
一旦你用这种方式打开了receiver,即使用户重启了设备,它还会保持打开状态。也就是说,代码中打开receiver,覆盖了清单文件中的设定,即使重启设备也不会被还原。这个receiver会一直保持开启状态,直至app关闭它。你可以像下面一样关闭一个receiver(例子中,假如用户设置了一个闹钟):
ComponentName receiver = new ComponentName(context, SampleBootReceiver.class);
PackageManager pm = context.getPackageManager();
pm.setComponentEnabledSetting(receiver,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
全篇完~