设置重复闹铃时间

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

闹钟具有以下特征:

  • 它们可让您按设定的时间和/或间隔触发 intent。
  • 您可以将它们与广播接收器结合使用,以启动服务以及执行其他操作。
  • 它们在应用外部运行,因此即使应用未运行,或设备本身处于休眠状态,您也可以使用它们来触发事件或操作。
  • 它们可以帮助您最大限度地降低应用的资源要求。您可以安排定期执行操作,而无需依赖定时器或持续运行后台服务。

注意:对于肯定会在应用生命周期内发生的定时操作,请考虑将 Handler 类与 TimerThread 结合使用。该方法可让 Android 更好地控制系统资源。

了解利弊

重复闹钟是一种相对简单的机制,灵活性有限。对于您的应用而言,它可能并不是理想的选择,尤其是当您需要触发网络操作时。设计不合理的闹钟会导致耗电过度,并会使服务器负载显著增加。

在应用生命周期之外触发操作的一种常见情形是与服务器同步数据。在这种情况下,您可能会想要使用重复闹钟。但是,如果您拥有托管应用数据的服务器,那么与 AlarmManager 相比,结合使用 Google 云消息传递 (GCM) 与同步适配器是更好的解决方案。同步适配器可为您提供与 AlarmManager 完全相同的时间安排选项,但灵活性要高得多。例如,同步操作可以基于来自服务器/设备的“新数据”消息(如需了解详情,请参阅运行同步适配器)、用户的活动状态(或非活动状态)、一天当中的时段等等。有关何时以及如何使用 GCM 和同步适配器的详细讨论,请参阅此页面顶部链接的视频。

当设备在低电耗模式下处于空闲状态时,不会触发闹钟。所有已设置的闹钟都会推迟,直到设备退出低电耗模式。如果您需要确保即使设备处于空闲状态您的工作也会完成,可以通过多种选项实现这一目的。您可以使用 setAndAllowWhileIdle()setExactAndAllowWhileIdle() 确保闹钟会执行。另一个选项是使用新的 WorkManager API,它可用于执行一次性或周期性的后台工作。如需了解详情,请参阅使用 WorkManager 安排任务

最佳做法

您在设计重复闹钟时所做的每一个选择都会对应用使用(或滥用)系统资源的方式产生影响。例如,假设有一个热门应用会与服务器同步。如果同步操作基于时钟时间,并且应用的每个实例都会在晚上 11:00 进行同步,那么服务器上的负载可能会导致高延迟,甚至是“拒绝服务”。请遵循以下使用闹钟的最佳做法:

  • 为重复闹钟触发的网络请求加入一些随机性(抖动):
    • 在闹钟触发时执行本地工作。“本地工作”是指无需连接至服务器或使用来自服务器的数据的任何工作。
    • 同时,将包含网络请求的闹钟设置为在某个随机时间段内触发。
  • 尽可能降低闹钟的触发频率。
  • 请勿在不必要的情况下唤醒设备(该行为取决于闹钟类型,如选择闹钟类型中所述)。
  • 请勿将闹钟的触发时间设置得过于精确。

    使用 setInexactRepeating() 代替 setRepeating()。当您使用 setInexactRepeating() 时,Android 会同步来自多个应用的重复闹钟,并同时触发它们。这可以减少系统必须唤醒设备的总次数,从而减少耗电量。从 Android 4.4(API 级别 19)开始,所有重复闹钟都是不精确的。请注意,尽管 setInexactRepeating() 是对 setRepeating() 的改进,但如果应用的每个实例都大约在同一时间连接至服务器,仍会使服务器不堪重负。因此,如上所述,对于网络请求,请为您的闹钟添加一些随机性。

  • 尽量避免基于时钟时间设置闹钟。.

    基于精确触发时间的重复闹钟无法很好地扩展。如果可以的话,请使用 ELAPSED_REALTIME。下一部分详细介绍了不同的闹钟类型。

设置重复闹钟

如上所述,对于安排常规的事件或数据查询而言,重复闹钟是一个不错的选择。重复闹钟具有以下特征:

  • 闹钟类型。要了解详情,请参阅选择闹钟类型
  • 触发时间。如果您指定的触发时间为过去的时间,则闹钟会立即触发。
  • 闹钟的间隔。例如,每天一次、每小时一次、每 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 会同步多个不精确的重复闹钟,并同时触发它们。这样可以减少耗电量。

对于具有严格时间要求的少数应用(例如,闹钟需要在上午 8:30 准时触发,并在此后每小时准点触发一次),请使用 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)中引入了低电耗模式和应用待机模式。当设备处于低电耗模式时,所有标准闹钟都会推迟,直到设备退出低电耗模式或维护期开始。如果必须让某个闹钟在低电耗模式下也能触发,可以使用 setAndAllowWhileIdle()setExactAndAllowWhileIdle()。您的应用将在处于空闲状态时(即用户在一段时间内未使用应用,并且应用没有前台进程时)进入应用待机模式。当应用处于应用待机模式时,闹钟会像设备处于低电耗模式一样被延迟。当应用不再处于空闲状态或者当设备接通电源时,该限制便会解除。如需详细了解这两种模式对应用的影响,请参阅对低电耗模式和应用待机模式进行针对性优化