开发者社区> ctrip_xzh> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

android AlarmManager讲解

简介: <p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; font-family:Arial; font-size:14px; line-height:26px"> Android系统闹钟定时功能框架,总体来说就是用数据库存储定时数据,有一个状态管理器来统一管理这些定时状态的触发和更
+关注继续查看

Android系统闹钟定时功能框架,总体来说就是用数据库存储定时数据,有一个状态管理器来统一管理这些定时状态的触发和更新。在Andriod系统中实现定时功能,最终还是要用到系统提供的AlarmManager,只是当一个定时完成后怎么继续处理,或者中间怎么更新定时的时间或者状态,像闹钟这种应用程序,每天重复定时,或者一周选择其中的几天,闹钟响了延迟5分钟再次响铃,这时候就需要想一种好的办法来让管理这些数据和状态,下面就分析一下Android系统闹钟的实现。


1、基本结构


Alarm

代表一条定时数据

AlarmInstance

代表一个定时项目的实例,一个AlarmInstance对应到一个Alarm,相比Alarm多存储了一些状态信息

AlarmStateManager

状态管理器,对定时项目进行调度,添加、删除、更改状态,是一个BroadcastReciever,定时到点后发广播到这里进行下一步处理

AlarmService

响应结果,也就是定时到达后要做的事,响铃,停止响铃

ClockDataHelper

里面创建了三个表,ALARMS_TABLE,INSTANCE_TABLE,CITIES_TABLE,前两个分别对应到上面的Alarm和AlarmInstance。

  1. private static void createAlarmsTable(SQLiteDatabase db) {  
  2.     db.execSQL("CREATE TABLE " + ALARMS_TABLE_NAME + " (" +  
  3.             ClockContract.AlarmsColumns._ID + " INTEGER PRIMARY KEY," +  
  4.             ClockContract.AlarmsColumns.HOUR + " INTEGER NOT NULL, " +  
  5.             ClockContract.AlarmsColumns.MINUTES + " INTEGER NOT NULL, " +  
  6.             ClockContract.AlarmsColumns.DAYS_OF_WEEK + " INTEGER NOT NULL, " +  
  7.             ClockContract.AlarmsColumns.ENABLED + " INTEGER NOT NULL, " +  
  8.             ClockContract.AlarmsColumns.VIBRATE + " INTEGER NOT NULL, " +  
  9.             ClockContract.AlarmsColumns.LABEL + " TEXT NOT NULL, " +  
  10.             ClockContract.AlarmsColumns.RINGTONE + " TEXT, " +  
  11.             ClockContract.AlarmsColumns.DELETE_AFTER_USE + " INTEGER NOT NULL DEFAULT 0);");  
  12.     Log.i("Alarms Table created");  
  13. }  
  1. private static void createInstanceTable(SQLiteDatabase db) {  
  2.     db.execSQL("CREATE TABLE " + INSTANCES_TABLE_NAME + " (" +  
  3.             ClockContract.InstancesColumns._ID + " INTEGER PRIMARY KEY," +  
  4.             ClockContract.InstancesColumns.YEAR + " INTEGER NOT NULL, " +  
  5.             ClockContract.InstancesColumns.MONTH + " INTEGER NOT NULL, " +  
  6.             ClockContract.InstancesColumns.DAY + " INTEGER NOT NULL, " +  
  7.             ClockContract.InstancesColumns.HOUR + " INTEGER NOT NULL, " +  
  8.             ClockContract.InstancesColumns.MINUTES + " INTEGER NOT NULL, " +  
  9.             ClockContract.InstancesColumns.VIBRATE + " INTEGER NOT NULL, " +  
  10.             ClockContract.InstancesColumns.LABEL + " TEXT NOT NULL, " +  
  11.             ClockContract.InstancesColumns.RINGTONE + " TEXT, " +  
  12.             ClockContract.InstancesColumns.ALARM_STATE + " INTEGER NOT NULL, " +  
  13.             ClockContract.InstancesColumns.ALARM_ID + " INTEGER REFERENCES " +  
  14.                 ALARMS_TABLE_NAME + "(" + ClockContract.AlarmsColumns._ID + ") " +  
  15.                 "ON UPDATE CASCADE ON DELETE CASCADE" +  
  16.             ");");  
  17.     Log.i("Instance table created");  
  18. }  

这里说一下几个特殊的字段,对于Alarm表,DAYS_OF_WEEK表示一周内需要定时的天(闹钟有个功能是选择一周中的几天),这里是个int值,用位来表示设置的天数,源码中有个专门的类DaysOfWeek来存储和处理。

AlarmInstance表中有一个ALARM_ID,关联到一个Alarm,可以看到在AlarmInstance表里也有时间,为什么不和Alarm表合成一个表?应该是这样的,Alarm表示原始的定时项,是一个基础数据,而AlarmInstance则代表了一个使用中的定时项目,或者是一个已经激活的定时项目,它的时间是可以变化的,比如闹钟响了以后延时5分钟再响,就需要改变这里的时间,而基础数据不能变,还需要显示在那里。ALARM_STATE代表了当前定时项目的状态,具体调度都在AlarmStateManager中管理。

忘了在哪里看到的,“编程最重要的是设计数据结构,接下来是分解各种代码块”。数据结构是基础,就像建筑里的钢筋水泥砖瓦,有了基础的材料后,剩下的工作就是对这些材料处理,也就是设计具体的处理逻辑。


2、具体的类分析


Alarm


从上面也可以看出,Alarm类作为定时的基础数据结构,主要是封装了一些数据库操作,完成增删改查功能。额外有一个方法createInstanceAfter,根据自身来创建一个AlarmInstance实例。代码如下

  1. public AlarmInstance createInstanceAfter(Calendar time) {  
  2.     Calendar nextInstanceTime = Calendar.getInstance();  
  3.     nextInstanceTime.set(Calendar.YEAR, time.get(Calendar.YEAR));  
  4.     nextInstanceTime.set(Calendar.MONTH, time.get(Calendar.MONTH));  
  5.     nextInstanceTime.set(Calendar.DAY_OF_MONTH, time.get(Calendar.DAY_OF_MONTH));  
  6.     nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour);  
  7.     nextInstanceTime.set(Calendar.MINUTE, minutes);  
  8.     nextInstanceTime.set(Calendar.SECOND, 0);  
  9.     nextInstanceTime.set(Calendar.MILLISECOND, 0);  
  10.   
  11.     // If we are still behind the passed in time, then add a day  
  12.     if (nextInstanceTime.getTimeInMillis() <= time.getTimeInMillis()) {  
  13.         nextInstanceTime.add(Calendar.DAY_OF_YEAR, 1);  
  14.     }  
  15.   
  16.     // The day of the week might be invalid, so find next valid one  
  17.     int addDays = daysOfWeek.calculateDaysToNextAlarm(nextInstanceTime);  
  18.     if (addDays > 0) {  
  19.         nextInstanceTime.add(Calendar.DAY_OF_WEEK, addDays);  
  20.     }  
  21.   
  22.     AlarmInstance result = new AlarmInstance(nextInstanceTime, id);  
  23.     result.mVibrate = vibrate;  
  24.     result.mLabel = label;  
  25.     result.mRingtone = alert;  
  26.     return result;  
  27. }  

AlarmInstance

AlarmInstance与Alarm很相似,像Alarm中的增删改查操作在AlarmInstance中都有相似的方法。那有什么不同呢,就是上面说的AlarmInstance的时间是可以根据当前状态改变的,也就多了时间的set和get方法。

  1. public void setAlarmTime(Calendar calendar) {  
  2.     mYear = calendar.get(Calendar.YEAR);  
  3.     mMonth = calendar.get(Calendar.MONTH);  
  4.     mDay = calendar.get(Calendar.DAY_OF_MONTH);  
  5.     mHour = calendar.get(Calendar.HOUR_OF_DAY);  
  6.     mMinute = calendar.get(Calendar.MINUTE);  
  7. }  
  8.   
  9. /** 
  10.  * Return the time when a alarm should fire. 
  11.  * 
  12.  * @return the time 
  13.  */  
  14. public Calendar getAlarmTime() {  
  15.     Calendar calendar = Calendar.getInstance();  
  16.     calendar.set(Calendar.YEAR, mYear);  
  17.     calendar.set(Calendar.MONTH, mMonth);  
  18.     calendar.set(Calendar.DAY_OF_MONTH, mDay);  
  19.     calendar.set(Calendar.HOUR_OF_DAY, mHour);  
  20.     calendar.set(Calendar.MINUTE, mMinute);  
  21.     calendar.set(Calendar.SECOND, 0);  
  22.     calendar.set(Calendar.MILLISECOND, 0);  
  23.     return calendar;  
  24. }  

AlarmStateManager

闹钟定时的核心逻辑就在这里,AlarmStateManager就是管理所有定时项目状态的调度器。



可以看到上面大多是static类型的方法,用于设置各种状态值。

先看一下定时的几种状态:

SILENT_STATE,alarm被激活,但是不需要显示任何东西,下一个状态是LOW_NOTIFICATION_STATE;

LOW_NOTIFICATION_STATE,这个状态表示alarm离触发的时间不远了,时间差是AlarmInstance.LOW_NOTIFICATION_HOUR_OFFSET=-2,也就是2个小时。下一个状态会进入HIGH_NOTIFICATION_STATE,HIDE_NOTIFICATION_STATE,DISMISS_STATE;

HIDE_NOTIFICATION_STATE,这是一个暂时态,表示用户想隐藏掉通知,这个状态会一直持续到HIGH_NOTIFICATION_STATE;

HIGH_NOTIFICATION_STATE,这个状态和LOW_NOTIFICATION_STATE相似,但不允许用户隐藏通知,负责触发FIRED_STATE或者DISMISS_STATE;

SNOOZED_STATE,像HIGH_NOTIFICATION_STATE,但是会增加一点定时的时间来完成延迟功能;

FIRED_STATE,表示响铃状态,会启动AlarmService直到用户将其变为SNOOZED_STATE或者DISMISS_STATE,如果用户放任不管,会之后进入MISSED_STATE;

MISSED_STATE,这个状态在FIRED_STATE之后,会在通知栏给出一个提醒刚才响铃了;

DISMISS_STATE,这个状态表示定时结束了,会根据定时项目的设置判断是否需要重复,从而决定要删除这个项目还是继续设定一个新的定时。

上面的 setXXXState 方法就是对这些状态的处理,同时会规划一个定时转换到下一个状态。比如setSilentState:

  1. public static void setSilentState(Context context, AlarmInstance instance) {  
  2.         Log.v("Setting silent state to instance " + instance.mId);  
  3.   
  4.         // Update alarm in db  
  5.         ContentResolver contentResolver = context.getContentResolver();  
  6.         instance.mAlarmState = AlarmInstance.SILENT_STATE;  
  7.         AlarmInstance.updateInstance(contentResolver, instance);  
  8.   
  9.         // Setup instance notification and scheduling timers  
  10.         AlarmNotifications.clearNotification(context, instance);  
  11.         scheduleInstanceStateChange(context, instance.getLowNotificationTime(),  
  12.                 instance, AlarmInstance.LOW_NOTIFICATION_STATE);  
  13.     }  

更新AlarmInstance的信息,同时通过scheduleInstanceStateChange()规划下一个状态:

  1. private static void scheduleInstanceStateChange(Context context, Calendar time,  
  2.             AlarmInstance instance, int newState) {  
  3.         long timeInMillis = time.getTimeInMillis();  
  4.         Log.v("Scheduling state change " + newState + " to instance " + instance.mId +  
  5.                 " at " + AlarmUtils.getFormattedTime(context, time) + " (" + timeInMillis + ")");  
  6.         Intent stateChangeIntent = createStateChangeIntent(context, ALARM_MANAGER_TAG, instance,  
  7.                 newState);  
  8.         PendingIntent pendingIntent = PendingIntent.getBroadcast(context, instance.hashCode(),  
  9.                 stateChangeIntent, PendingIntent.FLAG_UPDATE_CURRENT);  
  10.   
  11.         AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);  
  12.         if (Utils.isKitKatOrLater()) {  
  13.             am.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);  
  14.         } else {  
  15.             am.set(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);  
  16.         }  
  17.     }  

通过AlarmManager发起一个定时,定时的时间从调用处可以看到是有AlarmInstance得到的,比如在setSilentState()中的定时时间是instance.getLowNotificationTime():

  1. public Calendar getLowNotificationTime() {  
  2.         Calendar calendar = getAlarmTime();  
  3.         calendar.add(Calendar.HOUR_OF_DAY, LOW_NOTIFICATION_HOUR_OFFSET);  
  4.         return calendar;  
  5.     }  

LOW_NOTIFICATION_HOUR_OFFSET值为-2,也就是在闹铃响之前的两小时那一刻会发这个LOW_NOTIFICATION_STATE的广播出来,AlarmStateManager接收到这个广播处理再转移到下一个。广播的接收在onReciever方法中,

  1.     @Override  
  2.     public void onReceive(final Context context, final Intent intent) {  
  3.         final PendingResult result = goAsync();  
  4.         final PowerManager.WakeLock wl = AlarmAlertWakeLock.createPartialWakeLock(context);  
  5.         wl.acquire();  
  6.         AsyncHandler.post(new Runnable() {  
  7.             @Override  
  8.             public void run() {  
  9.                 handleIntent(context, intent);  
  10.                 result.finish();  
  11.                 wl.release();  
  12.             }  
  13.         });  
  14.     }  
  15.   
  16.     private void handleIntent(Context context, Intent intent) {  
  17.         final String action = intent.getAction();  
  18.         Log.v("AlarmStateManager received intent " + intent);  
  19.         if (CHANGE_STATE_ACTION.equals(action)) {  
  20.             Uri uri = intent.getData();  
  21.             AlarmInstance instance = AlarmInstance.getInstance(context.getContentResolver(),  
  22.                     AlarmInstance.getId(uri));  
  23.             if (instance == null) {  
  24.                 // Not a big deal, but it shouldn't happen  
  25.                 Log.e("Can not change state for unknown instance: " + uri);  
  26.                 return;  
  27.             }  
  28.   
  29.             int globalId = getGlobalIntentId(context);  
  30.             int intentId = intent.getIntExtra(ALARM_GLOBAL_ID_EXTRA, -1);  
  31.             int alarmState = intent.getIntExtra(ALARM_STATE_EXTRA, -1);  
  32.             if (intentId != globalId) {  
  33.                 Log.i("Ignoring old Intent. IntentId: " + intentId + " GlobalId: " + globalId +  
  34.                         " AlarmState: " + alarmState);  
  35.                 return;  
  36.             }  
  37.   
  38.             if (alarmState >= 0) {  
  39.                 setAlarmState(context, instance, alarmState);  
  40.             } else {  
  41.                 registerInstance(context, instance, true);  
  42.             }  
  43.         } else if (SHOW_AND_DISMISS_ALARM_ACTION.equals(action)) {  
  44.             Uri uri = intent.getData();  
  45.             AlarmInstance instance = AlarmInstance.getInstance(context.getContentResolver(),  
  46.                     AlarmInstance.getId(uri));  
  47.   
  48.             long alarmId = instance.mAlarmId == null ? Alarm.INVALID_ID : instance.mAlarmId;  
  49.             Intent viewAlarmIntent = Alarm.createIntent(context, DeskClock.class, alarmId);  
  50.             viewAlarmIntent.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.ALARM_TAB_INDEX);  
  51.             viewAlarmIntent.putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, alarmId);  
  52.             viewAlarmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);  
  53.             context.startActivity(viewAlarmIntent);  
  54.             setDismissState(context, instance);  
  55.         }  
  56.     }  
  57. }  

在handleIntent方法中统一处理,状态的分发在setAlarmState中:

  1. public void setAlarmState(Context context, AlarmInstance instance, int state) {  
  2.         switch(state) {  
  3.             case AlarmInstance.SILENT_STATE:  
  4.                 setSilentState(context, instance);  
  5.                 break;  
  6.             case AlarmInstance.LOW_NOTIFICATION_STATE:  
  7.                 setLowNotificationState(context, instance);  
  8.                 break;  
  9.             case AlarmInstance.HIDE_NOTIFICATION_STATE:  
  10.                 setHideNotificationState(context, instance);  
  11.                 break;  
  12.             case AlarmInstance.HIGH_NOTIFICATION_STATE:  
  13.                 setHighNotificationState(context, instance);  
  14.                 break;  
  15.             case AlarmInstance.FIRED_STATE:  
  16.                 setFiredState(context, instance);  
  17.                 break;  
  18.             case AlarmInstance.SNOOZE_STATE:  
  19.                 setSnoozeState(context, instance);  
  20.                 break;  
  21.             case AlarmInstance.MISSED_STATE:  
  22.                 setMissedState(context, instance);  
  23.                 break;  
  24.             case AlarmInstance.DISMISSED_STATE:  
  25.                 setDismissState(context, instance);  
  26.                 break;  
  27.             default:  
  28.                 Log.e("Trying to change to unknown alarm state: " + state);  
  29.         }  
  30.     }  

对没一个state又转移相应的setXXXState方法中,完成下一次状态的转换,形成一个定时的循环,直到在DISMISSED_STATE里停用或者删除定时项目,如果需要重复则获取下一次定时的时间。

整体的框架就是这样,在AlarmStateManager里使用AlarmManager形成了一个定时的状态机,不断转移到下一个状态处理。

源码在这里https://android.googlesource.com/platform/packages/apps/DeskClock/+/android-4.4.4_r2.0.1

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
Android小技巧之无限循环的ViewPager
前言 之所以会写着篇文章的原因是我现在项目用运用到了广告轮播(BannerView),当时在赶项目的时候在github上面找到了符合的开 源库 就直接引用了,但是该开源库稍微有点庞大,功能比较繁多。
973 0
android小技巧之不缓存的ViewPager
前言 在开发中我们会经常用到ViewPager这个类,比如引导页的啦,主页啦,等等之类的。 一般情况下,我在ViewPger中都是放的Framgnt,这样简单有方便,但是ViewPager却会默认的缓存当前页面的最近两个页面,于是问题就产生了,我们的需...
1820 0
Android开发之Loader与LoaderManager
Loader是什么,有什么作用? 顾名思义就是加载器,简单来说,Loader做了2件事: (1)在单独的线程中读取数据,不会阻塞UI线程 (2)监视数据的更新 LoaderManager是什么,有什么作用? LoaderManager就是加载器的管理器,一个LoaderManager可以管理一个或多个Loader,一个Activity或者Fragment只能有一个LoadManager。
684 0
Android开发之ViewPager简单使用
什么是ViewPager? ViewPager(android.support.v4.view.ViewPager)是android扩展包v4包中的类,这个类可以让用户左右切换当前的view,实现滑动切换的效果,在使用这个类之前,必须明白: ViewPager类直接继承了ViewGroup类,也就是说它和我们经常打交道的LinearLayout一样,都是一个容器,需要在里面添加我们想要显示的内容。
729 0
Android ViewPager轮播图
  Android客户端开发中很多时候需要用到轮播图的方式进行重点新闻的推送或者欢迎页面的制作,下面这个轮播图效果的Deamo来自互联网再经过修改而成。   1、布局文件activity_main.
1166 0
+关注
文章
问答
文章排行榜
最热
最新
相关电子书
更多
Android组件化实现
立即下载
Android插件化:从入门到放弃
立即下载
《深入探索Android热修复技术原理》
立即下载