Android NDK开发详解闹铃之设置重复闹铃时间

管理设备唤醒状态

当 Android 设备空闲时,它会首先调暗屏幕,然后关闭屏幕,最终关闭 CPU。这可以防止设备的电池电量快速耗尽。不过,有时您的应用可能需要采取不同的行为:

游戏或电影应用等应用可能需要使屏幕保持开启状态。
其他应用可能不需要屏幕始终处于开启状态,但可能需要 CPU 持续运行,直到某项关键操作完成。
以下课程介绍了如何在必要时使设备保持唤醒状态,而不大量消耗电池电量。

课程

使设备保持唤醒状态
了解如何根据需要使屏幕或 CPU 保持唤醒状态,同时最大限度减少对电池续航时间的影响。
设置重复闹铃时间
了解如何使用重复闹钟来安排在应用生命周期之外定期执行操作(即使应用未运行且/或设备处于休眠状态)。

设置重复闹铃时间

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

闹钟具有以下特征:

它们可让您按设定的时间和/或间隔触发 intent。
您可以将它们与广播接收器结合使用,以启动服务以及执行其他操作。
它们在应用外部运行,因此即使应用未运行,或设备本身处于休眠状态,您也可以使用它们来触发事件或操作。
它们可以帮助您最大限度地降低应用的资源要求。您可以安排定期执行操作,而无需依赖定时器或持续运行后台服务。
注意:对于肯定会在应用生命周期内发生的定时操作,请考虑将 Handler 类与 Timer 和 Thread 结合使用。该方法可让 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);
    }

注意:如果 PendingIntent 是使用 FLAG_ONE_SHOT 创建的,就无法将其取消。

选择闹钟类型

使用重复闹钟时的首要考虑因素之一是,它应该是哪种类型。

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

本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。

最后更新时间 (UTC):2020-06-20。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

五一编程

程序之路有我与你同行

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值