一般来说,应用开发大部分处理的都是前台任务,例如用户点击按钮跳转到某个页面,或者点击刷新按钮获取最新的数据,这种交互形式是很常见的,然后还有一些任务是需要在后台执行,在用户不可见的场景下完成任务执行,然而Android操作系统对于后台任务的把控比较严格,相当于前台进程来说,后台进程优先级更低,系统在内存不足的情况下会选择杀死进程,从而终止后台任务。因此对于业务上的需求,我们需要分清任务的种类并选择正确的方案。
对于即时任务,如果在前台执行,我们常用的就是ViewModel配合协程;如果应用退到了后台,需要在后台执行即时任务,建议使用WorkManager,这个属于Jetpack组件中的一个具备执行持久化、周期性任务和即时任务的组件,具体的使用方式和原理,我将会在单独的文章中介绍。而且对于延期任务也是建议使用WorkManager做任务调度。
而对于精确任务,我们建议使用AlarmManager设置闹铃时间,从而执行精细化的任务。
对于Android操作系统来说,为了避免电池电量的消耗,在锁屏一段时间之后,会进入深度睡眠的状态,前提是没有进程持有wakelock。 对于想要执行精确任务的进程,单单开一个Timer定时器是不可能完成的,因此对于Timer来说,在设备没有插电的情况下,计时会有偏差而且进入深度睡眠之后,Timer将会彻底停止工作。
所以如果想要在应用外,或者应用未运行、系统处于休眠时,唤醒设备触发操作,AlarmManager将会是一个不错的选择,但是其灵活性是有限制的。
public static PendingIntent getActivity(Context context, int requestCode, Intent intent, int flags) { throw new RuntimeException("Stub!"); } // 堆代码 duidaima.com 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构建。
Intent(context,AlarmReceiver::class.java).also { PendingIntent.getBroadcast(context,1,it, PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE) } class AlarmReceiver : BroadcastReceiver() { override fun onReceive(p0: Context?, p1: Intent?) { } }2.1.2 闹钟的类型
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触发一次,此种类型不会唤醒设备。
manager?.setInexactRepeating( AlarmManager.ELAPSED_REALTIME_WAKEUP, // triggerAtMillis SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HALF_HOUR, AlarmManager.INTERVAL_HALF_HOUR, pendingIntent )
当然,这里我使用了setInexactRepeating设置重复时钟,相较于使用setRepeating,前者的时间精度上会差一点,当然这也是为了系统整体的耗电量做出优化,使用setInexactRepeating时,Android系统会同步来自多个应用的重复闹钟,从而只唤醒一次设备;如果使用setRepeating,则是作为最精确的闹钟,可能会多次唤醒系统。
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;所以两者在使用上,还是要具体场景具体分析。
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构建,这样能够保持低的耗电量。
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>这两个权限,前者是能够在系统启动之后,app可以接收到系统启动的广播;后者是支持唤醒系统。
<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,之所以这么设置是因为可以防止系统不必要地启动接收器,只有应用程序内部明确要使用该接收器,那么才可以使用。
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) } } } } }在接收器里的逻辑就是,当系统启动之后,重新设置闹钟规则。
//启动接收器 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。
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触发之后,再设置新的事件。