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。