Android进阶宝典 -- Android AlarmManager闹钟任务

一般来说,应用开发大部分处理的都是前台任务,例如用户点击按钮跳转到某个页面,或者点击刷新按钮获取最新的数据,这种交互形式是很常见的,然后还有一些任务是需要在后台执行,在用户不可见的场景下完成任务执行,然而Android操作系统对于后台任务的把控比较严格,相当于前台进程来说,后台进程优先级更低,系统在内存不足的情况下会选择杀死进程,从而终止后台任务。因此对于业务上的需求,我们需要分清任务的种类并选择正确的方案。

1 后台任务的种类

常见的后台任务主要分为以下几种:

  • 即时任务
  • 延期任务
  • 精确任务
  1. 即时任务,即用户需要与应用互动时完成,例如点击按钮刷新数据;
  2. 延期任务,即任务不需要精确的时间点完成,例如云控配置,不需要用户即时感受到,可选择一些时机触发,从而处理相应的业务逻辑;
  3. 精确任务,即需要明确的时间点完成,例如在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触发之后,再设置新的事件。

相关推荐
阿巴斯甜21 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker21 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android