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触发之后,再设置新的事件。

相关推荐
姑苏风1 小时前
《Kotlin实战》-附录
android·开发语言·kotlin
数据猎手小k4 小时前
AndroidLab:一个系统化的Android代理框架,包含操作环境和可复现的基准测试,支持大型语言模型和多模态模型。
android·人工智能·机器学习·语言模型
你的小105 小时前
JavaWeb项目-----博客系统
android
风和先行5 小时前
adb 命令查看设备存储占用情况
android·adb
AaVictory.6 小时前
Android 开发 Java中 list实现 按照时间格式 yyyy-MM-dd HH:mm 顺序
android·java·list
似霰7 小时前
安卓智能指针sp、wp、RefBase浅析
android·c++·binder
大风起兮云飞扬丶7 小时前
Android——网络请求
android
干一行,爱一行7 小时前
android camera data -> surface 显示
android
断墨先生7 小时前
uniapp—android原生插件开发(3Android真机调试)
android·uni-app
无极程序员9 小时前
PHP常量
android·ide·android studio