计划闹钟

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

闹钟具有以下特点

  • 它们允许您在设定的时间和/或间隔触发 Intent。

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

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

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

设置不精确闹钟

当应用设置不精确闹钟时,系统将在将来某个时间传递闹钟。不精确闹钟提供了一些关于闹钟传递时机的保证,同时尊重电池节电限制,例如 Doze

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

在特定时间后传递闹钟

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

在 Android 12(API 级别 31)及更高版本上,系统将在提供的触发时间的一小时内调用闹钟,除非任何电池节电限制生效,例如 省电模式Doze

在时间窗口内传递闹钟

如果您的应用调用 setWindow(),则闹钟绝不会在提供的触发时间之前触发。除非任何电池节电限制生效,否则闹钟将在指定的时间窗口内传递,从给定的触发时间开始。

如果您的应用面向 Android 12 或更高版本,系统可能会将时间窗口不精确警报的调用延迟至少 10 分钟。因此,小于 600000windowLengthMillis 参数值会被截断为 600000

以大致规则的间隔传递重复警报

如果您的应用调用 setInexactRepeating(),系统会调用多个警报

  1. 第一个警报会在指定的时间窗口内触发,从给定的触发时间开始。
  2. 后续警报通常会在指定的时间窗口过去后触发。两次连续调用警报之间的时间可能会有所不同。

设置精确警报

系统会在将来的某个精确时刻触发精确警报

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

可能不需要精确警报的用例

以下列表显示了可能不需要精确警报的常见工作流程

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

设置精确警报的方法

您的应用可以使用以下方法之一设置精确警报。这些方法按顺序排列,列表底部的方法服务于更关键的时间任务,但需要更多系统资源。

setExact()

在将来触发一个几乎精确时间的警报,只要其他省电措施未生效。

除非您的应用工作对用户至关重要,否则请使用此方法设置精确警报。

setExactAndAllowWhileIdle()

在将来触发一个几乎精确时间的警报,即使省电措施生效。

setAlarmClock()

在将来某个精确时间触发警报。由于这些警报对用户高度可见,因此系统绝不会调整它们的传递时间。系统将这些警报识别为最关键的警报,并在必要时退出低功耗模式以传递警报。

系统资源消耗

当系统触发您的应用设置的精确警报时,设备会消耗大量资源,例如电池寿命,尤其是在省电模式下。此外,系统无法轻松地批量处理这些请求以更有效地使用资源。

强烈建议您尽可能创建 不精确警报。要执行较长时间的工作,请使用警报的 BroadcastReceiver 中的 WorkManagerJobScheduler 安排它。要在设备处于 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_ALARMUSE_EXACT_ALARM 权限。

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

虽然 SCHEDULE_EXACT_ALARMUSE_EXACT_ALARM 权限都表示相同的功能,但它们的授予方式和支持的用例不同。您的应用应仅在用户界面功能需要精确计时的操作时使用精确警报,并声明 SCHEDULE_EXACT_ALARMUSE_EXACT_ALARM 权限。

USE_EXACT_ALARM

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

  1. 确认您的应用是否仍拥有特殊应用访问权限。为此,请调用 canScheduleExactAlarms()。此检查可以防止用户授予您的应用权限,然后几乎立即撤销权限的情况。
  2. 根据应用的当前状态重新安排应用所需的任何精确警报。此逻辑应类似于应用在收到 ACTION_BOOT_COMPLETED 广播时所执行的操作。

要求用户授予 SCHEDULE_EXACT_ALARM 权限

The option is called 'Allow setting alarms and reminders'
图 1. 系统设置中的“闹钟和提醒”特殊应用访问页面,用户可以在其中允许您的应用设置精确警报。

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

  1. 在应用的 UI 中,向用户解释您的应用为何需要安排精确警报。
  2. 调用包含 ACTION_REQUEST_SCHEDULE_EXACT_ALARM 意图操作的意图。

设置重复警报

重复警报允许系统按照重复计划通知您的应用。

设计不佳的警报会导致电池电量消耗并给服务器带来沉重的负载。因此,在 Android 4.4(API 级别 19)及更高版本上,所有重复警报都是 不精确警报

重复闹钟具有以下特征

  • 闹钟类型。有关更多讨论,请参阅选择闹钟类型

  • 触发时间。如果您指定的时间已过去,则闹钟会立即触发。

  • 闹钟的间隔。例如,每天一次、每小时一次或每 5 分钟一次。

  • 闹钟触发时触发的挂起意图。当您设置第二个使用相同挂起意图的闹钟时,它会替换原始闹钟。

要取消PendingIntent(),请传递FLAG_NO_CREATEPendingIntent.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:根据设备自启动以来的时间量触发挂起意图,但不唤醒设备。经过的时间包括设备处于睡眠状态的任何时间。

  • ELAPSED_REALTIME_WAKEUP:唤醒设备并在设备启动后经过指定时间长度后触发挂起意图。

  • RTC:在指定时间触发挂起意图,但不唤醒设备。

  • RTC_WAKEUP:唤醒设备以在指定时间触发挂起意图。

经过的真实时间闹钟示例

以下是一些使用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_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. 将接收器添加到应用的清单文件中,并使用 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。下一节将更详细地描述不同的闹钟类型。