本次给大家分析的是Android中Alarm的机制所用源码为最新的Android4.4.4。首先简单介绍如何使用Alarm并给出其工作原理,接着分析Alarm和Timer以及Handler在完成定时任务上的差别,最后分析Alarm机制的源码。
什么是Alarm
Alarm是android提供的用于完成闹钟式定时任务的类,系统通过AlarmManager来管理所有的Alarm,Alarm支持一次性定时任务和循环定时任务,它的使用方式很简单,这里不多做介绍,只给出一个简单的示例:
- AlarmManageralarmMgr=(AlarmManager)getSystemService(Context.ALARM_SERVICE);
- Intentintent=newIntent(getApplicationContext(),TestActivity.class);
- PendingIntentpendIntent=PendingIntent.getActivity(getApplicationContext(),
- 0,intent,PendingIntent.FLAG_UPDATE_CURRENT);
- //5秒后发送广播,只发送一次
- inttriggerAtTime=SystemClock.elapsedRealtime()+5*1000;
- alarmMgr.set(AlarmManager.ELAPSED_REALTIME,triggerAtTime,pendIntent);
Alarm和Timer以及Handler在定时任务上的区别
相同点:
三者都可以完成定时任务,都支持一次性定时和循环定时(注:Handler可以间接支持循环定时任务)
不同点:
Handler和Timer在定时上是类似的,二者在系统休眠的情况下无法正常工作,定时任务不会按时触发。Alarm在系统休眠的情况下可以正常工作,并且还可以决定是否唤醒系统,同时Alarm在自身不启动的情况下仍能正常收到定时任务提醒,但是当系统重启或者应用被杀死的情况下,Alarm定时任务会被取消。另外,从Android4.4开始,Alarm事件默认采用非精准方式,即定时任务可能会有小范围的提前或延后,当然我们可以强制采用精准方式,而在此之前,Alarm事件都是精准方式。
Alarm与Binder的交互
Alarm由AlarmManager来管理,从使用方式来看,AlarmManager很简单,我们只要得到了AlarmManager的对象,就可以调用set方法来设定定时任务了,而如何得到AlarmManager对象呢?也很简单,AlarmManager alarmMgr = (AlarmManager) getSystemService(Context.ALARM_SERVICE);下面我们去看看AlarmManager的set方法,当然AlarmManager还有setRepeating方法,但是二者是类似的。为了更好地理解下面的内容,需要你了解AIDL,如果你还不了解,请参看android跨进程通信(IPC):使用AIDL。
code:AlarmManager#set
- publicvoidset(inttype,longtriggerAtMillis,PendingIntentoperation){
- setImpl(type,triggerAtMillis,legacyExactLength(),0,operation,null);
- }
- publicvoidset(inttype,longtriggerAtMillis,longwindowMillis,longintervalMillis,
- PendingIntentoperation,WorkSourceworkSource){
- setImpl(type,triggerAtMillis,windowMillis,intervalMillis,operation,workSource);
- }
- privatevoidsetImpl(inttype,longtriggerAtMillis,longwindowMillis,longintervalMillis,
- PendingIntentoperation,WorkSourceworkSource){
- if(triggerAtMillis<0){
- /*NOTYET
- if(mAlwaysExact){
- //FatalerrorforKLP+appstousenegativetriggertimes
- thrownewIllegalArgumentException("Invalidalarmtriggertime"
- +triggerAtMillis);
- }
- */
- triggerAtMillis=0;
- }
- try{
- //定时任务实际上都有mService来完成,也就是说AlarmManager只是一个空壳
- //从下面的构造方法可以看出,这个mService是IAlarmManager类型的,而IAlarmManager是一个接口
- //如果大家了解AIDL就应该知道IAlarmManager应该是一个AIDL接口
- mService.set(type,triggerAtMillis,windowMillis,intervalMillis,operation,
- workSource);
- }catch(RemoteExceptionex){
- }
- }
- AlarmManager(IAlarmManagerservice,Contextctx){
- mService=service;
- finalintsdkVersion=ctx.getApplicationInfo().targetSdkVersion;
- mAlwaysExact=(sdkVersion<Build.VERSION_CODES.KITKAT);
- }
说明:我对代码进行了注释,从注释可以看出,现在我们需要去找到这个mService,其实我已经帮大家找到了,它就是AlarmManagerService
Alarm机制分析
通过上面的一系列分析,我们知道AlarmManager的所有功能都是通过AlarmManagerService来完成的,在分析源码之前,我先来描述下Alarm的工作原理:从Android4.4开始,Alarm默认为非精准模式,除非显示指定采用精准模式。在非精准模式下,Alarm是批量提醒的,每个alarm根据其触发时间和最大触发时间的不同会被加入到不同的batch中,同一个batch的不同alarm是同时发生的,这样就无法实现精准闹钟,官方的解释是批量处理可以减少设备被唤醒次数以及节约电量,不过针对精准闹钟,官方预留的方法是setExact和setWindow,二者都是通过将时间窗口定义为0来实现精准闹钟的,因为时间窗口为0,意味着触发时间和最大触发时间是一样的,因为典型的情况下:最大触发时间= 触发时间 + 时间窗口。同时所有的batch是按开始时间升序排列的,在一个batch内部,不同的闹钟也是按触发时间升序排列的,所以闹钟的唤醒顺序是按照batch的排序依次触发的,而同一个batch中的alarm是同时触发的,可以用下面这个示意图来描述:上图是示意图,系统中可以有多个batch,每个batch中可以有多个alarm。下面我们分析一下AlarmManagerService中的代码。其入口方法为set,set又调用了setImplLocked,所以我们直接看setImplLocked。
code:AlarmManagerService#setImplLocked
- privatevoidsetImplLocked(inttype,longwhen,longwhenElapsed,longmaxWhen,longinterval,
- PendingIntentoperation,booleanisStandalone,booleandoValidate,
- WorkSourceworkSource){
- /**创建一个alarm,其中各参数的含义如下:
- *type闹钟类型ELAPSED_REALTIME、RTC、RTC_WAKEUP等
- *when触发时间UTC类型,绝对时间,通过System.currentTimeMillis()得到
- *whenElapsed相对触发时间,自开机算起,含休眠,通过SystemClock.elapsedRealtime()得到
- *maxWhen最大触发时间
- *interval触发间隔,针对循环闹钟有效
- *operation闹钟触发时的行为,PendingIntent类型
- */
- Alarma=newAlarm(type,when,whenElapsed,maxWhen,interval,operation,workSource);
- //根据PendingIntent删除之前已有的同一个闹钟
- removeLocked(operation);
- booleanreschedule;
- //尝试将alarm加入到合适的batch中,如果alarm是独立的或者无法找到合适的batch去容纳此alarm,返回-1
- intwhichBatch=(isStandalone)?-1:attemptCoalesceLocked(whenElapsed,maxWhen);
- if(whichBatch<0){
- //没有合适的batch去容纳alarm,则新建一个batch
- Batchbatch=newBatch(a);
- batch.standalone=isStandalone;
- //将batch加入mAlarmBatches中,并对mAlarmBatches进行排序:按开始时间升序排列
- reschedule=addBatchLocked(mAlarmBatches,batch);
- }else{
- //如果找到合适了batch去容纳此alarm,则将其加入到batch中
- Batchbatch=mAlarmBatches.get(whichBatch);
- //如果当前alarm的加入引起了batch开始时间和结束时间的改变,则reschedule为true
- reschedule=batch.add(a);
- if(reschedule){
- //由于batch的起始时间发生了改变,所以需要从列表中删除此batch并重新加入、重新对batch列表进行排序
- mAlarmBatches.remove(whichBatch);
- addBatchLocked(mAlarmBatches,batch);
- }
- }
- if(DEBUG_VALIDATE){
- if(doValidate&&!validateConsistencyLocked()){
- Slog.v(TAG,"Tipping-pointoperation:type="+type+"when="+when
- +"when(hex)="+Long.toHexString(when)
- +"whenElapsed="+whenElapsed+"maxWhen="+maxWhen
- +"interval="+interval+"op="+operation
- +"standalone="+isStandalone);
- rebatchAllAlarmsLocked(false);
- reschedule=true;
- }
- }
- if(reschedule){
- rescheduleKernelAlarmsLocked();
- }
- }
说明:通过上述代码可以看出,当我们创建一个alarm的时候,仅仅是将这个alarm加入到某个batch中,系统中有一个batch列表,专门用于存储所有的alarm。可是仅仅把alarm加入到batch中还不行,系统还必须提供一个类似于Looper的东西一直去遍历这个列表,一旦它发现有些alarm的时间已经到达就要把它取出来去执行。事实上,AlarmManagerService中的确有一个类似于Looper的东西去干这个事情,只不过它是个线程,叫做AlarmThread。下面看它的代码:
code:AlarmManagerService#AlarmThread
- privateclassAlarmThreadextendsThread
- {
- publicAlarmThread()
- {
- super("AlarmManager");
- }
- publicvoidrun()
- {
- //当前时间触发的alarm列表
- ArrayList<Alarm>triggerList=newArrayList<Alarm>();
- while(true)
- {
- //jni方法,顾名思义,阻塞式方法,当有alarm的时候会被唤醒
- intresult=waitForAlarm(mDescriptor);
- triggerList.clear();
- if((result&TIME_CHANGED_MASK)!=0){
- if(DEBUG_BATCH){
- Slog.v(TAG,"Timechangednotificationfromkernel;rebatching");
- }
- remove(mTimeTickSender);
- //将所有的alarm重新排序
- rebatchAllAlarms();
- mClockReceiver.scheduleTimeTickEvent();
- Intentintent=newIntent(Intent.ACTION_TIME_CHANGED);
- intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
- |Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
- mContext.sendBroadcastAsUser(intent,UserHandle.ALL);
- }
- synchronized(mLock){
- finallongnowRTC=System.currentTimeMillis();
- finallongnowELAPSED=SystemClock.elapsedRealtime();
- if(localLOGV)Slog.v(
- TAG,"Checkingforalarms...rtc="+nowRTC
- +",elapsed="+nowELAPSED);
- if(WAKEUP_STATS){
- if((result&IS_WAKEUP_MASK)!=0){
- longnewEarliest=nowRTC-RECENT_WAKEUP_PERIOD;
- intn=0;
- for(WakeupEventevent:mRecentWakeups){
- if(event.when>newEarliest)break;
- n++;//numberofnow-staleentriesatthelisthead
- }
- for(inti=0;i<n;i++){
- mRecentWakeups.remove();
- }
- recordWakeupAlarms(mAlarmBatches,nowELAPSED,nowRTC);
- }
- }
- //这个方法会把batch列表中的第一个batch取出来然后加到触发列表中
- //当然,前提是此batch的开始时间不大于当前时间
- //同时,如果是循环闹钟,则会对下次任务进行再次定时
- triggerAlarmsLocked(triggerList,nowELAPSED,nowRTC);
- rescheduleKernelAlarmsLocked();
- //遍历触发列表,发送PendingIntent
- for(inti=0;i<triggerList.size();i++){
- Alarmalarm=triggerList.get(i);
- try{
- if(localLOGV)Slog.v(TAG,"sendingalarm"+alarm);
- //这里PendingIntent会被send,结果就是我们的定时任务被执行了
- alarm.operation.send(mContext,0,
- mBackgroundIntent.putExtra(
- Intent.EXTRA_ALARM_COUNT,alarm.count),
- mResultReceiver,mHandler);
- //wehaveanactivebroadcastsostayawake.
- if(mBroadcastRefCount==0){
- setWakelockWorkSource(alarm.operation,alarm.workSource);
- mWakeLock.acquire();
- }
- finalInFlightinflight=newInFlight(AlarmManagerService.this,
- alarm.operation,alarm.workSource);
- mInFlight.add(inflight);
- mBroadcastRefCount++;
- finalBroadcastStatsbs=inflight.mBroadcastStats;
- bs.count++;
- if(bs.nesting==0){
- bs.nesting=1;
- bs.startTime=nowELAPSED;
- }else{
- bs.nesting++;
- }
- finalFilterStatsfs=inflight.mFilterStats;
- fs.count++;
- if(fs.nesting==0){
- fs.nesting=1;
- fs.startTime=nowELAPSED;
- }else{
- fs.nesting++;
- }
- if(alarm.type==ELAPSED_REALTIME_WAKEUP
- ||alarm.type==RTC_WAKEUP){
- bs.numWakeup++;
- fs.numWakeup++;
- //针对能唤醒设备的闹钟,这里会做一些唤醒设备的事情
- ActivityManagerNative.noteWakeupAlarm(
- alarm.operation);
- }
- }catch(PendingIntent.CanceledExceptione){
- if(alarm.repeatInterval>0){
- //ThisIntentSenderisnolongervalid,butthis
- //isarepeatingalarm,sotossthehoser.
- remove(alarm.operation);
- }
- }catch(RuntimeExceptione){
- Slog.w(TAG,"Failuresendingalarm.",e);
- }
- }
- }
- }
- }
- }
总结
本文没有详细介绍如何使用Alarm,因为很简单,看一下官方文档或者网上搜一下,到处都是。关于Alarm,有一点需要强调一下:当手机重启或者应用被杀死的时候,Alarm会被删除,因此,如果想通过Alarm来完成长久定时任务是不可靠的,如果非要完成长久定时任务,可以这样:将应用的所有Alarm信息存到数据库中,每次应用启动的时候都重新注册Alarm并更新Alarm的触发时间,通过这种方式就不存在Alarm丢失的情况了。本文很长,耗时8个小时才完成的,感谢大家阅读本文,希望本文能给大家带来一点帮助。