Android 4.0 Alarm机制浅析

Android 4.0  Alarm机制浅析

Author: [email protected]

最近在做关于Alarm的一些东西,所以就把Android平台上的alarm的源代码给稍微看了看。我个人其实基本不写文档的,而且即使写,也不过区区数字,这个应该是我工作4年来的第二篇文档(第一篇是写的和我一直以来工作相关的Messaging)所以内容上和排版上大家就不要见怪。

Android系统中alarm机制最大的体现着就是闹钟这个app了。通过这个应用我们可以设置自己的各种定时闹钟,当然系统中的各种定时相关功能的实现也基本全部依赖Alarm机制。

闹钟的代码在packages\apps\DeskClock\src\com\android\deskclock目录下,可以自行查看,这里主要说的是Alarm机制。

          

Alarm机制实现的代码主要在

./frameworks/base/core/java/android/app/AlarmManager.java
./frameworks/base/services/java/com/android/server/AlarmManagerService.java
./frameworks/base/services/jni/com_android_server_AlarmManagerService.cpp

再往下就是驱动和kernel的代码,个人对驱动和kernel的代码不了解,就不说了。

AlarmManagerframework中提供给用户来使用的API,其实现在AlarmManagerService,为一个server,通过binder机制来提供服务,开机便注册到system_server进程中(所有server实现基本都如此)代码如下(systemserver.java

            alarm = new AlarmManagerService(context);

            ServiceManager.addService(Context.ALARM_SERVICE, alarm);

下面就来介绍一下AlarmManagerService,本来想用ams代替,不过一般情况下ams指的是ActivityManagerService,所以也就罢了。

AlarmManagerService的初始化:

1. mDescriptor = init();  打开设备驱动,其jni实现为(com_android_server_AlarmManagerService.cpp

static jint android_server_AlarmManagerService_init(JNIEnv* env, jobject obj)

{

    return open("/dev/alarm", O_RDWR);

}

2. 设置时区

        String tz = SystemProperties.get(TIMEZONE_PROPERTY);

        if (tz != null) {

            setTimeZone(tz);

        }

3. mTimeTickSender   这个pendingintent的作用应该是系统中经常用到的,它是用来给发送一个时间改变的broadcastIntent.ACTION_TIME_TICK,每整数分钟的开始发送一次,就是每分钟的开始就发送,应用可以注册对应的receiver来干各种事,譬如更新时间显示等等,具体怎么触发的稍后再说。

mDateChangeSender  这个pendingintent的作用是啥?代码中时这样写的Intent.ACTION_DATE_CHANGED,其实和mTimeTickSender   差不多,只是它是每天的开始发送一次,应该就是00:00:00发送吧

2pendingintent ClockReceiver有莫大的关系,ClockReceiver的构造函数如下

        public ClockReceiver() {

            IntentFilter filter = new IntentFilter();

            filter.addAction(Intent.ACTION_TIME_TICK);

            filter.addAction(Intent.ACTION_DATE_CHANGED);

            mContext.registerReceiver(this, filter);

        }

同时alarmmanagerservice中还有如下代码

 mClockReceiver.scheduleTimeTickEvent();

         mClockReceiver.scheduleDateChangedEvent();

深入scheduleTimeTickEvent 和scheduleDateChangedEvent你就可以知道上面2pendingintent的作用了

同时ClockReceiver也能收到这2intent,说明时间变了,立刻set下一次alarm,以便系统不停的发送该消息。

4. mUninstallReceiver 这个还真暂时太不清楚它的作用。貌似和应用的安装和卸载有比较大的关系。

5. registerReceiver(我就不说这个receiver名字起得太2了),看他的intent filter

        mIntentFilter.addAction(UNREGISTER_POWEROFF_CLOCK);

        mIntentFilter.addAction(REGISTER_POWEROFF_CLOCK);

我们可以得知,这个是给应用来注册POWEROFF_CLOCK的,也就是关机闹钟啦,这个貌似是4.0新加的功能,不知道是qualcomm实现的还是google新添加的代码。

它允许用户注册关机闹钟的权限,在关机情况下,当时间到了以后(可能会提前2分钟 什么的),系统会先开机,然后执行你注册的pendingintent。貌似你需要在   Intent 中设置extraextraPOWEROFF_CLOCK_INTENT_EXTRA)内容为package的名字。

如果你的应用没有通过REGISTER_POWEROFF_CLOCK去注册这个权限的话,那么应用是不会在关机时候去开机执行的。这里的注册只是类似于权限的注册,闹钟设置还是需要调用set去实现。

问题:如果关机后开机,那先前设置的闹钟或者先前设置的alarm(不是指闹钟这个应用,是指定时任务)你认为还有效么?why

6. mWaitThread.start()  (AlarmThread

这个应该是AlarmManagerService中最重要的部分了,整个server的执行线程,跑在一个while死循环里面。具体实现了哪些功能以及怎么实现的,后面具体讲到

至此,初始化完毕了。下次就看看AlarmThread 这个用来支撑Alarm机制实现的线程,来看看它是怎么运作的。

AlarmThread的具体流程:

1. 首先,int result = waitForAlarm(mDescriptor); 这个我个人理解其作用就是等待一个底层RTC闹钟的触发,这个过程应该是同步阻塞的。

如果result & TIME_CHANGED_MASK,那么首先remove(mTimeTickSender);然后重新设置mClockReceiver.scheduleTimeTickEvent();就是重新设置scheduleTimeTickEvent这个pendingintent了;然后给系统发送一个ACTION_TIME_CHANGEDbroadcast,当然接受者是有Intent.FLAG_RECEIVER_REPLACE_PENDING  Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT的限制。

2. 针对各种情况的MASK RTC_WAKEUP_MASK| RTC_MASK| ELAPSED_REALTIME_WAKEUP_MASK| ELAPSED_REALTIME_MASK,对此进行

triggerAlarmsLocked(ArrayList alarmList, ArrayList triggerList,

                                     long now)

3. 我们就来分析下triggerAlarmsLocked 这个函数的作用。

AlarmManager中定义了4种类型的alarm RTC_WAKEUP |RTC  ELAPSED_REALTIME_WAKEUP   ELAPSED_REALTIME,所以在service中定义了4ArrayList 来对应4中类型的alarmmRtcWakeupAlarms|mRtcAlarmsmElapsedRealtimeWakeupAlarms|mElapsedRealtimeAlarms;当应用调用AlarmManager.set接口去设置alarm,随后就会调用到service中的addAlarmLocked,其根据alarm类型将其add到对应的ArrayList中去。

首先来判断alarm是否到期了,如果还没有到期,直接跳出整个while循环(大家注意这里alarmList是个arraylist,同时其在add的时候对其进行了按照alarm.when从小到大来排序,所以如果alarm.whe>now,那么后面的alarm.when必定> now);

if (alarm.when > now) {

                // don't fire alarms in the future

                break;

            }

while里面,我们逐一的找出alarm.when <=nowalarm,将其addtriggerListtriggerList.add(alarm);中传出去,以便后用;同时将其从alarmListremove掉。这里面还有一个alarm.count,看说明大家就知道其中的意思了:                this adjustment will be zero if we're late byless than one full repeat interval),就是说闹钟过期了多少个间隔时间段,计算方法:时间差/间隔 + 1

alarm.count += (now - alarm.when) / alarm.repeatInterval           如果是重复的alarm,那么将其保存在repeats这个arraylist中。

// if it repeats queue it up to be read-added to the list

if (alarm.repeatInterval > 0) {

repeats.add(alarm);

遍历完了alarmList之后,做2件事:

①:把repeats 这个arraylist中的重复响的闹钟计算出新的alarm,将其addalarm对应的arraylist中去。

②:setLocked,将alarmList中最近的一个闹钟(其按照从小到大排列,故第一个就是最小的)set到系统中去。

好了,这个函数的作用分析完了。简而言之,这个函数作用就是找出对应alarmlist中到期的alarm,将其取出来;同时将重复的alarm计算出新的alarm添加到对应的list中,然后set最近时间的alarm

4. 执行完triggerAlarmsLocked后,我们得到了需要进行操作的triggerList,逐一取出,随后就开始了最为重要的一部分

                            alarm.operation.send(mContext, 0,

                                    mBackgroundIntent.putExtra(

                                            Intent.EXTRA_ALARM_COUNT, alarm.count),

                                    mResultReceiver, mHandler);

pendingintent发送出去,这样,先前注册alarm到期时所期望做的的操作这个时候就开始执行了。

AlarmThread的执行流程就是这样,它一直重复的等待着底层alarm到期,然后从列表中取出到期的alarm,逐一对pendingintent进行send操作,直到系统挂了为止。

AlarmManagerService中重要的操作:

1. set/setRepeating:设置(重复的)alarm

外部如果需要设置一个alarm来进行某些操作,一般流程都是

manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);

manager.set(AlarmManager.RTC_WAKEUP, time, pi);

一般都会用AlarmManager这个代理类来进行对应的操作,其实质会调用到AlarmManagerService中的set/setRepeating函数,当然set其实也是调用setRepeating,所以我们就来看看setRepeating这个函数

setRepeating(int type, long triggerAtTime, long interval, PendingIntent operation),这个函数有4个参数,其中大家要注意的是triggerAtTimetype要对应起来,我们用RTC_WAKEUPRTC的时候,就要对应系统的绝对时间(System.currentTimeMillis()得到);而用ELAPSED_REALTIME_WAKEUPELAPSED_REALTIME的时候,就要对应系统的相对时间(相对开机流逝了多少时间通过SystemClock.elapsedRealtime()得到),interval来表示间隔interval时间间隔重复该alarmoperation就是alarm到期后你要进行的操作。

①:创建一个Alarm来组织传进来的数据,随后进行的这一步大家要稍微注意:

           // Remove this alarm if already scheduled.

            removeLocked(operation);

该函数会从type对应的alarm arraylistremove掉此operationpendingintent)对应的alarm,其中匹配规则为alarm.operation.equals(operation),也就是pendingintentequal函数。所以这个大家可能要注意,因为如果你在一个app中不同的地方new pendingintent的时候的参数都一样的话,那么new出来的pendingintent通过pendingintent.equal来比较的话是相等的,也就是说你这个时候不能通过一般途径来设置2alarm,因为这个时候会把前一个alarm移除掉,具体可以参见pendingintent.equal()以及pendingintent.mTarget怎么创建的,其实就是比较mTarget是否相等,如果你要设置2alarm的话,就要用不同的requestcode或者intent

②:执行完remove后,开始执行

         index = addAlarmLocked(alarm);

此函数的作用首先通过Collections.binarySearch(alarmList, alarm, mIncreasingTimeOrder)将此alarm定位出其在alarmlist(此arraylist为按照alarm.when从小到大排列)的位置index,然后将该alarm添加到对应的alarm arraylist中去 alarmList.add(index, alarm);同时返回该index(后面还要用)。

③:接下来,会去判断调用setapp是否为系统自带的闹钟或者是在系统中注册了power_off_clock权限的appalarmmanagerservice初始化中讲到过),如果是,并且alarm.type == AlarmManager.RTC_WAKEUP,那么newType = RTC_DEVICEUP

接着看:if (index == 0 || newType == RTC_DEVICEUP) {     setLocked(alarm);   }

也就是说如果该alarm是最进将要触发的alarmindex =0)或者有power_off_clock权限的alarm,那么将立刻执行setLocked操作,也就是设置到系统底层RTC时钟中去。

setLocked函数中同样也有关于power_off_clock权限的检测,代码如下

String callingPackage =       mContext.getPackageManager().getNameForUid(Binder.getCallingUid());

if (SELF_CLOCK.equals(callingPackage) && alarm.type == AlarmManager.RTC_WAKEUP) {

            type = RTC_DEVICEUP;

 }

SharedPreferences mSharedPref = mContext.getSharedPreferences("power_off_clock", 0);

        if (mSharedPref.contains(callingPackage) && alarm.type == AlarmManager.RTC_WAKEUP) {

            type = RTC_DEVICEUP;

}

随后就会通过jni调用底层的set函数,这里不多说。

2. remove:取消alarm

取消一个alarm的流程那就太simple了,直接removeLocked(operation),这个过程在和setremove相同。通过pendingintent.equal来确定2alarm是否相等。

3. setTime:设置系统时间

该功能需要android.permission.SET_TIME,需要注意。

4. setTimeZone:设置系统时区

该功能需要android.permission. SET_TIME_ZONE,需要注意。

疑问:

1.关机alarm问题

在初始化分析中,提出了这样一个问题:如果关机后开机,那先前设置的闹钟或者先前设置的alarm(不是指闹钟这个应用,是指定时任务)你认为还有效么?why

有人觉得有效,因为闹钟不就是个很好的例子么?也有人说无效,因为自己set一个alarm,重启到时间后对应的pendingintent并没有执行。那么到底是有效还是无效呢?在这里,我很负责任(个人责任)的告诉大家,是无效的。

Why

其实我们set alarm最终调用的是jni层的set函数,看看这个函数的参数          set(int fd, int type, long seconds, long nanoseconds);我们很容易发现并没有将pendingintent保存起来,pendingintent只是保存在arraylistAlarm结构体中,同时重启后,先前arraylist的数据并没有保存到固化空间上,全部都丢失掉了,所以重启后刚开始的arraylist是空的。那么你以前设置的alarm显然就无效啦。

那为什么系统的闹钟有效呢?带着这个问题我问了一些同事,但是没有什么发现。最后我在闹钟的app中发现了这个ReceiverAlarmInitReceiver

看到这里,你明白了吧?闹钟这个app接收了开机完成事件,然后将其保存在数据库中的alarm数据重新setAlarmManagerService(具体看AlarmInitReceiver.java),所以闹钟关机重启还有效。

如果你的程序需要设置定时任务,并且不管系统是否关机重启等,那么你就可以仿照闹钟注册开机完成(BOOT_CVOMPLETED)事件,重新set alarm(其实我是偶然看到SMP同事写的定时信息feature想到的这个问题,鉴定重启后果然不能定时发送)。

2.pendingintent 比较问题

由于在setremove alarm的时候都会执行removeLocked(operation)的操作,其中涉及到pendingintent的比较问题,pendingintent.equal函数比较2pendingintent其实只是比较2pendingintentmTarget,所以即使2pendingintent不相等,但是pendingintent .equal确实相等。

PendingIntent p1 = PendingIntent.getBroadcast(this, 0, new Intent("test1"), 1);

PendingIntent p2 = PendingIntent.getBroadcast(this, 0, new Intent("test1"), 1);

此时 p1 != p2,但是p1.equals(p2) == true

所以大家需要设置不同的alarm的时候,new PendingIntent时候需要注意用不同的参数,requestCodeintentflags 有一个不同即可。

注意 :如果FlagFLAG_CANCEL_CURRENT的话,那么不管怎么样,p1.equals(p2) == false

注意影响PendingIntent相等的参数就是这个key

(这里的意思是这3Flagkey中不起作用)

如果Flag中有FLAG_CANCEL_CURRENT的话,那么将会执行:

以及

所以就会造成不相等的情况。

这里就有一个很严重的问题了啊,既然用了FLAG_CANCEL_CURRENT,那么为什么我先前设置了一个10点钟的闹钟,再去取消的时候,为什么能够取消?不是上面刚说了2pendingintent不相等吗?这个问题我断断续续看了很长时间,今天给别人讲闹钟的设置取消和AlarmManagerService的时候才总算是弄明白了。

让我们来看一下取消闹钟的代码

先去得到一个pendingintent,大家可要注意啊,这个sender和我们设置闹钟时候得到的sender不相等啊,并且其中的mTarget对象也不相等啊,真是急死人。都不相等了,你还去am.set,am.cancel有什么作用?我可以负责任的告诉大家,这2个函数在这里完全是打酱油的作用,完全可以去掉。为什么android的源代码这样设计,估计也是为了我们自己看着这样可以cancel掉吧。

Set我就不讲了,我们来看看cancel,最终调用的是AlarmManagerServiceremove函数

OMG,就是想从4个列表中remove掉和传进去的sender相等的pendingintent对应的alarm,关键是这个sender不和其中任何一个相等啊,因为你用了这个该死的FLAG啊亲FLAG_CANCEL_CURRENT,所以我说这2行代码完全可以去掉。这2行代码本意是好的,想去remove掉对应的alarm,可以效果你懂的,哎,大家不信可以去跟跟断点,打打log,或者dumpsys alarm看看alarm的信息即可。

既然这2行代码没有去remove掉这个alarm,那为什么我咱闹钟中取消某个闹钟后,起作用了呢。这个得从pendingintent的创建说起了。当我们设置了FLAG_CANCEL_CURRENT之后,然后想去get到一个pendingintent,具体的实现代码在AMS中的getIntentSenderLocked这个函数,上面已经讲了一部分,当我们设置了该FLAG后,将会执行

请着重注意这行代码,因为这个值在pendingintent触发的时候起到至关重要的作用,我们看看在AlarmManagerService中的当一个alarm时间到了,便会触发下面的操作:

也就是pendingintentsend操作,最终会调用到PendingIntentRecordsend操作(具体是sendInner,不要问我为什么,不懂的可以去看下binder机制),请大家睁大眼睛啊:

各种操作

                   

看清楚没有啊亲,也就是说你先前设置的pendingintent虽然在AlarmManagerService中,但是走到send的时候其实是什么操作都没做啊,所以这个就是为什么闹钟不响了啊。

OKAlarmManagerService浅析就差不多这些了。

OVER了,如果你对此有什么疑问或者见解,那么可以和我一起讨论。此文纯个人理解,有错误在所难免,所以请大家切勿轻信,欢迎指正([email protected])。

你可能感兴趣的:(Android 4.0 Alarm机制浅析)