AlarmManager详解

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

一、定时任务的实现方案

(1)使用Timer实现

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)获取实例

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

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

(2)设置闹钟

public void set(@AlarmType int type, long triggerAtMillis, PendingIntent operation)

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

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是闹钟任务的触发时间,这里要说到两种时间的概念

相对时间:设备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 对应;

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

    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)时,新增了另一个精准闹钟方法

public void setAlarmClock(AlarmClockInfo info, PendingIntent operation)

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

    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)下也能触发闹钟,那么请使用

manager.setAndAllowWhileIdle(...);

或者

manager.setExactAndAllowWhileIdle(...);

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

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);
            }

参数说明如下:

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的支持。

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

    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)设置系统时间

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

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

(4)设置系统时区

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

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

(5)取消闹钟

    manager.cancel(pendingIntent);

或者

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

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

    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(),可用来检查应用的权限状态。
三、简单代码例子
public class AlarmReceiver extends BroadcastReceiver {

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

    }
}
    <receiver android:name=".AlarmReceiver">
        <intent-filter>
            <action android:name="com.test.alarm"/>
        </intent-filter>
    </receiver>
    // 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);
    }
四、总结
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新增的,需要做版本判断;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值