> 编写:[jdneo](https://github.com/jdneo) - 原文:[http://developer.android.com/training/scheduling/alarms.html](http://developer.android.com/training/scheduling/alarms.html)
### 教学视频(需翻墙):
- [The App Clinic:Cricket](http://www.youtube.com/watch?v=yxW29JVXCqc)
- [DevBytes:Efficient Data Transfers](https://www.youtube.com/playlist?list=PLWz5rJ2EKKc-VJS9WQlj9xM_ygPopZ-Qd)
闹钟(基于[AlarmManager](https://developer.android.com/reference/android/app/AlarmManager.html)类)给予你一种在应用使用期之外执行与时间相关的操作的方法。你可以使用闹钟初始化一个长时间的操作,例如每天开启一次后台服务,下载当日的天气预报。
闹钟具有如下特性:
- 允许你通过预设时间或者设定某个时间间隔,来触发Intent;
- 你可以将它与BroadcastReceiver相结合,来启动服务并执行其他操作;
- 可在应用范围之外执行,所以你可以在你的应用没有运行或设备处于睡眠状态的情况下,使用它来触发事件或行为;
- 帮助你的应用最小化资源需求,你可以使用闹钟调度你的任务,来替代计时器或者长时间连续运行的后台服务。
> **Note**:对于那些需要确保在应用使用期之内发生的定时操作,可以使用闹钟替代使用[Handler](https://developer.android.com/reference/android/os/Handler.html)结合[Timer](https://developer.android.com/reference/java/util/Timer.html)与[Thread](https://developer.android.com/reference/java/lang/Thread.html)的方法。因为它可以让Android系统更好地统筹系统资源。
### 权衡利弊
重复闹钟的机制比较简单,没有太多的灵活性。它对于你的应用来说或许不是一种最好的选择,特别是当你想要触发网络操作的时候。设计不佳的闹钟会导致电量快速耗尽,而且会对服务端产生巨大的负荷。
当我们从服务端同步数据时,往往会在应用不被使用的时候时触发某些操作。此时你可能希望使用重复闹钟。但是如果存储数据的服务端是由你控制的,使用[Google Cloud Messaging](https://developer.android.com/google/gcm/index.html)(GCM)结合[sync adapter](https://developer.android.com/training/sync-adapters/index.html)是一种更好解决方案。SyncAdapter提供的任务调度选项和[AlarmManager](https://developer.android.com/reference/android/app/AlarmManager.html)基本相同,但是它能提供更多的灵活性。比如:同步的触发可能基于一条“新数据”提示消息,而消息的产生可以基于服务器或设备(祥见[执行Sync Adpater](#)),用户的操作(或者没有操作),每天的某一时刻等等。你可以观看本节课最开始提供的两端视频了解一下有关如何以及何时使用GCM和SyncAdapter的知识。
### 最佳实践方法
在设计重复闹钟过程中,你所做出的每一个决定都有可能影响到你的应用将会如何使用系统资源。例如,我们假想一个会从服务器同步数据的应用。同步操作基于的是时钟时间,具体来说,每一个应用的实例会在下午11十一点整进行同步,巨大的服务器负荷会导致服务器响应时间变长,甚至拒绝服务。因此在我们使用闹钟时,请牢记下面的最佳实践建议:
- 对任何由重复闹钟触发的网络请求添加一定的随机性(抖动):
- 在闹钟触发时做一些本地任务。“本地任务”指的是任何不需要访问服务器或者从服务器获取数据的任务;
- 同时对于那些包含有网络请求的闹钟,在调度时机上增加一些随机性。
- 尽量让你的闹钟频率最小;
- 如果不是必要的情况,不要唤醒设备(这一点与闹钟的类型有关,本节课后续部分会提到);
- 触发闹钟的时间不必过度精确;尽量使用[setInexactRepeating()](https://developer.android.com/reference/android/app/AlarmManager.html#setInexactRepeating(int, long, long, android.app.PendingIntent))方法替代[setRepeating()](https://developer.android.com/reference/android/app/AlarmManager.html#setRepeating(int, long, long, android.app.PendingIntent))方法。当你使用[setInexactRepeating()](https://developer.android.com/reference/android/app/AlarmManager.html#setInexactRepeating(int, long, long, android.app.PendingIntent))方法时,Android系统会集中多个应用的重复闹钟同步请求,并一起触发它们。这可以减少系统将设备唤醒的总次数,以此减少电量消耗。从Android 4.4(API Level19)开始,所有的重复闹钟都将是非精确型的。注意虽然[setInexactRepeating()](https://developer.android.com/reference/android/app/AlarmManager.html#setInexactRepeating(int, long, long, android.app.PendingIntent))是[setRepeating()](https://developer.android.com/reference/android/app/AlarmManager.html#setRepeating(int, long, long, android.app.PendingIntent))的改进版本,它依然可能会导致每一个应用的实例在某一时间段内同时访问服务器,造成服务器负荷过重。因此如之前所述,对于网络请求,我们需要为闹钟的触发时机增加随机性。
- 尽量避免让闹钟基于时钟时间。想要在某一个精确时刻触发重复闹钟是比较困难的。我们应该尽可能使用[ELAPSED_REALTIME](https://developer.android.com/reference/android/app/AlarmManager.html#ELAPSED_REALTIME)。不同的闹钟类型会在本节课后半部分展开。
### 设置重复闹钟
如上所述,对于定期执行的任务或者数据查询而言,使用重复闹钟是一个不错的选择。它具有下列属性:
- 闹钟类型(后续章节中会展开讨论);
- 触发时间。如果触发时间是过去的某个时间点,闹钟会立即被触发;
- 闹钟间隔时间。例如,一天一次,每小时一次,每五秒一次,等等;
- 在闹钟被触发时才被发出的Pending Intent。如果你为同一个Pending Intent设置了另一个闹钟,那么它会将第一个闹钟覆盖。
### 选择闹钟类型
使用重复闹钟要考虑的第一件事情是闹钟的类型。
闹钟类型有两大类:`ELAPSED_REALTIME`和`REAL_TIME_CLOCK`(RTC)。`ELAPSED_REALTIME`从系统启动之后开始计算,`REAL_TIME_CLOCK`使用的是世界统一时间(UTC)。也就是说由于`ELAPSED_REALTIME`不受地区和时区的影响,所以它适合于基于时间差的闹钟(例如一个每过30秒触发一次的闹钟)。`REAL_TIME_CLOCK`适合于那些依赖于地区位置的闹钟。
两种类型的闹钟都还有一个唤醒(`WAKEUP`)版本,也就是可以在设备屏幕关闭的时候唤醒CPU。这可以确保闹钟会在既定的时间被激活,这对于那些实时性要求比较高的应用(比如含有一些对执行时间有要求的操作)来说非常有效。如果你没有使用唤醒版本的闹钟,那么所有的重复闹钟会在下一次设备被唤醒时被激活。
如果你只是简单的希望闹钟在一个特定的时间间隔被激活(例如每半小时一次),那么你可以使用任意一种`ELAPSED_REALTIME`类型的闹钟,通常这会是一个更好的选择。
如果你的闹钟是在每一天的特定时间被激活,那么你可以选择`REAL_TIME_CLOCK`类型的闹钟。不过需要注意的是,这个方法会有一些缺陷——如果地区发生了变化,应用可能无法做出正确的改变;另外,如果用户改变了设备的时间设置,这可能会造成应用产生预期之外的行为。使用`REAL_TIME_CLOCK`类型的闹钟还会有精度的问题,因此我们建议你尽可能使用`ELAPSED_REALTIME`类型。
下面列出闹钟的具体类型:
- [ELAPSED_REALTIME](https://developer.android.com/reference/android/app/AlarmManager.html#ELAPSED_REALTIME):从设备启动之后开始算起,度过了某一段特定时间后,激活Pending Intent,但不会唤醒设备。其中设备睡眠的时间也会包含在内。
- [ELAPSED_REALTIME_WAKEUP](https://developer.android.com/reference/android/app/AlarmManager.html#ELAPSED_REALTIME_WAKEUP):从设备启动之后开始算起,度过了某一段特定时间后唤醒设备。
- [RTC](https://developer.android.com/reference/android/app/AlarmManager.html#RTC):在某一个特定时刻激活Pending Intent,但不会唤醒设备。
- [RTC_WAKEUP](https://developer.android.com/reference/android/app/AlarmManager.html#RTC_WAKEUP):在某一个特定时刻唤醒设备并激活Pending Intent。
### ELAPSED_REALTIME_WAKEUP案例
下面是使用[ELAPSED_REALTIME_WAKEUP](https://developer.android.com/reference/android/app/AlarmManager.html#ELAPSED_REALTIME_WAKEUP)的例子。
每隔在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案例
下面是使用[RTC_WAKEUP](https://developer.android.com/reference/android/app/AlarmManager.html#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分钟唤醒一次:
~~~
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()](https://developer.android.com/reference/android/app/AlarmManager.html#setInexactRepeating(int, long, long, android.app.PendingIntent))会是一个正确的选择。当你使用该方法时,Android系统会集中多个应用的重复闹钟同步请求,并一起触发它们。这样可以减少电量的损耗。
对于另一些实时性要求较高的应用——例如,闹钟需要精确地在上午8点半被激活,并且自此之后每隔1小时激活一次——那么可以使用[setRepeating()](https://developer.android.com/reference/android/app/AlarmManager.html#setRepeating(int, long, long, android.app.PendingIntent))。不过你应该尽量避免使用精确的闹钟。
使用[setRepeating()](https://developer.android.com/reference/android/app/AlarmManager.html#setRepeating(int, long, long, android.app.PendingIntent))时,你可以制定一个自定义的时间间隔,但在使用[setInexactRepeating()](https://developer.android.com/reference/android/app/AlarmManager.html#setInexactRepeating(int, long, long, android.app.PendingIntent))时不支持这么做。此时你只能选择一些时间间隔常量,例如:[INTERVAL_FIFTEEN_MINUTES](https://developer.android.com/reference/android/app/AlarmManager.html#INTERVAL_FIFTEEN_MINUTES) ,[INTERVAL_DAY](http://developer.android.com/reference/android/app/AlarmManager.html#INTERVAL_DAY)等。完整的常量列表,可以查看[AlarmManager](https://developer.android.com/reference/android/app/AlarmManager.html)。
### 取消闹钟
你可能希望在应用中添加取消闹钟的功能。要取消闹钟,可以调用AlarmManager的[cancel()](https://developer.android.com/reference/android/app/AlarmManager.html#cancel(android.app.PendingIntent))方法,并把你不想激活的[PendingIntent](https://developer.android.com/reference/android/app/PendingIntent.html)传递进去,例如:
~~~
// If the alarm has been set, cancel it.
if (alarmMgr!= null) {
alarmMgr.cancel(alarmIntent);
}
~~~
### 在设备启动后启用闹钟
默认情况下,所有的闹钟会在设备关闭时被取消。要防止闹钟被取消,你可以让你的应用在用户重启设备后自动重启一个重复闹钟。这样可以让[AlarmManager](https://developer.android.com/reference/android/app/AlarmManager.html)继续执行它的工作,且不需要用户手动重启闹钟。
具体步骤如下:
1.在应用的Manifest文件中设置[RECEIVE_BOOT_CMPLETED](https://developer.android.com/reference/android/Manifest.permission.html#RECEIVE_BOOT_COMPLETED)权限,这将允许你的应用接收系统启动完成后发出的[ACTION_BOOT)COMPLETED](https://developer.android.com/reference/android/content/Intent.html#ACTION_BOOT_COMPLETED)广播(只有在用户至少将你的应用启动了一次后,这样做才有效):
~~~
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
~~~
2.实现[BoradcastReceiver](https://developer.android.com/reference/android/content/BroadcastReceiver.html)用于接收广播:
~~~
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.在你的Manifest文件中添加一个接收器,其Intent-Filter接收[ACTION_BOOT)COMPLETED](https://developer.android.com/reference/android/content/Intent.html#ACTION_BOOT_COMPLETED)这一Action:
~~~
<receiver android:name=".SampleBootReceiver"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
</intent-filter>
</receiver>
~~~
注意Manifest文件中,对接收器设置了`android:enabled="false"`属性。这意味着除非应用显式地启用它,不然该接收器将不被调用。这可以防止接收器被不必要地调用。你可以像下面这样启动接收器(比如用户设置了一个闹钟):
~~~
ComponentName receiver = new ComponentName(context, SampleBootReceiver.class);
PackageManager pm = context.getPackageManager();
pm.setComponentEnabledSetting(receiver,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
~~~
一旦你像上面那样启动了接收器,它将一直保持启动状态,即使用户重启了设备也不例外。换句话说,通过代码设置的启用配置将会覆盖掉Manifest文件中的现有配置,即使重启也不例外。接收器将保持启动状态,直到你的应用将其禁用。你可以像下面这样禁用接收器(比如用户取消了一个闹钟):
~~~
ComponentName receiver = new ComponentName(context, SampleBootReceiver.class);
PackageManager pm = context.getPackageManager();
pm.setComponentEnabledSetting(receiver,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
~~~
- 序言
- Android入门基础:从这里开始
- 建立第一个App
- 创建Android项目
- 执行Android程序
- 建立简单的用户界面
- 启动其他的Activity
- 添加ActionBar
- 建立ActionBar
- 添加Action按钮
- 自定义ActionBar的风格
- ActionBar的覆盖层叠
- 兼容不同的设备
- 适配不同的语言
- 适配不同的屏幕
- 适配不同的系统版本
- 管理Activity的生命周期
- 启动与销毁Activity
- 暂停与恢复Activity
- 停止与重启Activity
- 重新创建Activity
- 使用Fragment建立动态的UI
- 创建一个Fragment
- 建立灵活动态的UI
- Fragments之间的交互
- 数据保存
- 保存到Preference
- 保存到文件
- 保存到数据库
- 与其他应用的交互
- Intent的发送
- 接收Activity返回的结果
- Intent过滤
- Android分享操作
- 分享简单的数据
- 给其他App发送简单的数据
- 接收从其他App返回的数据
- 给ActionBar增加分享功能
- 分享文件
- 建立文件分享
- 分享文件
- 请求分享一个文件
- 获取文件信息
- 使用NFC分享文件
- 发送文件给其他设备
- 接收其他设备的文件
- Android多媒体
- 管理音频播放
- 控制音量与音频播放
- 管理音频焦点
- 兼容音频输出设备
- 拍照
- 简单的拍照
- 简单的录像
- 控制相机硬件
- 打印
- 打印照片
- 打印HTML文档
- 打印自定义文档
- Android图像与动画
- 高效显示Bitmap
- 高效加载大图
- 非UI线程处理Bitmap
- 缓存Bitmap
- 管理Bitmap的内存
- 在UI上显示Bitmap
- 使用OpenGL ES显示图像
- 建立OpenGL ES的环境
- 定义Shapes
- 绘制Shapes
- 运用投影与相机视图
- 添加移动
- 响应触摸事件
- 添加动画
- View间渐变
- 使用ViewPager实现屏幕侧滑
- 展示卡片翻转动画
- 缩放View
- 布局变更动画
- Android网络连接与云服务
- 无线连接设备
- 使得网络服务可发现
- 使用WiFi建立P2P连接
- 使用WiFi P2P服务
- 执行网络操作
- 连接到网络
- 管理网络
- 解析XML数据
- 高效下载
- 为网络访问更加高效而优化下载
- 最小化更新操作的影响
- 避免下载多余的数据
- 根据网络类型改变下载模式
- 云同步
- 使用备份API
- 使用Google Cloud Messaging
- 解决云同步的保存冲突
- 使用Sync Adapter传输数据
- 创建Stub授权器
- 创建Stub Content Provider
- 创建Sync Adpater
- 执行Sync Adpater
- 使用Volley执行网络数据传输
- 发送简单的网络请求
- 建立请求队列
- 创建标准的网络请求
- 实现自定义的网络请求
- Android联系人与位置信息
- Android联系人信息
- 获取联系人列表
- 获取联系人详情
- 使用Intents修改联系人信息
- 显示联系人头像
- Android位置信息
- 获取最后可知位置
- 获取位置更新
- 显示位置地址
- 创建和监视地理围栏
- Android可穿戴应用
- 赋予Notification可穿戴特性
- 创建Notification
- 在Notifcation中接收语音输入
- 为Notification添加显示页面
- 以Stack的方式显示Notifications
- 创建可穿戴的应用
- 创建并运行可穿戴应用
- 创建自定义的布局
- 添加语音功能
- 打包可穿戴应用
- 通过蓝牙进行调试
- 创建自定义的UI
- 定义Layouts
- 创建Cards
- 创建Lists
- 创建2D-Picker
- 创建确认界面
- 退出全屏的Activity
- 发送并同步数据
- 访问可穿戴数据层
- 同步数据单元
- 传输资源
- 发送与接收消息
- 处理数据层的事件
- Android TV应用
- 创建TV应用
- 创建TV应用的第一步
- 处理TV硬件部分
- 创建TV的布局文件
- 创建TV的导航栏
- 创建TV播放应用
- 创建目录浏览器
- 提供一个Card视图
- 创建详情页
- 显示正在播放卡片
- 帮助用户在TV上探索内容
- TV上的推荐内容
- 使得TV App能够被搜索
- 使用TV应用进行搜索
- 创建TV游戏应用
- 创建TV直播应用
- TV Apps Checklist
- Android企业级应用
- Ensuring Compatibility with Managed Profiles
- Implementing App Restrictions
- Building a Work Policy Controller
- Android交互设计
- 设计高效的导航
- 规划屏幕界面与他们之间的关系
- 为多种大小的屏幕进行规划
- 提供向下和横向导航
- 提供向上和历史导航
- 综合:设计样例 App
- 实现高效的导航
- 使用Tabs创建Swipe视图
- 创建抽屉导航
- 提供向上的导航
- 提供向后的导航
- 实现向下的导航
- 通知提示用户
- 建立Notification
- 当启动Activity时保留导航
- 更新Notification
- 使用BigView风格
- 显示Notification进度
- 增加搜索功能
- 建立搜索界面
- 保存并搜索数据
- 保持向下兼容
- 使得你的App内容可被Google搜索
- 为App内容开启深度链接
- 为索引指定App内容
- Android界面设计
- 为多屏幕设计
- 兼容不同的屏幕大小
- 兼容不同的屏幕密度
- 实现可适应的UI
- 创建自定义View
- 创建自定义的View类
- 实现自定义View的绘制
- 使得View可交互
- 优化自定义View
- 创建向后兼容的UI
- 抽象新的APIs
- 代理至新的APIs
- 使用旧的APIs实现新API的效果
- 使用版本敏感的组件
- 实现辅助功能
- 开发辅助程序
- 开发辅助服务
- 管理系统UI
- 淡化系统Bar
- 隐藏系统Bar
- 隐藏导航Bar
- 全屏沉浸式应用
- 响应UI可见性的变化
- 创建使用Material Design的应用
- 开始使用Material Design
- 使用Material的主题
- 创建Lists与Cards
- 定义Shadows与Clipping视图
- 使用Drawables
- 自定义动画
- 维护兼容性
- Android用户输入
- 使用触摸手势
- 检测常用的手势
- 跟踪手势移动
- Scroll手势动画
- 处理多触摸手势
- 拖拽与缩放
- 管理ViewGroup中的触摸事件
- 处理键盘输入
- 指定输入法类型
- 处理输入法可见性
- 兼容键盘导航
- 处理按键动作
- 兼容游戏控制器
- 处理控制器输入动作
- 支持不同的Android系统版本
- 支持多个控制器
- Android后台任务
- 在IntentService中执行后台任务
- 创建IntentService
- 发送工作任务到IntentService
- 报告后台任务执行状态
- 使用CursorLoader在后台加载数据
- 使用CursorLoader执行查询任务
- 处理查询的结果
- 管理设备的唤醒状态
- 保持设备的唤醒
- 制定重复定时的任务
- Android性能优化
- 管理应用的内存
- 代码性能优化建议
- 提升Layout的性能
- 优化layout的层级
- 使用include标签重用layouts
- 按需加载视图
- 使得ListView滑动顺畅
- 优化电池寿命
- 监测电量与充电状态
- 判断与监测Docking状态
- 判断与监测网络连接状态
- 根据需要操作Broadcast接受者
- 多线程操作
- 在一个线程中执行一段特定的代码
- 为多线程创建线程池
- 启动与停止线程池中的线程
- 与UI线程通信
- 避免出现程序无响应ANR
- JNI使用指南
- 优化多核处理器(SMP)下的Android程序
- Android安全与隐私
- Security Tips
- 使用HTTPS与SSL
- 为防止SSL漏洞而更新Security
- 使用设备管理条例增强安全性
- Android测试程序
- 测试你的Activity
- 建立测试环境
- 创建与执行测试用例
- 测试UI组件
- 创建单元测试
- 创建功能测试
- 術語表