AlarmManager详解

AlarmManager提供对系统闹钟服务的访问。应用将自己的意图传递给系统闹钟服务,当闹钟响起时,系统会向应用发送广播。如果应用尚未执行,则会自动启动该应用程序。当设备属于睡眠状态时,注册的闹钟会被保留(如果此期间,闹钟关闭,可以选择唤醒设备),但如果系统关闭并重新启动,则会被清除。

一、定时任务的实现方案

(1)使用Timer实现

arduino 复制代码
Timer.schedule 可以实现延迟执行某个任务,也可以重复执行某个任务。
​
Timer常常用于时间较短的延迟处理,或者循环次数较少的重复任务,比如30秒倒计时,倒计时结束之后需要cache任务;
​
Timer底层封装了一个线程,和TimerTask队列,这个队列按照一定的方式将任务排队处理。当Timer实例被创建的时候,线程被启动,从而执行线程的run方法。
​
但是,Timer却有一个致命的缺点,如果CPU一旦进入休眠状态(熄掉屏幕等待一段时间),线程将会失去CPU时间片而阻塞,从而造成定时任务失效。
实际上,熄掉屏幕等待一段时间之后,定时任务将不再定时,而是比预定的时间延迟一段时间才会执行,从而造成定时任务的不准确性,
当CPU休眠之后,定时任务将被完全阻塞。
如果再次唤醒屏幕,打开应用,定时任务将会被从新唤起,继续执行任务(非重新执行任务,而是继续执行任务,除非应用进程被系统杀死);
​
Timer定时器解决方案:如果能做到让CPU不休眠,那么就可以保证定时任务的准确性,使用WakeLock可以让CPU保持唤醒状态。
​
WakeLock一般我们不会使用它,因为让CPU保持唤醒是一个比较耗电的方式,用耗电的方式解决Timer定时器不准确的问题是得不偿失的。

(2)使用Handler实现

复制代码
Handler 的 postDelayed 的方法可以实现延时任务,假如CPU进入休眠状态,延迟任务会失效。
这个现象和Timer一致。

(3)使用AlarmManager实现

复制代码
AlarmManager是本章的重点,它是定时任务的重要方式,下文会有详细介绍。
二、AlarmManager接口介绍

(1)获取实例

scss 复制代码
    // 获取系统闹钟服务管理对象
    AlarmManager manager = (AlarmManager) getSystemService(Service.ALARM_SERVICE);

getSystemService方法可以获取系统的Manager Service实例,是应用和系统通信的主要方式。 AlarmManager是系统闹钟服务的管理对象。

(2)设置闹钟

less 复制代码
public void set(@AlarmType int type, long triggerAtMillis, PendingIntent operation)

以上是设置闹钟的一个重要方法,但是从 Android 4.4(API 19) 开始,将变得不再精准,为了能够闹钟精准,Google提供了另外一个接口:

less 复制代码
public void setExact(@AlarmType int type, long triggerAtMillis, PendingIntent operation)

第一个参数type是闹钟类型,它有四种选项,分别是

复制代码
AlarmManager.RTC_WAKEUP:让定时任务的触发时间从1970年1月1日0点开始算起,但会唤醒CPU
AlarmManager.RTC:让定时任务的触发时间从1970年1月1日0点开始算起,但不唤醒CPU
AlarmManager.ELAPSED_REALTIME_WAKEUP:让定时任务的触发时间从系统开机开始算起,但会唤醒CPU
AlarmManager.ELAPSED_REALTIME:让定时任务的触发时间从系统开机开始算起,但不唤醒CPU

第二个参数triggerAtMillis是闹钟任务的触发时间,这里要说到两种时间的概念

scss 复制代码
相对时间:设备boot后到当前经历的时间,SystemClock.elapsedRealtime()获取到的是相对时间。
绝对时间:1970年1月1日到当前经历的时间,System.currentTimeMillis()和Calendar.getTimeInMillis()获取到的都是绝对时间。
​
如果是相对时间,那么计算triggerAtMillis就需要使用SystemClock.elapsedRealtime();
如果是绝对时间,那么计算triggerAtMillis就需要使用System.currentTimeMillis()或者calendar.getTimeInMillis();
​
绝对时间与 AlarmManager.RTC_WAKEUP 和 AlarmManager.RTC 对应;
相对时间与 AlarmManager.ELAPSED_REALTIME_WAKEUP 和 AlarmManager.ELAPSED_REALTIME 对应;

为了让闹钟精准,下面简单做下适配

scss 复制代码
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        
        // Android 4.4 (API 19)被添加
        // 设置精准的闹钟
        manager.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
        // 或者
        // manager.setWindow(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),  5*1000, pendingIntent);
    } else {

        // Android 4.4 之前为精准闹钟,之后不是精准闹钟
        manager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
    }

在 Android 5.0 (API 21)时,新增了另一个精准闹钟方法

java 复制代码
public void setAlarmClock(AlarmClockInfo info, PendingIntent operation)

setAlarmClock 的默认闹钟类型是RTC_WAKEUP,代码如下:

scss 复制代码
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
        AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(System.currentTimeMillis() + 5*1000, pendingIntent);
        // Android 5.0 (API 21)被添加
        // 设置精确的闹钟
        // 类似于 setExact 方法,但是 setAlarmClock 默认的闹钟类型是 RTC_WAKEUP
        manager.setAlarmClock(alarmClockInfo, pendingIntent);
    }

Android 6.0(API 23) 中引入了低电耗模式(Doze)和应用待机模式(standby)。 由于低电耗模式(Doze)的限制,AlarmManager闹钟将推迟到下一个维护期,如果您希望在设备处于低电耗模式(Doze)下也能触发闹钟,那么请使用

ini 复制代码
manager.setAndAllowWhileIdle(...);

或者

ini 复制代码
manager.setExactAndAllowWhileIdle(...);

常用的闹钟接受消息是用 BroadcastReceiver 或者 Service,从Android 7.0(API 24)开始,新增了另一种接收方式,使用OnAlarmListener监听的方式接收:

less 复制代码
public void set(@AlarmType int type, long triggerAtMillis, String tag, OnAlarmListener listener,
        Handler targetHandler)

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                manager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, "MyTAG", new AlarmManager.OnAlarmListener() {
                    @Override
                    public void onAlarm() {

                    }
                }, null);
            }

参数说明如下:

csharp 复制代码
tag:listener的TAG,在缓存中,肯能存在多个listener,为了方便管理,指定tag,为listener烙印上唯一标识;
listener:取代 BroadcastReceiver 和 Service;
targetHandler:一般设置为null即可,一旦设置为null,onAlarm方法必然在主线程执行,如果targetHandler不为null,那么需要注意:
    不能在子线程中创建Handler对象,不确定是主线程还是子线程,那么需要在Handler的构造方法中执行主线程,把null替换成
    new Handler(Looper.getMainLooper())
    即可。

                manager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, "AA", new AlarmManager.OnAlarmListener() {
                    @Override
                    public void onAlarm() {
                    }
                }, new Handler(Looper.getMainLooper()));

    或者执行Looper.prepare()
    
                Looper.prepare();
                manager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, "AA", new AlarmManager.OnAlarmListener() {
                    @Override
                    public void onAlarm() {
                    }
                }, new Handler());

同样,对应的 setExact 和 setWindow 也在Android 7.0(API 24)的时候新增了OnAlarmListener的支持。

下面开始介绍,如何设置重复闹钟:

ini 复制代码
    manager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), 1000, pendingIntent);
    和
    manager.setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), 1000, pendingIntent);

setInexactRepeating:设置不准确的重复闹钟,可以当作定时器 setRepeating:在Android 4.4之前(Android 19之前,不包括 19)是准确的,但是Android 4.4之后变为不准确;

(3)设置系统时间

scss 复制代码
    // 设置系统时间
    // 需要添加权限 android.Manifest.permission#SET_TIME
    manager.setTime(xxx);

需要在AndroidManifest文件中添加权限:android.permission.SET_TIME

(4)设置系统时区

scss 复制代码
    // 设置时区
    // 需要添加权限 android.Manifest.permission#SET_TIME_ZONE
    manager.setTimeZone(xxx);

需要在AndroidManifest文件中添加权限:android.permission.SET_TIME_ZONE

(5)取消闹钟

scss 复制代码
    manager.cancel(pendingIntent);

或者

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        manager.cancel(alarmListener);
    }

(6)是否可以安排精确闹钟

scss 复制代码
    if (Build.VERSION.SDK_INT >= 31) {
        manager.canScheduleExactAlarms();
    }

canScheduleExactAlarms添加于Android 12(API 31),用于获取是否可以安排精确闹钟。
在大多数情况下,应用应该使用非精确闹钟 (inexact alarms),这样可以减少电池消耗。
Android 系统可以通过低电耗模式 (Doze) 和应用待机模式 (App Standby) 等机制管理这些闹钟,从而最大限度地减少设备唤醒和电池消耗。
对于那些需要精确闹钟的情况,例如闹铃应用和定时器,您仍然可以使用精确闹钟 (exact alarms)。
精确闹钟功能非常方便可靠,但也会加大电量消耗。
适配策略:尽可能调整为不需精确闹钟,针对 Android 12 的应用如果想要使用精确闹钟,现在需要申请一个新的权限: SCHEDULE_EXACT_ALARM。
这是一个一般权限,所以只要您的应用在清单中进行了声明,就会在第一次启动时被自动授予该权限。但用户仍可拒绝授予或撤销权限。
使用canScheduleExactAlarms(),可用来检查应用的权限状态。
三、简单代码例子
scala 复制代码
public class AlarmReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {

    }
}
xml 复制代码
    <receiver android:name=".AlarmReceiver">
        <intent-filter>
            <action android:name="com.test.alarm"/>
        </intent-filter>
    </receiver>
csharp 复制代码
    // PendingIntent
    Intent intent = new Intent(MainActivity.this, AlarmReceiver.class);
    intent.setAction("com.test.alarm");
    intent.putExtra("name", "value");
    PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, intent,  PendingIntent.FLAG_UPDATE_CURRENT);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
​
        // Android 4.4 (API 19)被添加
        // 设置精准的闹钟
        manager.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
    } else {
​
        // Android 4.4 之前为精准闹钟,之后不是精准闹钟
        manager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
    }
四、总结
ini 复制代码
(1)设置系统时间需要添加权限:android.permission.SET_TIME;
​
   <uses-permission android:name="android.permission.SET_TIME" />
​
(2)设置系统时区需要添加权限:android.permission.SET_TIME_ZONE
​
    <uses-permission android:name="android.permission.SET_TIME_ZONE" />
​
(3)如果需要在Android 12(API 31)上使用精准闹钟,必须设置权限:SCHEDULE_EXACT_ALARM
​
    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
​
(4)重复闹钟只有 setRepeating 和 setInexactRepeating,其它均为一次性闹钟
​
(5)单次闹钟适配可以是这样的
​
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        manager.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
    } else {
        manager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
    }
​
但是,如果需要在低功耗空闲状态下,闹钟也能生效,那么需要修改代码:
​
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        manager.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
    } else {
        manager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
    }
​
setExact效果等同于setWindow。
​
(6)重复闹钟适配可以是这样的
​
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        manager.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5*1000, pendingIntent);
    } else {
        manager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), 5*1000, pendingIntent);
    }
​
由于setExactAndAllowWhileIdle和setExact是单次闹钟,当接收到闹钟时,再重新设置一下闹钟即可。
setRepeating本身就是重复闹钟,所以不需要重新设置;
​
(7)接收闹钟可以是BroadcastReceiver,也可以是Service,还可以是OnAlarmListener,只不过OnAlarmListener是Android 7.0新增的,需要做版本判断;
相关推荐
hawk2014bj4 小时前
Ubuntu 安装 MySQL
android·mysql·ubuntu
_小马快跑_6 小时前
Android 图像合成:玩转 PorterDuff.Mode 的 18 种混合模式
android
_小马快跑_6 小时前
Android | 多种方式实现图片圆角矩形和圆形效果(续)
android
_小马快跑_6 小时前
MaterialShapeDrawable vs CardView:两种方式实现阴影效果对比
android
_小马快跑_7 小时前
玩转ShapeableImageView:实现灵活的自定义形状与边框效果
android
菜鸟xiaowang8 小时前
Android 使用ninja加速编译的方法
android
_一条咸鱼_9 小时前
Android大厂面试秘籍: View 相关面试题深入分析
android·面试·android jetpack
_一条咸鱼_10 小时前
Android 大厂面试秘籍:Hilt 框架的测试支持模块(八)
android·面试·kotlin
匹马夕阳10 小时前
(十三)安卓开发中的输入框、复选框、单选框和开关等表单控件详解
android
yangshuo128112 小时前
WSA(Windows Subsystem for Android)安装LSPosed和应用教程
android·windows·模拟器·lsposed·windows安卓子系统