AlarmManager
类)为您提供了一种在应用生命周期之外执行基于时间操作的方法。例如,您可以使用闹钟启动一项长时间运行的操作,例如每天启动一次服务以下载天气预报。
闹钟具有以下特征
它们允许您在设定的时间和/或间隔触发 Intent。
您可以将它们与广播接收器结合使用,以计划 作业 或 WorkRequest 以执行其他操作。
它们在您的应用之外运行,因此您可以使用它们来触发事件或操作,即使您的应用未运行,甚至设备本身处于休眠状态时也可以。
它们可帮助您最大程度地减少应用的资源需求。您可以安排操作,而无需依赖计时器或持续运行的服务。
设置不精确闹钟
当应用设置不精确闹钟时,系统将在未来的某个时间传递闹钟。不精确闹钟在尊重省电限制(例如 Doze)的同时,提供了一些关于闹钟传递时机的保证。
开发者可以利用以下 API 保证来自定义不精确闹钟传递的时机。
在特定时间后传递闹钟
如果您的应用调用 set()
、setInexactRepeating()
或 setAndAllowWhileIdle()
,则闹钟永远不会在提供的触发时间之前响起。
在 Android 12(API 级别 31)及更高版本上,除非任何省电限制生效(例如 省电模式 或 Doze),否则系统将在提供的触发时间后一小时内触发闹钟。
在时间窗口内传递闹钟
如果您的应用调用 setWindow()
,则闹钟永远不会在提供的触发时间之前响起。除非任何省电限制生效,否则闹钟将在指定的时间窗口内传递,从给定的触发时间开始。
如果您的应用面向 Android 12 或更高版本,则系统可以将时间窗口化不精确闹钟的触发延迟至少 10 分钟。因此,windowLengthMillis
参数值低于 600000
的值将被剪裁为 600000
。
以大致规则的间隔传递重复闹钟
如果您的应用调用 setInexactRepeating()
,则系统将触发多个闹钟
- 第一个闹钟将在指定的时间窗口内触发,从给定的触发时间开始。
- 后续闹钟通常在指定的时间窗口过去后触发。两次连续触发闹钟之间的时间可能会有所不同。
设置精确闹钟
系统将在未来的某个精确时刻触发精确闹钟。
大多数应用可以使用 不精确闹钟 来计划任务和事件以完成几个 常见用例。如果您的应用的核心功能依赖于精确计时的闹钟(例如闹钟应用或日历应用),则可以使用精确闹钟。
可能不需要精确闹钟的用例
以下列表显示了可能不需要精确闹钟的常见工作流程
- 在应用生命周期内调度定时操作
Handler
类包含一些用于处理定时操作的良好方法,例如在应用处于活动状态时每隔 n 秒执行某些操作:postAtTime()
和postDelayed()
。请注意,这些 API 依赖于系统启动时间,而不是实际时间。- 计划的后台工作,例如更新您的应用和上传日志
WorkManager
提供了一种调度时间敏感的定期工作的方法。您可以提供重复间隔和flexInterval
(最少 15 分钟)来定义工作的粒度运行时间。- 用户指定的在特定时间后应发生的操作(即使系统处于空闲状态)
- 使用非精确闹钟。具体来说,调用
setAndAllowWhileIdle()
。 - 用户指定的在特定时间后应发生的操作
- 使用非精确闹钟。具体来说,调用
set()
。 - 用户指定的可以在指定时间窗口内发生的的操作
- 使用非精确闹钟。具体来说,调用
setWindow()
。请注意,如果您的应用面向 Android 12 或更高版本,则允许的最短窗口长度为 10 分钟。
设置精确闹钟的方法
您的应用可以使用以下方法之一设置精确闹钟。这些方法的排序方式是,列表底部的方法服务于更多时间关键的任务,但需要更多系统资源。
setExact()
在将来几乎精确的时间唤醒闹钟,只要其他省电措施未生效。
除非您的应用的工作对用户来说是时间关键的,否则请使用此方法设置精确闹钟。
setExactAndAllowWhileIdle()
即使省电措施生效,也在将来几乎精确的时间唤醒闹钟。
setAlarmClock()
在将来精确的时间唤醒闹钟。由于这些闹钟对用户高度可见,因此系统绝不会调整其传递时间。系统将这些闹钟识别为最关键的闹钟,并在必要时退出低功耗模式以传递闹钟。
系统资源消耗
当系统触发您的应用设置的精确闹钟时,设备会消耗大量资源,例如电池寿命,尤其是在省电模式下。此外,系统无法轻松地批量处理这些请求以更有效地使用资源。
强烈建议您在任何可能的情况下创建一个非精确闹钟。要执行较长时间的工作,请使用来自闹钟 BroadcastReceiver
的 WorkManager
或 JobScheduler
来安排它。要使设备在 Doze 模式下执行工作,请使用 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 所示。为此,请完成以下步骤
- 在应用的 UI 中,向用户解释您的应用为何需要安排精确闹钟。
- 唤醒包含
ACTION_REQUEST_SCHEDULE_EXACT_ALARM
意图操作的意图。
设置重复闹钟
重复闹钟允许系统根据循环计划通知您的应用。
设计不佳的闹钟会导致电池电量消耗并给服务器带来很大负载。因此,在 Android 4.4(API 级别 19)及更高版本上,所有重复闹钟都是非精确闹钟。
重复闹钟具有以下特征
闹钟类型。有关更多讨论,请参阅选择闹钟类型。
触发时间。如果您指定过去的时间作为触发时间,则闹钟会立即触发。
闹钟的间隔。例如,每天一次、每小时一次或每 5 分钟一次。
在触发闹钟时触发的挂起意图。当您设置第二个使用相同挂起意图的闹钟时,它会替换原始闹钟。
要取消 PendingIntent()
,请将 FLAG_NO_CREATE
传递给 PendingIntent.getService()
以获取意图的实例(如果存在),然后将该意图传递给 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
。
取消闹钟
根据您的应用,您可能希望包含取消闹钟的功能。要取消闹钟,请在 Alarm Manager 上调用 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. } } }
将接收器添加到应用的清单文件中,并使用一个 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>
请注意,在清单文件中,启动接收器设置为
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,该 API 旨在执行后台工作。您可以指示系统应加快您的工作速度,以便工作尽快完成。有关更多信息,请参阅 使用 WorkManager 安排任务
最佳实践
您在设计重复闹钟时做出的每个选择都可能对应用如何使用(或滥用)系统资源产生影响。例如,假设一个流行的应用与服务器同步。如果同步操作基于时钟时间,并且每个应用实例都在晚上 11:00 同步,则服务器上的负载可能会导致高延迟甚至“拒绝服务”。在使用闹钟时,请遵循以下最佳实践
为由重复闹钟触发的任何网络请求添加随机性(抖动)
在闹钟触发时执行任何本地工作。“本地工作”是指任何不访问服务器或不需要服务器数据的工作。
同时,安排包含网络请求的闹钟在某个随机时间段内触发。
将闹钟频率保持在最低限度。
不要不必要地唤醒设备(此行为由闹钟类型决定,如 选择闹钟类型 中所述)。
不要使闹钟的触发时间比必要时更精确。
使用
setInexactRepeating()
而不是setRepeating()
。当您使用setInexactRepeating()
时,Android 会同步来自多个应用的重复闹钟并在同一时间触发它们。这减少了系统必须唤醒设备的总次数,从而减少了对电池的消耗。从 Android 4.4(API 级别 19)开始,所有重复闹钟都是 不精确闹钟。请注意,虽然setInexactRepeating()
比setRepeating()
有所改进,但如果每个应用实例都在同一时间左右访问服务器,它仍然会压垮服务器。因此,对于网络请求,请为您的闹钟添加一些随机性,如前所述。如果可能,请避免基于时钟时间设置闹钟。
基于精确触发时间的重复闹钟扩展性不佳。如果可以,请使用
ELAPSED_REALTIME
。下一部分将更详细地介绍不同的闹钟类型。