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的代码不了解,就不说了。
AlarmManager是framework中提供给用户来使用的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的作用应该是系统中经常用到的,它是用来给发送一个时间改变的broadcast,Intent.ACTION_TIME_TICK,每整数分钟的开始发送一次,就是每分钟的开始就发送,应用可以注册对应的receiver来干各种事,譬如更新时间显示等等,具体怎么触发的稍后再说。
mDateChangeSender 这个pendingintent的作用是啥?代码中时这样写的Intent.ACTION_DATE_CHANGED,其实和mTimeTickSender 差不多,只是它是每天的开始发送一次,应该就是00:00:00发送吧
这2个pendingintent 和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你就可以知道上面2个pendingintent的作用了
同时ClockReceiver也能收到这2个intent,说明时间变了,立刻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 中设置extra,extra(POWEROFF_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_CHANGED的broadcast,当然接受者是有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
long now)
3. 我们就来分析下triggerAlarmsLocked 这个函数的作用。
AlarmManager中定义了4种类型的alarm :RTC_WAKEUP |RTC ELAPSED_REALTIME_WAKEUP | ELAPSED_REALTIME,所以在service中定义了4个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 <=now的alarm,将其add到triggerList(triggerList.add(alarm);)中传出去,以便后用;同时将其从alarmList中remove掉。这里面还有一个alarm.count,看说明大家就知道其中的意思了: (this adjustment will be zero if we're late by,less 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,将其add到alarm对应的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个参数,其中大家要注意的是triggerAtTime和type要对应起来,我们用RTC_WAKEUP和RTC的时候,就要对应系统的绝对时间(System.currentTimeMillis()得到);而用ELAPSED_REALTIME_WAKEUP和ELAPSED_REALTIME的时候,就要对应系统的相对时间(相对开机流逝了多少时间通过SystemClock.elapsedRealtime()得到),interval来表示间隔interval时间间隔重复该alarm,operation就是alarm到期后你要进行的操作。
①:创建一个Alarm来组织传进来的数据,随后进行的这一步大家要稍微注意:
// Remove this alarm if already scheduled.
removeLocked(operation);
该函数会从type对应的alarm arraylist中remove掉此operation(pendingintent)对应的alarm,其中匹配规则为alarm.operation.equals(operation),也就是pendingintent的equal函数。所以这个大家可能要注意,因为如果你在一个app中不同的地方new pendingintent的时候的参数都一样的话,那么new出来的pendingintent通过pendingintent.equal来比较的话是相等的,也就是说你这个时候不能通过一般途径来设置2个alarm,因为这个时候会把前一个alarm移除掉,具体可以参见pendingintent.equal()以及pendingintent.mTarget怎么创建的,其实就是比较mTarget是否相等,如果你要设置2个alarm的话,就要用不同的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(后面还要用)。
③:接下来,会去判断调用set的app是否为系统自带的闹钟或者是在系统中注册了power_off_clock权限的app(alarmmanagerservice初始化中讲到过),如果是,并且alarm.type == AlarmManager.RTC_WAKEUP,那么newType = RTC_DEVICEUP。
接着看:if (index == 0 || newType == RTC_DEVICEUP) { setLocked(alarm); }
也就是说如果该alarm是最进将要触发的alarm(index =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),这个过程在和set中remove相同。通过pendingintent.equal来确定2个alarm是否相等。
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只是保存在arraylist中Alarm结构体中,同时重启后,先前arraylist的数据并没有保存到固化空间上,全部都丢失掉了,所以重启后刚开始的arraylist是空的。那么你以前设置的alarm显然就无效啦。
那为什么系统的闹钟有效呢?带着这个问题我问了一些同事,但是没有什么发现。最后我在闹钟的app中发现了这个Receiver:AlarmInitReceiver,
看到这里,你明白了吧?闹钟这个app接收了开机完成事件,然后将其保存在数据库中的alarm数据重新set到AlarmManagerService(具体看AlarmInitReceiver.java),所以闹钟关机重启还有效。
如果你的程序需要设置定时任务,并且不管系统是否关机重启等,那么你就可以仿照闹钟注册开机完成(BOOT_CVOMPLETED)事件,重新set alarm(其实我是偶然看到SMP同事写的定时信息feature想到的这个问题,鉴定重启后果然不能定时发送)。
2.pendingintent 比较问题
由于在set和remove alarm的时候都会执行removeLocked(operation)的操作,其中涉及到pendingintent的比较问题,pendingintent.equal函数比较2个pendingintent其实只是比较2个pendingintent的mTarget,所以即使2个pendingintent不相等,但是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时候需要注意用不同的参数,requestCode,intent,flags 有一个不同即可。
注意 :如果Flag是FLAG_CANCEL_CURRENT的话,那么不管怎么样,p1.equals(p2) == false。
注意影响PendingIntent相等的参数就是这个key:
(这里的意思是这3个Flag在key中不起作用)
如果Flag中有FLAG_CANCEL_CURRENT的话,那么将会执行:
以及
所以就会造成不相等的情况。
这里就有一个很严重的问题了啊,既然用了FLAG_CANCEL_CURRENT,那么为什么我先前设置了一个10点钟的闹钟,再去取消的时候,为什么能够取消?不是上面刚说了2个pendingintent不相等吗?这个问题我断断续续看了很长时间,今天给别人讲闹钟的设置取消和AlarmManagerService的时候才总算是弄明白了。
让我们来看一下取消闹钟的代码
先去得到一个pendingintent,大家可要注意啊,这个sender和我们设置闹钟时候得到的sender不相等啊,并且其中的mTarget对象也不相等啊,真是急死人。都不相等了,你还去am.set,am.cancel有什么作用?我可以负责任的告诉大家,这2个函数在这里完全是打酱油的作用,完全可以去掉。为什么android的源代码这样设计,估计也是为了我们自己看着这样可以cancel掉吧。
Set我就不讲了,我们来看看cancel,最终调用的是AlarmManagerService的remove函数
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时间到了,便会触发下面的操作:
也就是pendingintent的send操作,最终会调用到PendingIntentRecord的send操作(具体是sendInner,不要问我为什么,不懂的可以去看下binder机制),请大家睁大眼睛啊:
各种操作
看清楚没有啊亲,也就是说你先前设置的pendingintent虽然在AlarmManagerService中,但是走到send的时候其实是什么操作都没做啊,所以这个就是为什么闹钟不响了啊。
OK,AlarmManagerService浅析就差不多这些了。
OVER了,如果你对此有什么疑问或者见解,那么可以和我一起讨论。此文纯个人理解,有错误在所难免,所以请大家切勿轻信,欢迎指正([email protected])。