AlarmManager
类)为您提供了一种在应用生命周期之外定时执行操作的方式。例如,您可以使用闹钟来启动长期运行的操作,比如每天启动一次某项服务以下载天气预报。
闹钟具有以下特征:
它们可让您按设定的时间和/或间隔触发 intent。
您可以将它们与广播接收器结合使用,以安排作业或 WorkRequest 以执行其他操作。
它们在应用外部运行,因此即使应用未运行,或设备本身处于休眠状态,您也可以使用它们来触发事件或操作。
它们可以帮助您最大限度地降低应用的资源要求。您可以安排定期执行操作,而无需依赖计时器或持续运行服务。
设置不精确闹钟
当应用设置不精确的闹钟时,系统会在未来某个时间点传送闹钟。不精确的闹钟在遵守省电限制(例如低电耗模式)的同时,可对闹钟提交时间提供一些保证。
开发者可以利用以下 API 保证来自定义不精确闹钟提交的时间。
在特定时间后发送闹钟
如果您的应用调用 set()
、setInexactRepeating()
或 setAndAllowWhileIdle()
,闹钟将永远不会在所提供的触发时间之前响铃。
在 Android 12(API 级别 31)及更高版本中,除非任何省电限制(例如省电模式或休眠模式)生效,否则系统会在所提供的触发时间一小时内调用闹钟。
在时间范围内提交闹钟
如果您的应用调用 setWindow()
,则闹钟绝不会在所提供的触发时间之前响铃。除非任何省电限制生效,否则闹钟会在指定的时间范围内(从给定触发时间开始)传送。
如果您的应用以 Android 12 或更高版本为目标平台,系统可以将有时间窗口的不精确闹钟的调用延迟至少 10 分钟。因此,600000
下的 windowLengthMillis
参数值会被剪裁为 600000
。
大致按固定时间间隔递送重复闹钟
如果您的应用调用 setInexactRepeating()
,系统会调用多个闹钟:
- 第一个闹钟会在指定的时间范围内响铃,从给定的触发时间开始。
- 后续闹钟通常会在指定时间范围结束后响铃。连续两次调用闹钟的时间可能会有所不同。
设置精确闹钟
系统会在未来的某个精确时刻调用精确闹钟。
大多数应用都可以使用不精确闹钟来安排任务和事件,以完成多种常见用例。如果应用的核心功能依赖于精确定时的闹钟(例如闹钟应用或日历应用),则可以改用精确闹钟。
可能不需要精确闹钟的用例
以下列表显示了可能不需要精确闹钟的常见工作流:
- 在应用生命周期中安排计时操作
Handler
类包含多种用于处理定时操作的实用方法,例如在应用处于活动状态时每隔 n 秒执行一些工作:postAtTime()
和postDelayed()
。请注意,这些 API 依赖于系统正常运行时间,而不是实时。- 安排好的后台工作,例如更新应用和上传日志
WorkManager
提供了一种安排时间敏感型定期工作的方式。您可以提供重复间隔和flexInterval
(至少 15 分钟),以定义工作的精细运行时间。- 应在特定时间过后执行的用户指定操作(即使系统处于空闲状态)
- 使用不精确闹钟。具体来说,就是调用
setAndAllowWhileIdle()
。 - 应在特定时间过后执行的用户指定操作
- 使用不精确闹钟。具体来说,就是调用
set()
。 - 可在指定时间范围内执行的用户指定操作
- 使用不精确闹钟。具体来说,就是调用
setWindow()
。请注意,如果您的应用以 Android 12 或更高版本为目标平台,则允许的最短时长为 10 分钟。
设置精确闹钟的方法
您的应用可以使用以下方法之一设置精确闹钟。这些方法的排序方式是,越靠近列表底部的任务越紧迫,但需要的系统资源也越多。
setExact()
在未来的某个几乎精确的时间调用闹钟,前提是没有其他省电措施生效。
除非应用的工作对用户来说是紧急的,否则请使用此方法设置精确闹钟。
setExactAndAllowWhileIdle()
在未来的某个几乎精确的时间调用闹钟,即使正在采用省电措施也是如此。
setAlarmClock()
在未来的某个精确时间调用闹钟。由于这些闹钟对用户来说非常醒目,因此系统绝不会调整其传送时间。系统会将这些闹钟识别为最重要的闹钟,并在必要时退出低功耗模式以提交闹钟。
系统资源消耗
当系统触发应用设置的精确的闹钟时,设备会消耗大量的资源(如电池续航时间),特别是在它处于省电模式时。此外,系统无法轻松地对这些请求进行批处理,以便更高效地使用资源。
我们强烈建议您尽可能创建不精确闹钟。如需执行时间更长的工作,请使用闹钟的 BroadcastReceiver
中的 WorkManager
或 JobScheduler
进行安排。如需在设备处于休眠模式的同时执行工作,请使用 setAndAllowWhileIdle()
创建不精确的闹钟,并从该闹钟启动作业。
声明适当的精确闹钟权限
如果您的应用以 Android 12 或更高版本为目标平台,您必须获取“闹钟和提醒”特殊应用访问权限。为此,请在应用的清单文件中声明 SCHEDULE_EXACT_ALARM
权限,如以下代码段所示:
<manifest ...> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <application ...> ... </application> </manifest>
如果您的应用以 Android 13(API 级别 33)或更高版本为目标平台,您可以选择声明 SCHEDULE_EXACT_ALARM
或 USE_EXACT_ALARM
权限。
<manifest ...> <uses-permission android:name="android.permission.USE_EXACT_ALARM"/> <application ...> ... </application> </manifest>
虽然 SCHEDULE_EXACT_ALARM
和 USE_EXACT_ALARM
权限都表示相同的功能,但授予方式不同,并且支持不同的用例。只有在应用中面向用户的功能需要精确计时的操作时,应用才应使用精确的闹钟,并声明 SCHEDULE_EXACT_ALARM
或 USE_EXACT_ALARM
权限。
USE_EXACT_ALARM
- 自动授予
- 无法被用户撤消
- 受即将推出的 Google Play 政策约束
- 使用情形受限
SCHEDULE_EXACT_ALARM
- 由用户授予
- 更广泛的用例
- 应用应确认相应权限未被撤消
系统不会向以 Android 13(API 级别 33)及更高版本为目标平台的新安装应用预先授予 SCHEDULE_EXACT_ALARM
权限。如果用户通过备份和恢复操作将应用数据转移到搭载 Android 14 的设备,则新设备上将拒绝 SCHEDULE_EXACT_ALARM
权限。不过,如果现有应用已拥有此权限,则当设备升级到 Android 14 时,系统会预先授予此权限。
注意:如果使用 OnAlarmListener
对象设置精确闹钟(例如使用 setExact
API),则不需要 SCHEDULE_EXACT_ALARM
权限。
使用 SCHEDULE_EXACT_ALARM
权限
与 USE_EXACT_ALARM
不同,SCHEDULE_EXACT_ALARM
权限必须由用户授予。用户和系统都可以撤消 SCHEDULE_EXACT_ALARM
权限。
如需检查系统是否已向您的应用授予相应权限,请先调用 canScheduleExactAlarms()
,然后再尝试设置精确闹钟。为您的应用撤消 SCHEDULE_EXACT_ALARM
权限后,您的应用会停止,并且将来的所有精确的闹钟都会取消。这也意味着,canScheduleExactAlarms()
返回的值在应用的整个生命周期内都有效。
向您的应用授予 SCHEDULE_EXACT_ALARMS
权限后,系统会向其发送 ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED
广播。您的应用应实现广播接收器,以便执行以下操作:
- 确认您的应用仍具有特殊应用访问权限。为此,请调用
canScheduleExactAlarms()
。此检查可保护您的应用免遭以下情况的侵害:用户向您的应用授予权限,然后几乎立即撤消该权限。 - 根据应用的当前状态,重新调度它所需的任何精确的闹钟。此逻辑应与您的应用接收
ACTION_BOOT_COMPLETED
广播时所执行的操作类似。
让用户授予 SCHEDULE_EXACT_ALARM
权限
如有必要,您可以将用户引导至系统设置中的闹钟和提醒屏幕,如图 1 所示。为此,请完成以下步骤:
- 在应用的界面中,向用户解释为什么您的应用需要调度精确的闹钟。
- 调用包含
ACTION_REQUEST_SCHEDULE_EXACT_ALARM
intent 操作的 intent。
设置重复闹钟
借助重复闹钟,系统可以按周期性时间表通知您的应用。
设计不合理的闹钟会导致耗电过度,并会使服务器负载显著增加。因此,在 Android 4.4(API 级别 19)及更高版本中,所有重复闹钟都是不精确闹钟。
重复闹钟具有以下特征:
闹钟类型。要了解详情,请参阅选择闹钟类型。
触发时间。如果您指定的触发时间为过去的时间,则闹钟会立即触发。
闹钟的间隔。例如,每天一次、每小时一次或每 5 分钟一次。
闹钟触发的待定 Intent。当您设置使用同一待定 intent 的第二个闹钟时,它会替换原始闹钟。
如需取消 PendingIntent()
,请将 FLAG_NO_CREATE
传递到 PendingIntent.getService()
,以获取该 intent 的实例(如果存在),然后将该 intent 传递到 AlarmManager.cancel()
Kotlin
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager val pendingIntent = PendingIntent.getService(context, requestId, intent, PendingIntent.FLAG_NO_CREATE) if (pendingIntent != null && alarmManager != null) { alarmManager.cancel(pendingIntent) }
Java
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); PendingIntent pendingIntent = PendingIntent.getService(context, requestId, intent, PendingIntent.FLAG_NO_CREATE); if (pendingIntent != null && alarmManager != null) { alarmManager.cancel(pendingIntent); }
选择闹钟类型
使用重复闹钟时的首要考虑因素之一是,它应该是哪种类型。
闹钟有两种常规时钟类型:“经过的时间”和“实时时钟”(RTC)。前者使用“自系统启动以来的时间”作为参考,而后者则使用世界协调时间 (UTC)(挂钟时间)。这意味着“经过的时间”类型适合用于设置基于时间流逝情况的闹钟(例如,每 30 秒触发一次的闹钟),因为它不受时区或语言区域的影响。“实时时钟”类型更适合依赖当前语言区域的闹钟。
这两种类型都有一个“唤醒”版本,该版本会在屏幕处于关闭状态时唤醒设备的 CPU。这可以确保闹钟在预定的时间触发。如果您的应用具有时间依赖项,这会非常有用。例如,如果它需要在限定时间内执行特定操作。如果您未使用闹钟类型的唤醒版本,则在您的设备下次处于唤醒状态时,所有重复闹钟都会触发。
如果您只需要让闹钟以特定的时间间隔(例如每半小时)触发,请使用某个“经过的时间”类型。一般而言,这是更好的选择。
如果您需要让闹钟在一天中的某个特定时段触发,请选择基于时钟的实时时钟类型之一。不过请注意,这种方法可能存在一些缺点。应用可能无法很好地转换到其他语言区域,并且如果用户更改设备的时间设置,可能会导致应用出现意外行为。如上所述,使用实时时钟闹钟类型也无法很好地扩展。如果可以的话,我们建议您使用“经过的时间”闹钟。
以下为类型列表:
ELAPSED_REALTIME
:基于自设备启动以来所经过的时间触发待定 intent,但不会唤醒设备。经过的时间包括设备处于休眠状态期间的任何时间。ELAPSED_REALTIME_WAKEUP
:唤醒设备,并在自设备启动以来特定时间过去之后触发待定 intent。RTC
:在指定的时间触发待定 intent,但不会唤醒设备。RTC_WAKEUP
:唤醒设备以在指定的时间触发待定 intent。
“经过的时间”闹钟示例
以下是使用 ELAPSED_REALTIME_WAKEUP
的一些示例
在 30 分钟后唤醒设备并触发闹钟,此后每 30 分钟触发一次:
Kotlin
// Hopefully your alarm will have a lower frequency than this! alarmMgr?.setInexactRepeating( AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HALF_HOUR, AlarmManager.INTERVAL_HALF_HOUR, alarmIntent )
Java
// Hopefully your alarm will have a lower frequency than this! alarmMgr.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HALF_HOUR, AlarmManager.INTERVAL_HALF_HOUR, alarmIntent);
在一分钟后唤醒设备并触发一个一次性(非重复)闹钟:
Kotlin
private var alarmMgr: AlarmManager? = null private lateinit var alarmIntent: PendingIntent ... alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager alarmIntent = Intent(context, AlarmReceiver::class.java).let { intent -> PendingIntent.getBroadcast(context, 0, intent, 0) } alarmMgr?.set( AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 60 * 1000, alarmIntent )
Java
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_WAKEUP
的一些示例。
在下午 2:00 左右唤醒设备并触发闹钟,并在每天的同一时间重复一次:
Kotlin
// Set the alarm to start at approximately 2:00 p.m. val calendar: Calendar = Calendar.getInstance().apply { timeInMillis = System.currentTimeMillis() 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.timeInMillis, AlarmManager.INTERVAL_DAY, alarmIntent )
Java
// 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:30 准时唤醒设备并触发闹钟,此后每 20 分钟触发一次:
Kotlin
private var alarmMgr: AlarmManager? = null private lateinit var alarmIntent: PendingIntent ... alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager alarmIntent = Intent(context, AlarmReceiver::class.java).let { intent -> PendingIntent.getBroadcast(context, 0, intent, 0) } // Set the alarm to start at 8:30 a.m. val calendar: Calendar = Calendar.getInstance().apply { timeInMillis = System.currentTimeMillis() set(Calendar.HOUR_OF_DAY, 8) set(Calendar.MINUTE, 30) } // setRepeating() lets you specify a precise custom interval--in this case, // 20 minutes. alarmMgr?.setRepeating( AlarmManager.RTC_WAKEUP, calendar.timeInMillis, 1000 * 60 * 20, alarmIntent )
Java
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);
确定所需的闹钟精确度
如前所述,选择闹钟类型通常是创建闹钟的第一步。除闹钟类型之外,进一步的区别在于所需的闹钟精确度。对于大多数应用来说,setInexactRepeating()
是合适的选择。使用此方法时,Android 会同步多个不精确的重复闹钟,并同时触发它们。这样可以减少耗电量。
尽量避免使用精确闹钟。不过,对于具有严格时间要求的少数应用,您可以通过调用 setRepeating()
设置精确闹钟。
使用 setInexactRepeating()
时,您无法像使用 setRepeating()
那样指定自定义时间间隔。您必须使用时间间隔常量(例如 INTERVAL_FIFTEEN_MINUTES
、INTERVAL_DAY
等)中的一个。如需查看完整的列表,请参阅 AlarmManager
。
取消闹钟
您可能需要添加取消闹钟的功能,具体取决于您的应用。如需取消闹钟,请在闹钟管理器上调用 cancel()
,并传入您不想再触发的 PendingIntent
。例如:
Kotlin
// If the alarm has been set, cancel it. alarmMgr?.cancel(alarmIntent)
Java
// If the alarm has been set, cancel it. if (alarmMgr!= null) { alarmMgr.cancel(alarmIntent); }
在设备重启时启动闹钟
默认情况下,当设备关机时,所有闹钟都会被取消。为了防止出现这种情况,您可以将应用设计为在用户重启设备时自动重新启动重复闹钟。这样可以确保 AlarmManager
继续执行其任务,而无需用户手动重新启动闹钟。
具体步骤如下:
在应用的清单中设置
RECEIVE_BOOT_COMPLETED
权限。通过这种方式,您的应用将能够接收系统完成启动后广播的ACTION_BOOT_COMPLETED
(这种方法仅适用于用户至少已启动过该应用一次的情况):<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
实现
BroadcastReceiver
以接收广播:Kotlin
class SampleBootReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (intent.action == "android.intent.action.BOOT_COMPLETED") { // Set the alarm here. } } }
Java
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. } } }
使用用于过滤
ACTION_BOOT_COMPLETED
操作的 intent 过滤器将该接收器添加到应用的清单文件中:<receiver android:name=".SampleBootReceiver" android:enabled="false"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED"></action> </intent-filter> </receiver>
请注意,在清单中,启动接收器设置为
android:enabled="false"
。这意味着除非应用明确启用该接收器,否则系统不会调用它。这可以防止系统不必要地调用启动接收器。您可以按照以下方式启用接收器(例如,如果用户设置了闹钟):Kotlin
val receiver = ComponentName(context, SampleBootReceiver::class.java) context.packageManager.setComponentEnabledSetting( receiver, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP )
Java
ComponentName receiver = new ComponentName(context, SampleBootReceiver.class); PackageManager pm = context.getPackageManager(); pm.setComponentEnabledSetting(receiver, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
您以这种方式启用接收器后,即使用户重启设备,它也会保持启用状态。也就是说,即使在设备重新启动后,以编程方式启用接收器也会覆盖清单设置。接收器将保持启用状态,直到您的应用将其停用。您可以按照以下方式停用接收器(例如,如果用户取消闹钟):
Kotlin
val receiver = ComponentName(context, SampleBootReceiver::class.java) context.packageManager.setComponentEnabledSetting( receiver, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP )
Java
ComponentName receiver = new ComponentName(context, SampleBootReceiver.class); PackageManager pm = context.getPackageManager(); pm.setComponentEnabledSetting(receiver, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
在设备处于休眠模式时调用闹钟
搭载 Android 6.0(API 级别 23)的设备支持休眠模式,有助于延长设备的电池续航时间。当设备处于低电耗模式时,闹钟不会触发。所有已设置的闹钟都会推迟,直到设备退出低电耗模式。如果您需要确保即使设备处于空闲状态您的工作也会完成,可以通过多种选项实现这一目的:
设置精确闹钟。
使用 WorkManager API,它可用于执行后台工作。您可以指示系统加快工作速度,以便尽快完成工作。如需了解详情,请参阅使用 WorkManager 安排任务
最佳做法
您在设计重复闹钟时所做的每一个选择都会对应用使用(或滥用)系统资源的方式产生影响。例如,假设有一个热门应用会与服务器同步。如果同步操作基于时钟时间,并且应用的每个实例都会在晚上 11:00 进行同步,那么服务器上的负载可能会导致高延迟,甚至是“拒绝服务”。请遵循以下使用闹钟的最佳做法:
为重复闹钟触发的网络请求加入一些随机性(抖动):
在闹钟触发时执行本地工作。“本地工作”是指无需连接至服务器或使用来自服务器的数据的任何工作。
同时,将包含网络请求的闹钟设置为在某个随机时间段内触发。
尽可能降低闹钟的触发频率。
请勿在不必要的情况下唤醒设备(该行为取决于闹钟类型,如选择闹钟类型中所述)。
请勿将闹钟的触发时间设置得过于精确。
使用
setInexactRepeating()
代替setRepeating()
。当您使用setInexactRepeating()
时,Android 会同步来自多个应用的重复闹钟,并同时触发它们。这可以减少系统必须唤醒设备的总次数,从而减少耗电量。从 Android 4.4(API 级别 19)开始,所有重复闹钟都是不精确闹钟。请注意,尽管setInexactRepeating()
是对setRepeating()
的改进,但如果应用的每个实例都大约在同一时间连接至服务器,仍会使服务器不堪重负。因此,如前所述,对于网络请求,请为您的闹钟添加一些随机性。尽量避免基于时钟时间设置闹钟。
基于精确触发时间的重复闹钟无法很好地扩展。如果可以的话,请使用
ELAPSED_REALTIME
。下一部分详细介绍了不同的闹钟类型。