Scheduling Repeating Alarms -- 设置重复闹钟

闹钟(基于AlarmManager类)给你提供一种跳出app生命周期之外的一种操作基于时间的行为的一种方式。例如,你可以用闹钟来初始化一个长时间运行的操作,如每天启动一个service来下载天气预报数据。

闹钟有如下特性:

  • 闹钟帮你在特定的时间或者特定的时间间隔后启动Intent
  • 你可以把闹钟和Broadcast Receiver结合起来启动Service,并执行其他操作
  • 闹钟的操作是游离于你的app之外的,因此你可以在app没有启动、甚至在设备睡眠时,通过闹钟来触发你app中的事件或这动作
  • 闹钟可以最小化app对于系统资源的需求。你可以在不依赖timer或者持续运行的后台service来执行一些计划任务

NOte: 对于确定发生在app的生命周期内的定时操作,尽量考虑使用Handler类结合TimerThread来实现。这种行为赋予Android对于系统资源更好的控制。

Understand the Trade-offs – 理解得失

一个重复的闹钟是一种具有有限灵活性的简单机制。它或许不是你app的最好选择,特别是你需要触发网络操作的时候。一个设计不当的闹钟会导致电量剧烈消耗,以及显著增加服务器端负载。

一个常见的需要在你的app的生命周期之外触发的操作,是从服务器同步数据。这个时候,你应该使用重复闹钟来实现。不过,如果你要同步数据的服务器是你自己持有的,Google Cloud Messaging(GCM)配合sync adapter是比AlarmManager更好的一种解决方案。sync adapter可以为你提供AlarmManager所有的功能,且更加灵活。例如,一个同步操作可能是由一个“要求”更新数据的消息触发的,这个消息可能来自服务器,或者来自设备本身:如用户操作Activity,设备上的时间等等(see Running a Sync Adapter for details)。

Best practices – 最佳实践

你在设计你的重复闹钟时做的每一个决定,都会影响到你的app如何使用、甚至是滥用系统资源。例如,一款流行应用需要和服务器同步数据。如果这个同步操作是基于闹钟来触发的,并且每个用户都在晚上11点同步数据,那么这个时候,服务器的负载就会很高,甚至导致服务器宕机。下面是使用重复闹钟时的一些最近实践:

  • 对于由重复闹钟触发的网络请求,请在时间上的添加一些随机性或者抖动。

    • 被重复闹钟触发后,做一些本地操作。本地操作是指无需访问服务器或从服务器获取数据的操作。
    • 与此同时,对于包含网络请求的重复闹钟,请在重复时间上添加一些随机变量。
  • 把你闹钟的频率降到最低

  • 只在最需要的时候唤醒设备(这个行为是闹钟的type属性决定的,在Choose an alarm type有描述)

  • 如非必要,无需把你的闹钟间隔设置的那么精确。

    使用setInexactRepeating()来代替setRepeating()。当你使用setInexactRepeating()时,Android系统会同步多个应用中的重复闹钟,在一个统一的时候欢喜这些闹钟。这样会减少系统必要的唤醒次数,降低电池电量的消耗。在Android4.4(API 19)中,所有的重复闹钟都是不精确的。注意一点,尽管setInexactRepeating()相对于setRepeating()来说是一个提升,但是每个用户上的app在同一个时间点附近集中访问服务器,同样会压垮服务器。因此,对于涉及网络请求的重复闹钟,在时间上增加随机性,就像上面提到的。

  • 避免使用时钟时间来设置重复闹钟

    基于精确时间的闹钟工作并不良好。尽可能用ELAPSED_REALTIME。不同的闹钟类型会在下面详细描述。

Set a Repeating Alarm – 设置一个重复闹钟

就像上面说的,在重复且有规律的执行事件或获取数据时,重复闹钟是一个很好的选择。一个重复闹钟有如下几个特性:

  • 闹钟类型。更多的讨论请看Choose an alarm type

  • 触发时间。如果你设定的触发时间是在过去,那个这个闹钟会被立即触发。

  • 时间间隔。例如每天、每小时、每5秒,等等。

  • 一个在闹钟触发后触发的Pending Intent。如果你用相同的Pending Intent设置了第二个闹钟,则前一个就会被取代。

Choose an alarm type – 选择闹钟类型 Choose an alarm type

使用重复闹钟时,第一个需要考虑到的问题就是它的类型。

闹钟大致有两种时钟类型:elapsed real time and real time clock (RTC)Elapsed real time 把自从系统启动后的时间作为参考,而RTC用UTC时间。这意味着elapsed real time适用于设置一个基于过去多少时间的闹钟(例如,每隔30s唤醒一次的闹钟),它不受时区和位置的影响。而RTC适用于基于当前时区或位置的闹钟。

这两张类型都有一个带“唤醒”的版本,也就是说在屏幕熄灭后依然可以唤醒CPU。这样确保了闹钟会在预定的事件被触发。这对于你的app有一个依赖时间的操作很有用–例如你的app有一个有限的窗口来执行以下特定操作。如果你没有使用可唤醒的闹钟类型,则所有的重复闹钟会在下次你的设备被唤醒的时候,一次性全部被触发。

如果你只是需要一个需要在特定时间间隔后唤醒的闹钟(例如,每隔半小时),请使用elapsed real time类型的闹钟。通常状况下,这是一个比较好的选择。

如果你需要你的闹钟在一天中的特定时间唤醒,请使用RTC类型的闹钟。记住,无论如何,这里有一些问题–app无法在切换到其他时区是良好运行,假如用户改变了设备的时间设置,就会在app中引起不可预知的行为。使用RTC类型的闹钟就像上面讨论的那样,同样无法适应的很好。我们强烈建议你在可能的情况下尽量使用elapsed real time类型的闹钟。

下面列出了所有的类型:

  • ELAPSED_REALTIME—在一段时间后,触发pending intent,但是不会唤醒设备。这个时间会把设备休眠的时间也统计进来。

  • ELAPSED_REALTIME_WAKEUP—在设备启动后过去了多长时间后,唤醒设备并发出pending intent。

  • RTC—在特定的时间触发pending intent,但是不会唤醒设备。

  • RTC_WAKEUP—在特定时间,唤醒设备并触发pending intent。

ELAPSED_REALTIME_WAKEUP examples

这有一些使用ELAPSED_REALTIME_WAKEUP的例子。

30分钟内唤醒设备并触发闹钟,之后每隔30分钟触发一次闹钟:

// Hopefully your alarm will have a lower frequency than this!
alarmMgr.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
        AlarmManager.INTERVAL_HALF_HOUR,
        AlarmManager.INTERVAL_HALF_HOUR, alarmIntent);

一分钟内唤醒设备并触发一个一次性的闹钟:

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 examples

这里有一些使用RTC_WAKEUP的例子。

在大约下午2点的时候,唤醒设备并触发这个闹钟,之后每天在同样的时间重复触发一次这个闹钟:

// 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点20分的时候准时唤醒设备,之后每隔20分钟重复一次:

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);

Decide how precise your alarm needs to be – 确定你的闹钟的精确度

如上所述,创建闹钟的第一步是选择闹钟类型。更近一步则是选择闹钟的精确度。对于大多数应用来说,setInexactRepeating()是一个正确选择。当你使用这个方法时,Android系统会同步多个不精确的重复闹钟,并在一个统一的时间触发他们。这样会减少电池电量的消耗。

对于少数有精确时间要求的app来说–例如,闹钟需要在上午8点20准时被触发,并且在之后每个小时重复一次–请使用setRepeating()。不过,你应该在可能的情况下尽量少使用精确闹钟。

使用setInexactRepeating()的时候,你无法像使用setRepeating()一样设置自定义的时间间隔。你只能使用一些时间间隔常量,如INTERVAL_FIFTEEN_MINUTES, INTERVAL_DAY,等等。如下表所示:

类型名字作用
StringACTION_NEXT_ALARM_CLOCK_CHANGEDBroadcast Action: Sent after the value returned by getNextAlarmClock() has changed.
intELAPSED_REALTIMEAlarm time in SystemClock.elapsedRealtime() (time since boot, including sleep).
intELAPSED_REALTIME_WAKEUPAlarm time in SystemClock.elapsedRealtime() (time since boot, including sleep), which will wake up the device when it goes off.
longINTERVAL_DAYAvailable inexact recurrence interval recognized by setInexactRepeating(int, long, long, PendingIntent) when running on Android prior to API 19.
longINTERVAL_FIFTEEN_MINUTESAvailable inexact recurrence interval recognized by setInexactRepeating(int, long, long, PendingIntent) when running on Android prior to API 19.
longINTERVAL_HALF_DAYAvailable inexact recurrence interval recognized by setInexactRepeating(int, long, long, PendingIntent) when running on Android prior to API 19.
longINTERVAL_HALF_HOURAvailable inexact recurrence interval recognized by setInexactRepeating(int, long, long, PendingIntent) when running on Android prior to API 19.
longINTERVAL_HOURAvailable inexact recurrence interval recognized by setInexactRepeating(int, long, long, PendingIntent) when running on Android prior to API 19.
intRTCAlarm time in System.currentTimeMillis() (wall clock time in UTC).
intRTC_WAKEUPAlarm time in System.currentTimeMillis() (wall clock time in UTC), which will wake up the device when it goes off.

Cancel an Alarm – 取消一个闹钟

在你的app中,你也许想加上取消闹钟的功能。为了取消一个闹钟,你需要使用AlarmManager调用cancel()方法,并把你不想再被触发的PendingIntent实例传到这个方法里。如下:

// If the alarm has been set, cancel it.
if (alarmMgr!= null) {
    alarmMgr.cancel(alarmIntent);
}

Start an Alarm When the Device Boots – 在设备启动的时候开启一个闹钟

默认情况下,当设备关闭的时候,所有的闹钟都会被关闭。为了防止这种情况发生,你需要让你的app在用户重启设备后自动重启重复闹钟。这样可以确保AlarmManager在无需用户手动重启闹钟的情况下,继续执行任务。

下面是具体步骤:
1. 在清单文件中,设置RECEIVE_BOOT_COMPLETED权限。这样可以确保你的app可以收到系统在完成启动时发出的ACTION_BOOT_COMPLETED广播(这个机制只会在用户已经启动过应用的情况下才能正常工作)。

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
  1. 实现一个BroadcastReceiver来接收开机广播。
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.
        }
    }
}
  1. 把这个receiver添加到app的清单文件中,并给这个receiver添加一个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>
  1. 请注意一点,在清单文件中,这个启动receiver的属性android:enabled="false"。这意味着这个receiver只有在app中显式的打开它,它才能收到开机广播。这样可以避免这个receiver被不必要的调用。你可以像下面一样打开一个receiver(例子中,假如用户设置了一个闹钟):
ComponentName receiver = new ComponentName(context, SampleBootReceiver.class);
PackageManager pm = context.getPackageManager();

pm.setComponentEnabledSetting(receiver,
        PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
        PackageManager.DONT_KILL_APP);

一旦你用这种方式打开了receiver,即使用户重启了设备,它还会保持打开状态。也就是说,代码中打开receiver,覆盖了清单文件中的设定,即使重启设备也不会被还原。这个receiver会一直保持开启状态,直至app关闭它。你可以像下面一样关闭一个receiver(例子中,假如用户设置了一个闹钟):

ComponentName receiver = new ComponentName(context, SampleBootReceiver.class);
PackageManager pm = context.getPackageManager();

pm.setComponentEnabledSetting(receiver,
        PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
        PackageManager.DONT_KILL_APP);

全篇完~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值