Android AlarmManager实现定时任务(也就是闹钟) 附Demo源码

原文链接https://www.shanya.world/archives/a7b639d4.html

创建定时任务

Android中的定时任务一般有两种实现方式,一种是使用 Java API 里提供的 Timer 类,一种是使用 Android 的 Alarm 机制。这两种方式在多数情况下都能实现类似的效果,但 Timer 有一个明显的短板,它并不适用于那些需要长期在后台运行的定时任务。我们都知道,为了能让电池更加耐用,每种手机都会有自己的休眠策略,Android 手机就会在长时间不操作的情况下自动让 CPU 进入到睡眠状态,这就有可能导致 Timer 中的定时任务无法正常运行。而 Alarm 则具有唤醒 CPU 的功能,它可以在需要执行定时任务的时候大吼一声:“小UU,不要跟我 bbll ,赶紧给我起来干活,不然你看我扎不扎你就完了。”
需要注意,这里唤醒 CPU 和唤醒屏幕完全不是一个概念,千万不要混淆。

Alarm机制

首先看一下 Alarm 机制的用法,并不复杂,主要就是借助了 AlarmManager 类实现的。

获取 AlarmManager 的实例,代码如下:
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);

API19之前AlarmManager常用的一些方法

  • set(int type,long startTime,PendingIntent pi) //该方法用于设置一次性定时器,到达时间执行完就GG了
  • setRepeating(int type,long startTime,long intervalTime,PendingIntent pi)//该方法用于设置可重复执行的定时器
  • setInexactRepeating(int type,long startTime,long intervalTime,PendingIntent pi)//该方法用于设置可重复执行的定时器。与setRepeating相比,这个方法更加考虑系统电量,比如系统在低电量情况下可能不会严格按照设定的间隔时间执行闹钟,因为系统可以调整报警的交付时间,使其同时触发,避免超过必要的唤醒设备。
参数说明

int type:闹钟类型,常用有几个类型,说明如下:

AlarmManager.ELAPSED_REALTIME 表示闹钟在手机睡眠状态下不可用,就是睡眠状态下不具备唤醒CPU的能力(跟普通Timer差不多了),该状态下闹钟使用相对时间,相对于系统启动开始。
AlarmManager.ELAPSED_REALTIME_WAKEUP 表示闹钟在睡眠状态下会唤醒系统并执行提示功能,该状态下闹钟也使用相对时间
AlarmManager.RTC 表示闹钟在睡眠状态下不可用,该状态下闹钟使用绝对时间,即当前系统时间
AlarmManager.RTC_WAKEUP 表示闹钟在睡眠状态下会唤醒系统并执行提示功能,该状态下闹钟使用绝对时间

long startTime: 定时任务的出发时间,以毫秒为单位。

PendingIntent pi: 到时间后的执行意图。PendingIntent是Intent的封装类。需要注意的是,如果是通过启动服务来实现闹钟提 示的话,PendingIntent对象的获取就应该采用Pending.getService(Context c,int i,Intent intent,int j)方法;如果是通过广播来实现闹钟提示的话,PendingIntent对象的获取就应该采用 PendingIntent.getBroadcast(Context c,int i,Intent intent,int j)方法;如果是采用Activity的方式来实现闹钟提示的话,PendingIntent对象的获取就应该采用 PendingIntent.getActivity(Context c,int i,Intent intent,int j)方法。关于PendingInten不是本文重点,请自行查阅使用方法。

使用举例

需求:定义一个在CPU休眠情况下也能执行的闹钟,到==指定时间==发送一次广播,代码如下:

        AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.HOUR_OF_DAY,21);
        calendar.set(Calendar.MINUTE,14);
        calendar.set(Calendar.SECOND,00);//这里代表 21.14.00
        Intent intent = new Intent("Li_Hua");  
        intent.putExtra("msg","起床了啊");     
        PendingIntent pi = PendingIntent.getBroadcast(this,0,intent,0);   
        // 到了 21点14分00秒 后通过PendingIntent pi对象发送广播  
        am.set(AlarmManager.RTC_WAKEUP,calendar.getTimeInMillis(),pi);  

AlarmManager的版本适配

以上的讲解和代码在 API < 19 的情况下是可以正常运行的。但是在 API > 19 的手机上运行就会发现 TMD 怎么就不好使了呢。例如我们设置1分钟后执行,结果却是2分钟后才执行。
But 这不是 BUG!
原因是 Google 对系统耗电性方面进行了优化。系统会自动检测目前有多少 Alarm 任务存在,然后将触发时间相近的几个任务放在一起执行,这就可以大幅度减少 CPU 被唤醒的次数,从而有效延长电池的使用时间。
当然,如果你要求的 Alarm 任务执行时间必须准确无误,Android 仍然提供了解决方案。使用 AlarmManager 的 setExact()方法来代替set()方法,就基本上可以保证任务能够准时执行了。

虽然Android的每个系统版本都在手机电量方面努力进行优化,不过一直没能解决后台服务泛滥、手机电量消耗过快的问题。于是在Android 6.0系统中,谷歌加入了一个全新的Doze模式,从而可以极大幅度地延长电池的使用寿命。
到底什么是Doze模式。当用户的设备是Android 6.0或以上系统时,如果该设备未插接电源,处于静止状态( Android 7.0中删除了这条件), 且屏幕关闭了一段时间之后,就会进人到Doze模式。在Doze模式下,系统会对CPU、网络、Alarm等活动进行限制,从而延长了电池的使用寿命。
当然,系统并不会一直处于 Doze模式,而是会间歇性地退出Doze模式一小段时间,在这段时间中,应用就可以去完成它们的同步操作、Alarm任务, 等等。
接下来我们具体看- 看在Doze模式下有哪些功能会受到限制吧。

  • 网络访问被禁止
  • 系统忽略唤醒CPU或者屏幕操作
  • 系统不再执行WIFI扫描
  • 系统不再执行同步服务
  • Alarm任务将会在下次退出Doze模式的时候执行

注意其中的最后一条, 也就是说,在Doze模式下,我们的Alarm任务将会变得不准时。当然,这在大多数情况下都是合理的,因为只有当用户长时间不使用手机的时候才会进入Doze模式,通常在这种情况下对Alarm任务的准时性要求并没有那么高。
不过,如果你真的有非常特殊的需求,要求Alarm任务即使在Doze模式下也必须正常执行,Android还是提供了解决方案。调用AlarmManager的setAndAllowWhileIdle()setExact-AndAllowhileIdle()方法就能让定时任务即使在Doze模式下也能正常执行了,这两个方法之间的区别和set()setExact()方法之间的区别是一样的。

AlarmManager实例Demo讲解(包含版本适配以及高版本设置重复闹钟)

好了经过上面讲解,我相信你是似懂非懂的,因为没看到具体代码啊,简单,一个小Demo就全都明白了。
实现功能:在CPU休眠情况下依然可以设定时间启动一次服务,在服务中执行相应逻辑(Demo中只是打印Log),适配各个版本。

先看一下最核心的AlarmManagerUtils类(AlarmManager工具类):

package com.shanya.testalarm;

import android.annotation.SuppressLint;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;

import java.util.Calendar;

public class AlarmManagerUtils {

    private static final long TIME_INTERVAL = 10 * 1000;//闹钟执行任务的时间间隔
    private Context context;
    public static AlarmManager am;
    public static PendingIntent pendingIntent;

    private Calendar calendar;
    //
    private AlarmManagerUtils(Context aContext) {
        this.context = aContext;
    }

    //singleton
    private static AlarmManagerUtils instance = null;

    public static AlarmManagerUtils getInstance(Context aContext) {
        if (instance == null) {
            synchronized (AlarmManagerUtils.class) {
                if (instance == null) {
                    instance = new AlarmManagerUtils(aContext);
                }
            }
        }
        return instance;
    }

    public void createGetUpAlarmManager() {
        am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        Intent intent = new Intent(context, MyService.class);
        pendingIntent = PendingIntent.getService(context, 0, intent, 0);//每隔10秒启动一次服务
    }

    @SuppressLint("NewApi")
    public void getUpAlarmManagerStartWork() {

        calendar = Calendar.getInstance();
        calendar.set(Calendar.HOUR_OF_DAY,23);
        calendar.set(Calendar.MINUTE,50);
        calendar.set(Calendar.SECOND,00);

        //版本适配 System.currentTimeMillis()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {// 6.0及以上
            am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,
                    calendar.getTimeInMillis(), pendingIntent);
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {// 4.4及以上
            am.setExact(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
                    pendingIntent);
        } else {
            am.setRepeating(AlarmManager.RTC_WAKEUP,
                    calendar.getTimeInMillis(), TIME_INTERVAL, pendingIntent);
        }
    }

    @SuppressLint("NewApi")
    public void getUpAlarmManagerWorkOnOthers() {
        //高版本重复设置闹钟达到低版本中setRepeating相同效果
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {// 6.0及以上
            am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,
                    System.currentTimeMillis() + TIME_INTERVAL, pendingIntent);
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {// 4.4及以上
            am.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis()
                    + TIME_INTERVAL, pendingIntent);
        }
    }
}

AlarmManagerUtils就是将与AlarmManager有关的操作都封装起来了,方便解耦。很简单,主要就是版本适配了,上面已经讲解够仔细了,这里就是判断不同版本调用不同API了。

MainActivity代码:

package com.shanya.testalarm;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;


public class MainActivity extends AppCompatActivity {

    private AlarmManagerUtils alarmManagerUtils;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        alarmManagerUtils = AlarmManagerUtils.getInstance(this);
        alarmManagerUtils.createGetUpAlarmManager();

        Button button = findViewById(R.id.am);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                alarmManagerUtils.getUpAlarmManagerStartWork();
                Toast.makeText(getApplicationContext(),"设置成功",Toast.LENGTH_SHORT).show();
            }
        });

    }
}

MainActivity中就是调用AlarmManagerUtils中已经封装好的代码进行初始化以及点击Button的时候调用getUpAlarmManagerStartWork方法完成第一次触发AlarmManager。

最后看下服务类中具体做了什么。
MyService类:

package com.shanya.testalarm;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.os.SystemClock;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.RequiresApi;

public class MyService extends Service {
    private static final String TAG = "MyService";
    public MyService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");

    }

    @RequiresApi(api = Build.VERSION_CODES.M)
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "run: ");
            }
        }).start();
        AlarmManagerUtils.getInstance(getApplicationContext()).getUpAlarmManagerWorkOnOthers();
        return super.onStartCommand(intent, flags, startId);
    }
}

总结

好了,本文到此就该结束了,相信经过以上讲述你对AlarmManager有了更进一步全面了解,在我们使用的时候请不要滥用,考虑一下用户电量,尽量优化自己APP。

源码下载

CSDN 下载
GitHub 下载

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容