设置闹钟

闹钟(基于 AlarmManager 类)为您提供了一种在应用生命周期之外执行基于时间的操作的方法。例如,您可以使用闹钟启动长时间运行的操作,如每天启动一次服务以下载天气预报。

闹钟具有以下特征:

  • 它们可让您按设定的时间和/或间隔触发 intent。

  • 您可以将它们与广播接收器结合使用,以安排作业WorkRequest 以执行其他操作。

  • 它们在应用之外运行,因此即使您的应用未运行,甚至设备本身处于休眠状态,您也可以使用它们触发事件或操作。

  • 它们可以帮助您最大限度地减少应用的资源需求。您可以安排操作,而无需依赖计时器或持续运行服务。

设置不精确闹钟

如果应用设置了不精确闹钟,系统会在未来的某个时间点发出闹钟。不精确闹钟可在遵循低电耗模式等省电限制的同时,保证闹钟的发送时间。

开发者可以利用以下 API 保证来自定义不精确闹钟的发送时间。

在特定时间后发出闹钟

如果您的应用调用 set()setInexactRepeating()setAndAllowWhileIdle(),则闹钟绝不会在提供的触发时间之前响起。

在 Android 12(API 级别 31)及更高版本中,除非已实施任何节电限制(例如省电模式低电耗模式),否则系统会在提供的触发时间后的 1 小时内调用闹钟。

在特定时间范围内触发闹钟

如果您的应用调用 setWindow(),则闹钟绝不会在所提供的触发时间之前响起。除非任何省电限制生效,否则闹钟会在指定的时间范围内(从指定的触发时间开始)内发出。

如果您的应用以 Android 12 或更高版本为目标平台,则系统可以将对带时间范围的不精确闹钟的调用延迟至少 10 分钟。因此,600000 下的 windowLengthMillis 参数值会被裁剪为 600000

以大致有规律的时间间隔响起重复闹钟

如果您的应用调用 setInexactRepeating(),系统会调用多个闹钟:

  1. 第一个闹钟会在指定的时间范围内触发,从指定的触发时间开始。
  2. 后续闹钟通常会在指定时间范围过后响起。两次连续的闹铃调用的间隔时间可能会有所不同。

设置精确的闹钟

系统会在未来的某个精确时刻调用精确闹钟。

大多数应用都可以使用不精确闹钟来调度任务和事件,以完成几个常见用例。如果应用的核心功能依赖于精确计时的闹钟(例如闹钟应用或日历应用),则可以改用精确闹钟。

可能不需要精确闹钟的用例

以下列表显示了可能不需要精确闹钟的常见工作流:

在应用的生命周期内调度计时操作
Handler 类包含几种处理计时操作的好方法,例如在应用处于活动状态时,每 n 秒执行一次工作:postAtTime()postDelayed()请注意,这些 API 依赖于系统正常运行时间,而不是实时
安排好的后台工作,例如更新应用和上传日志
WorkManager 提供了一种安排时间敏感型定期工作的方法。您可以提供重复间隔和 flexInterval(至少 15 分钟),以定义工作的精细运行时间。
应在特定时间过后执行的用户指定操作(即使系统处于空闲状态)
使用不精确闹钟。具体来说,就是调用 setAndAllowWhileIdle()
应在特定时间过后执行的用户指定操作
使用不精确闹钟。具体来说,就是调用 set()
可在指定时间范围内执行的用户指定操作
使用不精确闹钟。具体来说,就是调用 setWindow()。请注意,如果应用以 Android 12 或更高版本为目标平台,则允许的最短窗口时长为 10 分钟。

设置精确闹钟的方法

您的应用可以使用以下方法之一设置精确闹钟。这些方法按顺序排列,因此更靠近列表底部的方法可处理对时间更重要的任务,但需要更多系统资源。

setExact()

只要其他省电措施没有生效,在未来一个近乎精确的时间就会触发闹钟。

除非应用的工作对用户来说非常关键,否则使用此方法设置精确的闹钟。

setExactAndAllowWhileIdle()

在未来近乎精确的时间调用闹钟,即使采用了省电措施也是如此。

setAlarmClock()

在未来某个精确时间启动闹钟。由于用户非常容易看到这些闹钟,因此系统绝不会调整其触发时间。系统会将这些闹钟识别为最关键的闹钟,并在必要时关闭低功耗模式以实现闹钟。

系统资源消耗

当系统触发您的应用设置的确切闹钟时,设备会消耗大量的资源(如电池续航时间),尤其是在设备处于节能模式时。此外,系统无法轻松地对这些请求进行批处理,以便更高效地使用资源。

强烈建议您尽可能创建不精确闹钟。如需执行时间更长的工作,请使用闹钟 BroadcastReceiver 中的 WorkManagerJobScheduler 进行调度。如需在设备处于低电耗模式时执行工作,请使用 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_ALARMUSE_EXACT_ALARM 权限。

<manifest ...>
    <uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
    <application ...>
        ...
    </application>
</manifest>

虽然 SCHEDULE_EXACT_ALARM 权限和 USE_EXACT_ALARM 权限表示的功能相同,但权限授予方式不同,支持不同的用例。仅当应用中面向用户的功能需要精确计时的操作时,您的应用才应使用精确闹钟,并声明 SCHEDULE_EXACT_ALARMUSE_EXACT_ALARM 权限。

USE_EXACT_ALARM

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 广播。您的应用应实现广播接收器,以便执行以下操作:

  1. 确认您的应用仍具有特殊应用访问权限。为此,请调用 canScheduleExactAlarms()。此检查可防止您的应用出现以下情况:用户授予应用该权限,然后几乎立即撤消该权限。
  2. 根据应用的当前状态,重新调度应用需要的任何精确的闹钟。此逻辑应与您的应用接收 ACTION_BOOT_COMPLETED 广播时所执行的操作类似。

要求用户授予 SCHEDULE_EXACT_ALARM 权限

该选项称为“允许设置闹钟和提醒”
图 1. 系统设置中的“闹钟和提醒”特殊应用访问权限页面,用户可以在该页面中允许您的应用设置精确的闹钟。

如有必要,您可以将用户转到系统设置中的闹钟和提醒屏幕,如图 1 所示。为此,请完成以下步骤:

  1. 在应用的界面中,向用户解释为什么您的应用需要调度精确的闹钟。
  2. 调用包含 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 点左右唤醒设备并触发闹钟,并在每天的同一时间重复一次:

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_MINUTESINTERVAL_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 继续执行其任务,而无需用户手动重启闹钟。

具体步骤如下所示:

  1. 在应用清单中设置 RECEIVE_BOOT_COMPLETED 权限。这样一来,您的应用便可以接收系统完成启动后广播的 ACTION_BOOT_COMPLETED(此方法仅适用于用户已至少启动过应用一次的情况):

    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
  2. 实现 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.
            }
        }
    }
    
  3. 使用根据 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。下一部分详细介绍了不同的闹钟类型。