android官方文档:AlarmManager
在Android平台,除了使用AlarmManger外,还可以使用Timer或者Handler来实现定时任务,但这两种方式定时并不会太准确;因此如果我们需要实现精准定时任务,使用AlarmManger更为合理。
AlarmManager类提供对系统闹钟服务(或称为定时器服务)的访问接口,使用它既可以指定单次执行的定时任务,也可以指定重复运行的任务。
当闹钟指定触发时间到达时,实际上是系统发出为这个闹钟注册的广播,因此我们需要实现一个针对特定闹钟事件的广播接口器。
从API 19开始,alarm的机制都是非准确传递,操作系统将会转换闹钟,来最小化唤醒和电池使用。
Note: Beginning with API 19 (KITKAT) alarm delivery is inexact: the OS will shift alarms in order to minimize wakeups and battery use. There are new APIs to support applications which need strict delivery guarantees; see setWindow(int, long, long, PendingIntent) and setExact(int, long, PendingIntent). Applications whose targetSdkVersion is earlier than API 19 will continue to see the previous behavior in which all alarms are delivered exactly when requested.
本文中的代码使用Kotlin来编写,Kotlin是JVM上的一种编程语言,可以与Java无缝对接,由JetBrains公司研发,官方介绍:
Statically typed programming language for the JVM, Android and the browser
100% interoperable with Java™
刚实现定时任务我就写了本文,因此对这方面的知识并没有完全掌握,文中可能会有错误,有机会还会更新。
本文没有将完整的示例代码贴出来,只是贴了关键代码,主要是闹钟的设置和取消,以及与Settings部分的交互。
首先定义几个常量,包括用于Settings部分的key、用于PendingIntent的requestCode和用于Intent中的action,代码如下:
const val ACTION_ALARM_REPLENISH_STOCK = "witmoon.auto.replenish.stock.action"
const val ACTION_ALARM_SYNCHRONIZE = "witmoon.auto.synchronize.action"
const val ALARM_REPLENISH_STOCK_CODE = 11
const val ALARM_SYNCHRONIZE_CODE = 12
const val KEY_SETTING_AUTO_SYNCHRONIZE = "auto_synchronize_status"
const val KEY_SETTING_SYNCHRONIZE_TIME = "auto_synchronize_time"
const val KEY_SETTING_AUTO_SUBMIT = "auto_submit_status"
const val KEY_SETTING_AUTO_SUBMIT_TIME = "auto_submit_time"
接下来是闹钟事件广播接收器,作为示例代码非常简单,收到广播事件后首先判断action,根据不同的action使用Toast打印出不同的内容:
/**
* 闹钟广播接收器
* Created by chuxin on 2016/5/4.
*/
class AlarmBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
var action = intent?.action
when {
ACTION_ALARM_SYNCHRONIZE.equals(action) -> doSynchronizeAction(context)
ACTION_ALARM_REPLENISH_STOCK.equals(action) -> doReplenishStockAction(context)
}
}
private fun doSynchronizeAction(context: Context?) {
Toast.makeText(context!!, "同步", Toast.LENGTH_SHORT).show()
}
/**
* 执行补充库存动作, 即下单/定货
*/
private fun doReplenishStockAction(context: Context?) {
Toast.makeText(context!!, "定货", Toast.LENGTH_SHORT).show()
}
}
不要忘记在AndroidManifest.xml文件中注册接收器:
<receiver android:name=".receiver.AlarmBroadcastReceiver">
<intent-filter>
<action android:name="witmoon.auto.replenish.stock.action"/>
<action android:name="witmoon.auto.synchronize.action"/>
intent-filter>
receiver>
由于我需要在App启动时就设置定时任务,因此我将代码放置到了Application中,并结合了Settings部分重新设置任务执行时间(每天一次),关于Settings部分后面说,此处只关心设置闹钟。
class HomeDoorApplication : Application() {
companion object {
// 自定义委托实现单例
var instance: HomeDoorApplication by DelegatesExt.notNullSingleValue()
}
override fun onCreate() {
super.onCreate()
instance = this
// 启动定时服务
startAlarm()
}
/**
* 启动定时器(使用系统闹铃服务)
*/
private fun startAlarm() {
var preferences = PreferenceManager.getDefaultSharedPreferences(this)
// 判断是否需要启动定时提交任务
var isAutoSubmitEnable = preferences.getBoolean(KEY_SETTING_AUTO_SUBMIT, true)
if (isAutoSubmitEnable) {
var hourMinuteArray = preferences.getString(KEY_SETTING_AUTO_SUBMIT_TIME, "00:00").split(":")
setAlarm(AlarmManager.RTC_WAKEUP, hourMinuteArray[0].toInt(), hourMinuteArray[1].toInt(),
AlarmManager.INTERVAL_DAY, ALARM_REPLENISH_STOCK_CODE, ACTION_ALARM_REPLENISH_STOCK)
}
// 判断是否需要启动定时同步任务
var isAutoSynchronizeEnable = preferences.getBoolean(KEY_SETTING_AUTO_SYNCHRONIZE, true)
if (isAutoSynchronizeEnable) {
var hourMinuteArray = preferences.getString(KEY_SETTING_SYNCHRONIZE_TIME, "00:00").split(":")
setAlarm(AlarmManager.RTC_WAKEUP, hourMinuteArray[0].toInt(), hourMinuteArray[1].toInt(),
AlarmManager.INTERVAL_DAY, ALARM_SYNCHRONIZE_CODE, ACTION_ALARM_SYNCHRONIZE)
}
}
/**
* 取消闹钟
*/
fun cancelAlarm(requestCode: Int, action: String) {
var alarmManager: AlarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.cancel(
PendingIntent.getBroadcast(this, requestCode, Intent(action), PendingIntent.FLAG_UPDATE_CURRENT))
}
fun setAlarm(type: Int, hour: Int, minute: Int, intervalMillis: Long, requestCode: Int, action: String) {
var now = Calendar.getInstance()
var targetTime = now.clone() as Calendar
targetTime.set(Calendar.HOUR_OF_DAY, hour)
targetTime.set(Calendar.MINUTE, minute)
targetTime.set(Calendar.SECOND, 0)
targetTime.set(Calendar.MILLISECOND, 0)
if (targetTime.before(now))
targetTime.add(Calendar.DATE, 1)
// 方便测试,这里指定即时启动,每20秒执行一次
setAlarm(type, 0, 20 * 1000, requestCode, action)
// setAlarm(type, targetTime.timeInMillis, intervalMillis, requestCode, action)
}
/**
* 设置闹钟
*/
fun setAlarm(type: Int, triggerAtMillis: Long, intervalMillis: Long, requestCode: Int, action: String) {
var alarmManager: AlarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.setRepeating(type, triggerAtMillis, intervalMillis, PendingIntent.getBroadcast(this,
requestCode, Intent(action), PendingIntent.FLAG_UPDATE_CURRENT))
}
}
这段代码实际上是同时设置了两个定时任务,自动提交订单和自动同步,为了更容易读懂代码,我们只看其中一个就可以。
在startAlarm方法中首先我读取了Settings部分的设置,判断是否需要启动闹钟,如果设置值为true,则去读取设置的任务执行时间;时间格式是每天的几点几分(如22:30表示每天晚上10点半执行)。
读取时间后拆分为小时和分钟,调用setAlarm方法,该方法参数包括闹钟类型、首次触发时间、任务间隔、以及requestCode和Action;在该方法中我使用拆分出的小时和分钟找到该时间点的毫秒数(如果时间在当前时间之前,即触发时间已是过去,则到明天这个时间再触发),调用setAlarm重载方法。
在setAlarm重载方法中,构建出PendingIntent对象并设置好闹钟。
首先看一下最终的设置界面
本文不介绍Settings部分内容,仅仅是记述重新设置任务执行时间的逻辑。
有关Android中Settings部分内容请参考官方文档:Settings
设置界面XML文件,其中用到了一个自定义的Preference用于选择时间,为了不显得太乱这部分代码不再贴出。
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="同步">
<SwitchPreference
android:defaultValue="true"
android:key="auto_synchronize_status"
android:summary="自动与服务器同步数据"
android:title="自动同步"/>
<com.witmoon.homedoor.ui.preference.TimePickPreference
android:defaultValue="22:30"
android:key="auto_synchronize_time"
android:summary="每天22点30分"
android:title="提交时间"/>
PreferenceCategory>
<PreferenceCategory android:title="下单">
<SwitchPreference
android:defaultValue="true"
android:key="auto_submit_status"
android:summary="开启后会在指定时间点自动提交缺货商品订单"
android:title="自动提交订单"/>
<com.witmoon.homedoor.ui.preference.TimePickPreference
android:defaultValue="23:30"
android:key="auto_submit_time"
android:summary="每天23点30分"
android:title="提交时间"/>
PreferenceCategory>
PreferenceScreen>
设置部分使用PreferenceFragment,在3.0以后官方建议使用PreferenceFragment来代替PreferenceActivity,定义好SettingFragment后将其放置到一个普通的Activity即可。
class SettingFragment : PreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
addPreferencesFromResource(R.xml.setting)
}
override fun onResume() {
super.onResume()
preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
super.onPause()
preferenceScreen.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
}
// 设置改变响应
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String) {
when {
KEY_SETTING_AUTO_SYNCHRONIZE.equals(key) -> onAutoSynchronizeChanged(sharedPreferences, key)
KEY_SETTING_SYNCHRONIZE_TIME.equals(key) -> onSynchronizeTimeChanged(sharedPreferences, key)
KEY_SETTING_AUTO_SUBMIT.equals(key) -> onAutoSubmitChanged(sharedPreferences, key)
KEY_SETTING_AUTO_SUBMIT_TIME.equals(key) -> onSubmitTimeChanged(sharedPreferences, key)
}
}
private fun onSubmitTimeChanged(preferences: SharedPreferences?, key: String) {
// 取消定时任务
HomeDoorApplication.instance.cancelAlarm(ALARM_REPLENISH_STOCK_CODE, ACTION_ALARM_REPLENISH_STOCK)
var hourMinuteArray = preferences?.getString(KEY_SETTING_AUTO_SUBMIT_TIME, "00:00")!!.split(":")
HomeDoorApplication.instance.setAlarm(AlarmManager.RTC_WAKEUP, hourMinuteArray[0].toInt(),
hourMinuteArray[1].toInt(), AlarmManager.INTERVAL_DAY, ALARM_REPLENISH_STOCK_CODE,
ACTION_ALARM_REPLENISH_STOCK)
}
private fun onSynchronizeTimeChanged(preferences: SharedPreferences?, key: String) {
// 取消定时任务
HomeDoorApplication.instance.cancelAlarm(ALARM_SYNCHRONIZE_CODE, ACTION_ALARM_SYNCHRONIZE)
// 设置定时任务
var hourMinuteArray = preferences?.getString(KEY_SETTING_SYNCHRONIZE_TIME, "00:00")!!.split(":")
HomeDoorApplication.instance.setAlarm(AlarmManager.RTC_WAKEUP, hourMinuteArray[0].toInt(),
hourMinuteArray[1].toInt(), AlarmManager.INTERVAL_DAY, ALARM_SYNCHRONIZE_CODE,
ACTION_ALARM_SYNCHRONIZE)
}
// 自动提交订单设置有变化,启用则绑定闹钟,禁用则取消闹钟
private fun onAutoSubmitChanged(preferences: SharedPreferences?, key: String) {
var isAutoSubmit = preferences?.getBoolean(key, true)!!
if (isAutoSubmit) {
preferenceScreen.findPreference(KEY_SETTING_AUTO_SUBMIT_TIME).isEnabled = true
var hourMinuteArray = preferences?.getString(KEY_SETTING_AUTO_SUBMIT_TIME, "00:00")!!.split(":")
HomeDoorApplication.instance.setAlarm(AlarmManager.RTC_WAKEUP, hourMinuteArray[0].toInt(),
hourMinuteArray[1].toInt(), AlarmManager.INTERVAL_DAY, ALARM_REPLENISH_STOCK_CODE,
ACTION_ALARM_REPLENISH_STOCK)
} else {
preferenceScreen.findPreference(KEY_SETTING_AUTO_SUBMIT_TIME).isEnabled = false
// 取消定时任务
HomeDoorApplication.instance.cancelAlarm(ALARM_REPLENISH_STOCK_CODE, ACTION_ALARM_REPLENISH_STOCK)
}
}
private fun onAutoSynchronizeChanged(preferences: SharedPreferences?, key: String) {
var isAutoSynchronize = preferences?.getBoolean(key, true)!!
if (isAutoSynchronize) {
preferenceScreen.findPreference(KEY_SETTING_SYNCHRONIZE_TIME).isEnabled = true
// 设置定时任务
var hourMinuteArray = preferences?.getString(KEY_SETTING_SYNCHRONIZE_TIME, "00:00")!!.split(":")
HomeDoorApplication.instance.setAlarm(AlarmManager.RTC_WAKEUP, hourMinuteArray[0].toInt(),
hourMinuteArray[1].toInt(), AlarmManager.INTERVAL_DAY, ALARM_SYNCHRONIZE_CODE,
ACTION_ALARM_SYNCHRONIZE)
} else {
preferenceScreen.findPreference(KEY_SETTING_SYNCHRONIZE_TIME).isEnabled = false
// 取消定时任务
HomeDoorApplication.instance.cancelAlarm(ALARM_SYNCHRONIZE_CODE, ACTION_ALARM_SYNCHRONIZE)
}
}
}
SharedPreferences.OnSharedPreferenceChangeListener接口用于监听设置项的改变,从代码中可以看出,如果是关闭自动任务,则直接调用取消闹钟;如果是重新设置了任务执行时间,则先取消闹钟,再用新的时间重新设置闹钟。取消和设置都在前面的Application中定义了,这里只是简单地调用即可。