一般来说,应用开发大部分处理的都是前台任务,例如用户点击按钮跳转到某个页面,或者点击刷新按钮获取最新的数据,这种交互形式是很常见的,然后还有一些任务是需要在后台执行,在用户不可见的场景下完成任务执行,然而Android操作系统对于后台任务的把控比较严格,相当于前台进程来说,后台进程优先级更低,系统在内存不足的情况下会选择杀死进程,从而终止后台任务。因此对于业务上的需求,我们需要分清任务的种类并选择正确的方案。
1 后台任务的种类
常见的后台任务主要分为以下几种:
- 即时任务
- 延期任务
- 精确任务
- 即时任务,即用户需要与应用互动时完成,例如点击按钮刷新数据;
- 延期任务,即任务不需要精确的时间点完成,例如云控配置,不需要用户即时感受到,可选择一些时机触发,从而处理相应的业务逻辑;
- 精确任务,即需要明确的时间点完成,例如在4点32分定了一个闹铃,就不能在4点33分提醒用户。
对于即时任务,如果在前台执行,我们常用的就是ViewModel配合协程;如果应用退到了后台,需要在后台执行即时任务,建议使用WorkManager
,这个属于Jetpack组件中的一个具备执行持久化、周期性任务和即时任务的组件 ,具体的使用方式和原理,我将会在单独的文章中介绍。而且对于延期任务也是建议使用WorkManager
做任务调度。
而对于精确任务,我们建议使用AlarmManager
设置闹铃时间,从而执行精细化的任务。
2 精确任务执行
对于即时任务和延期任务,我会在WorkManager
单独的专题中介绍,本文将会着重介绍下精确任务的执行。
2.1 AlarmManager
对于Android操作系统来说,为了避免电池电量的消耗,在锁屏一段时间之后,会进入深度睡眠的状态,前提是没有进程持有wakelock。 对于想要执行精确任务的进程,单单开一个Timer定时器是不可能完成的,因此对于Timer来说,在设备没有插电的情况下,计时会有偏差而且进入深度睡眠之后,Timer将会彻底停止工作。
所以如果想要在应用外,或者应用未运行、系统处于休眠时,唤醒设备触发操作,AlarmManager将会是一个不错的选择,但是其灵活性是有限制的。
2.1.1 PendingIntent
PendingIntent从字面意思看是待确定的意图,它与我们常见的Intent还是有所不同:
- Intent创建之后会立即执行,而PendingIntent则是会在事件触发之后才执行;
- 在应用程序结束之后,Intent就会失效,而PendingIntent则是会继续有效,除非被取消;
那么PendingIntent的获取方式则是有很多,基本上都是与四大组件相关,例如getActivity获取到的PendingIntent则是会启动页面;通过getBroadcast获取到的Intent则是会激活广播,在onReceive中处理逻辑。
java
public static PendingIntent getActivity(Context context, int requestCode, Intent intent, int flags) {
throw new RuntimeException("Stub!");
}
public static PendingIntent getActivity(Context context, int requestCode, @NonNull Intent intent, int flags, @Nullable Bundle options) {
throw new RuntimeException("Stub!");
}
public static PendingIntent getActivities(Context context, int requestCode, @NonNull Intent[] intents, int flags) {
throw new RuntimeException("Stub!");
}
public static PendingIntent getActivities(Context context, int requestCode, @NonNull Intent[] intents, int flags, @Nullable Bundle options) {
throw new RuntimeException("Stub!");
}
public static PendingIntent getBroadcast(Context context, int requestCode, @NonNull Intent intent, int flags) {
throw new RuntimeException("Stub!");
}
public static PendingIntent getService(Context context, int requestCode, @NonNull Intent intent, int flags) {
throw new RuntimeException("Stub!");
}
所以对于定时任务来说,如果不需要页面展现,则是可以通过getBroadcast或者getService构建PendingIntent,如果需要跳出页面,则可以通过getActivity构建。
kotlin
Intent(context,AlarmReceiver::class.java).also {
PendingIntent.getBroadcast(context,1,it,
PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE)
}
kotlin
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(p0: Context?, p1: Intent?) {
}
}
2.1.2 闹钟的类型
AlarmManager主要是分为以下几种类型:
java
public static final int ELAPSED_REALTIME = 3;
public static final int ELAPSED_REALTIME_WAKEUP = 2;
public static final int RTC = 1;
public static final int RTC_WAKEUP = 0;
- ELAPSED_REALTIME:基于设备自启动以来经过的时间,适合每隔一段时间触发一次Intent,例如每隔10min触发一次,此种类型不会唤醒设备。
- ELAPSED_REALTIME_WAKEUP:与ELAPSED_REALTIME唯一区别在于,这种类型会唤醒设备;
- RTC:在指定的时间触发Intent,适合做每日闹钟提醒,此种类型不会唤醒设备;
- RTC_WAKEUP:与RTC唯一的区别就是,这种类型会唤醒设备。
那么基于此,我设置一个闹钟,半小时后触发PendingIntent,此后每半小时触发一次并唤醒设备。
Kotlin
manager?.setInexactRepeating(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
// triggerAtMillis
SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HALF_HOUR,
AlarmManager.INTERVAL_HALF_HOUR, pendingIntent
)
当然,这里我使用了setInexactRepeating
设置重复时钟,相较于使用setRepeating
,前者的时间精度上会差一点,当然这也是为了系统整体的耗电量做出优化,使用setInexactRepeating
时,Android系统会同步来自多个应用的重复闹钟,从而只唤醒一次设备;如果使用setRepeating
,则是作为最精确的闹钟,可能会多次唤醒系统。
还有一点,在使用setInexactRepeating
时,无法自定义时间间隔,只能使用AlarmManger
中时间间隔常量如下:
java
public static final long INTERVAL_DAY = 86400000L;
public static final long INTERVAL_FIFTEEN_MINUTES = 900000L;
public static final long INTERVAL_HALF_DAY = 43200000L;
public static final long INTERVAL_HALF_HOUR = 1800000L;
public static final long INTERVAL_HOUR = 3600000L;
所以两者在使用上,还是要具体场景具体分析。
例如我们定了早晨8点的起床闹钟,不想起,此后每10min提醒一次,便可以使用setRepeating
创建精确闹钟,并自定义时间间隔,此时使用setRepeating
便是一个好的选择。
kotlin
val calendar: Calendar = Calendar.getInstance().apply {
timeInMillis = System.currentTimeMillis()
set(Calendar.HOUR_OF_DAY, 8)
set(Calendar.MINUTE, 30)
}
manager?.setRepeating(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
10 * 60 * 1000, pendingIntent
)
如果只是想定一个早晨8点左右的闹钟,则是可以使用setInexactRepeating
构建,这样能够保持低的耗电量。
2.2 设备重启时启动闹钟
当用户把设备关机之后,闹钟也会被取消,如果保证用户开机之后,也能继续按照原先的配置执行,而不需要用户重新设置闹钟。
- 申请权限
xml
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
这两个权限,前者是能够在系统启动之后,app可以接收到系统启动的广播;后者是支持唤醒系统。
- 静态注册广播,以便接收系统启动广播
在Android 8.0以上的版本中,系统对清单文件中的接收器进行了严格的限制,对于大多数隐式广播(没有明确针对某个应用的广播),也就是静态广播,不能使用清单来声明接收器。 但对于某些隐式广播,系统给予了豁免权,比如说我们用到的android.intent.action.BOOT_COMPLETED
就是其中之一,而还有哪些隐式广播有豁免权可以查看 隐式广播例外情况
xml
<receiver android:name=".alarm.SystemBootReceiver"
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
我们看这里是设置了enabled属性为false,之所以这么设置是因为可以防止系统不必要地启动接收器,只有应用程序内部明确要使用该接收器,那么才可以使用。
kotlin
class SystemBootReceiver : BroadcastReceiver() {
override fun onReceive(p0: Context?, p1: Intent?) {
when (p1?.action) {
"android.intent.action.BOOT_COMPLETED" -> {
Log.d("TAG", "onReceive: 系统启动了")
p0?.let {
AlarmUtil.setAlarm(it)
}
}
}
}
}
在接收器里的逻辑就是,当系统启动之后,重新设置闹钟规则。
- 启用接收器
在清单文件中,我们设置了广播接收器的enabled属性为false,那么在应用程序中,我们通过这种方式启用接收器:
kotlin
//启动接收器
val receiver = ComponentName(this, SystemBootReceiver::class.java)
packageManager.setComponentEnabledSetting(
receiver,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)
在创建组件之后,调用PackageManager的setComponentEnabledSetting方法,设置组件状态为COMPONENT_ENABLED_STATE_ENABLED,代表此组件可以使用,此时即便是设备重启,那么也会覆盖清单文件中的配置,此组件会一直可用,直到应用不再使用闹钟,此时就可以设置组件状态为COMPONENT_ENABLED_STATE_DISABLED。
2.3 低功耗模式下触发闹钟
在Android 6.0之后,引入了低功耗和应用待机模式,即在低功耗模式下,所有的闹钟都会被推迟,直到退出低功耗模式,或者接通电源之后,才会恢复。
所以,如果想要在低功耗模式下触发闹钟事件,可以使用setAndAllowWhileIdle
或者setExactAndAllowWhileIdle
java
public void setAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation) {
throw new RuntimeException("Stub!");
}
public void setExactAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation) {
throw new RuntimeException("Stub!");
}
从这两个方法中,我们看到不能设置触发周期,像在低功耗模式下的周期性任务,可以在PendingIntent触发之后,再设置新的事件。