Android中Alarm的机制

摘要:
这一次,我们将分析Android中Alarm机制的源代码是最新的Android 4.4.4。首先,它简要介绍了如何使用Alarm并给出了其工作原理,然后分析了Alarm、Timer和Handler在完成计时任务方面的差异,最后分析了Alrm机制的源码。此外,从Android 4.4开始,警报事件默认采用不精确模式,即计时任务可能在小范围内提前或延迟。当然,我们可以强制精确模式,但在此之前,警报事件是精确的。

本次给大家分析的是Android中Alarm的机制所用源码为最新的Android4.4.4。首先简单介绍如何使用Alarm并给出其工作原理,接着分析Alarm和Timer以及Handler在完成定时任务上的差别,最后分析Alarm机制的源码。

什么是Alarm

Alarm是android提供的用于完成闹钟式定时任务的类,系统通过AlarmManager来管理所有的Alarm,Alarm支持一次性定时任务和循环定时任务,它的使用方式很简单,这里不多做介绍,只给出一个简单的示例:

  1. AlarmManageralarmMgr=(AlarmManager)getSystemService(Context.ALARM_SERVICE);
  2. Intentintent=newIntent(getApplicationContext(),TestActivity.class);
  3. PendingIntentpendIntent=PendingIntent.getActivity(getApplicationContext(),
  4. 0,intent,PendingIntent.FLAG_UPDATE_CURRENT);
  5. //5秒后发送广播,只发送一次
  6. inttriggerAtTime=SystemClock.elapsedRealtime()+5*1000;
  7. 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

  1. publicvoidset(inttype,longtriggerAtMillis,PendingIntentoperation){
  2. setImpl(type,triggerAtMillis,legacyExactLength(),0,operation,null);
  3. }
  4. publicvoidset(inttype,longtriggerAtMillis,longwindowMillis,longintervalMillis,
  5. PendingIntentoperation,WorkSourceworkSource){
  6. setImpl(type,triggerAtMillis,windowMillis,intervalMillis,operation,workSource);
  7. }
  8. privatevoidsetImpl(inttype,longtriggerAtMillis,longwindowMillis,longintervalMillis,
  9. PendingIntentoperation,WorkSourceworkSource){
  10. if(triggerAtMillis<0){
  11. /*NOTYET
  12. if(mAlwaysExact){
  13. //FatalerrorforKLP+appstousenegativetriggertimes
  14. thrownewIllegalArgumentException("Invalidalarmtriggertime"
  15. +triggerAtMillis);
  16. }
  17. */
  18. triggerAtMillis=0;
  19. }
  20. try{
  21. //定时任务实际上都有mService来完成,也就是说AlarmManager只是一个空壳
  22. //从下面的构造方法可以看出,这个mService是IAlarmManager类型的,而IAlarmManager是一个接口
  23. //如果大家了解AIDL就应该知道IAlarmManager应该是一个AIDL接口
  24. mService.set(type,triggerAtMillis,windowMillis,intervalMillis,operation,
  25. workSource);
  26. }catch(RemoteExceptionex){
  27. }
  28. }
  29. AlarmManager(IAlarmManagerservice,Contextctx){
  30. mService=service;
  31. finalintsdkVersion=ctx.getApplicationInfo().targetSdkVersion;
  32. mAlwaysExact=(sdkVersion<Build.VERSION_CODES.KITKAT);
  33. }

说明:我对代码进行了注释,从注释可以看出,现在我们需要去找到这个mService,其实我已经帮大家找到了,它就是AlarmManagerService


Alarm机制分析

通过上面的一系列分析,我们知道AlarmManager的所有功能都是通过AlarmManagerService来完成的,在分析源码之前,我先来描述下Alarm的工作原理:从Android4.4开始,Alarm默认为非精准模式,除非显示指定采用精准模式。在非精准模式下,Alarm是批量提醒的,每个alarm根据其触发时间和最大触发时间的不同会被加入到不同的batch中,同一个batch的不同alarm是同时发生的,这样就无法实现精准闹钟,官方的解释是批量处理可以减少设备被唤醒次数以及节约电量,不过针对精准闹钟,官方预留的方法是setExact和setWindow,二者都是通过将时间窗口定义为0来实现精准闹钟的,因为时间窗口为0,意味着触发时间和最大触发时间是一样的,因为典型的情况下:最大触发时间= 触发时间 + 时间窗口。同时所有的batch是按开始时间升序排列的,在一个batch内部,不同的闹钟也是按触发时间升序排列的,所以闹钟的唤醒顺序是按照batch的排序依次触发的,而同一个batch中的alarm是同时触发的,可以用下面这个示意图来描述:

Android中Alarm的机制第5张

上图是示意图,系统中可以有多个batch,每个batch中可以有多个alarm。下面我们分析一下AlarmManagerService中的代码。其入口方法为set,set又调用了setImplLocked,所以我们直接看setImplLocked。

code:AlarmManagerService#setImplLocked

  1. privatevoidsetImplLocked(inttype,longwhen,longwhenElapsed,longmaxWhen,longinterval,
  2. PendingIntentoperation,booleanisStandalone,booleandoValidate,
  3. WorkSourceworkSource){
  4. /**创建一个alarm,其中各参数的含义如下:
  5. *type闹钟类型ELAPSED_REALTIME、RTC、RTC_WAKEUP等
  6. *when触发时间UTC类型,绝对时间,通过System.currentTimeMillis()得到
  7. *whenElapsed相对触发时间,自开机算起,含休眠,通过SystemClock.elapsedRealtime()得到
  8. *maxWhen最大触发时间
  9. *interval触发间隔,针对循环闹钟有效
  10. *operation闹钟触发时的行为,PendingIntent类型
  11. */
  12. Alarma=newAlarm(type,when,whenElapsed,maxWhen,interval,operation,workSource);
  13. //根据PendingIntent删除之前已有的同一个闹钟
  14. removeLocked(operation);
  15. booleanreschedule;
  16. //尝试将alarm加入到合适的batch中,如果alarm是独立的或者无法找到合适的batch去容纳此alarm,返回-1
  17. intwhichBatch=(isStandalone)?-1:attemptCoalesceLocked(whenElapsed,maxWhen);
  18. if(whichBatch<0){
  19. //没有合适的batch去容纳alarm,则新建一个batch
  20. Batchbatch=newBatch(a);
  21. batch.standalone=isStandalone;
  22. //将batch加入mAlarmBatches中,并对mAlarmBatches进行排序:按开始时间升序排列
  23. reschedule=addBatchLocked(mAlarmBatches,batch);
  24. }else{
  25. //如果找到合适了batch去容纳此alarm,则将其加入到batch中
  26. Batchbatch=mAlarmBatches.get(whichBatch);
  27. //如果当前alarm的加入引起了batch开始时间和结束时间的改变,则reschedule为true
  28. reschedule=batch.add(a);
  29. if(reschedule){
  30. //由于batch的起始时间发生了改变,所以需要从列表中删除此batch并重新加入、重新对batch列表进行排序
  31. mAlarmBatches.remove(whichBatch);
  32. addBatchLocked(mAlarmBatches,batch);
  33. }
  34. }
  35. if(DEBUG_VALIDATE){
  36. if(doValidate&&!validateConsistencyLocked()){
  37. Slog.v(TAG,"Tipping-pointoperation:type="+type+"when="+when
  38. +"when(hex)="+Long.toHexString(when)
  39. +"whenElapsed="+whenElapsed+"maxWhen="+maxWhen
  40. +"interval="+interval+"op="+operation
  41. +"standalone="+isStandalone);
  42. rebatchAllAlarmsLocked(false);
  43. reschedule=true;
  44. }
  45. }
  46. if(reschedule){
  47. rescheduleKernelAlarmsLocked();
  48. }
  49. }

说明:通过上述代码可以看出,当我们创建一个alarm的时候,仅仅是将这个alarm加入到某个batch中,系统中有一个batch列表,专门用于存储所有的alarm。可是仅仅把alarm加入到batch中还不行,系统还必须提供一个类似于Looper的东西一直去遍历这个列表,一旦它发现有些alarm的时间已经到达就要把它取出来去执行。事实上,AlarmManagerService中的确有一个类似于Looper的东西去干这个事情,只不过它是个线程,叫做AlarmThread。下面看它的代码:

code:AlarmManagerService#AlarmThread

  1. privateclassAlarmThreadextendsThread
  2. {
  3. publicAlarmThread()
  4. {
  5. super("AlarmManager");
  6. }
  7. publicvoidrun()
  8. {
  9. //当前时间触发的alarm列表
  10. ArrayList<Alarm>triggerList=newArrayList<Alarm>();
  11. while(true)
  12. {
  13. //jni方法,顾名思义,阻塞式方法,当有alarm的时候会被唤醒
  14. intresult=waitForAlarm(mDescriptor);
  15. triggerList.clear();
  16. if((result&TIME_CHANGED_MASK)!=0){
  17. if(DEBUG_BATCH){
  18. Slog.v(TAG,"Timechangednotificationfromkernel;rebatching");
  19. }
  20. remove(mTimeTickSender);
  21. //将所有的alarm重新排序
  22. rebatchAllAlarms();
  23. mClockReceiver.scheduleTimeTickEvent();
  24. Intentintent=newIntent(Intent.ACTION_TIME_CHANGED);
  25. intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
  26. |Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
  27. mContext.sendBroadcastAsUser(intent,UserHandle.ALL);
  28. }
  29. synchronized(mLock){
  30. finallongnowRTC=System.currentTimeMillis();
  31. finallongnowELAPSED=SystemClock.elapsedRealtime();
  32. if(localLOGV)Slog.v(
  33. TAG,"Checkingforalarms...rtc="+nowRTC
  34. +",elapsed="+nowELAPSED);
  35. if(WAKEUP_STATS){
  36. if((result&IS_WAKEUP_MASK)!=0){
  37. longnewEarliest=nowRTC-RECENT_WAKEUP_PERIOD;
  38. intn=0;
  39. for(WakeupEventevent:mRecentWakeups){
  40. if(event.when>newEarliest)break;
  41. n++;//numberofnow-staleentriesatthelisthead
  42. }
  43. for(inti=0;i<n;i++){
  44. mRecentWakeups.remove();
  45. }
  46. recordWakeupAlarms(mAlarmBatches,nowELAPSED,nowRTC);
  47. }
  48. }
  49. //这个方法会把batch列表中的第一个batch取出来然后加到触发列表中
  50. //当然,前提是此batch的开始时间不大于当前时间
  51. //同时,如果是循环闹钟,则会对下次任务进行再次定时
  52. triggerAlarmsLocked(triggerList,nowELAPSED,nowRTC);
  53. rescheduleKernelAlarmsLocked();
  54. //遍历触发列表,发送PendingIntent
  55. for(inti=0;i<triggerList.size();i++){
  56. Alarmalarm=triggerList.get(i);
  57. try{
  58. if(localLOGV)Slog.v(TAG,"sendingalarm"+alarm);
  59. //这里PendingIntent会被send,结果就是我们的定时任务被执行了
  60. alarm.operation.send(mContext,0,
  61. mBackgroundIntent.putExtra(
  62. Intent.EXTRA_ALARM_COUNT,alarm.count),
  63. mResultReceiver,mHandler);
  64. //wehaveanactivebroadcastsostayawake.
  65. if(mBroadcastRefCount==0){
  66. setWakelockWorkSource(alarm.operation,alarm.workSource);
  67. mWakeLock.acquire();
  68. }
  69. finalInFlightinflight=newInFlight(AlarmManagerService.this,
  70. alarm.operation,alarm.workSource);
  71. mInFlight.add(inflight);
  72. mBroadcastRefCount++;
  73. finalBroadcastStatsbs=inflight.mBroadcastStats;
  74. bs.count++;
  75. if(bs.nesting==0){
  76. bs.nesting=1;
  77. bs.startTime=nowELAPSED;
  78. }else{
  79. bs.nesting++;
  80. }
  81. finalFilterStatsfs=inflight.mFilterStats;
  82. fs.count++;
  83. if(fs.nesting==0){
  84. fs.nesting=1;
  85. fs.startTime=nowELAPSED;
  86. }else{
  87. fs.nesting++;
  88. }
  89. if(alarm.type==ELAPSED_REALTIME_WAKEUP
  90. ||alarm.type==RTC_WAKEUP){
  91. bs.numWakeup++;
  92. fs.numWakeup++;
  93. //针对能唤醒设备的闹钟,这里会做一些唤醒设备的事情
  94. ActivityManagerNative.noteWakeupAlarm(
  95. alarm.operation);
  96. }
  97. }catch(PendingIntent.CanceledExceptione){
  98. if(alarm.repeatInterval>0){
  99. //ThisIntentSenderisnolongervalid,butthis
  100. //isarepeatingalarm,sotossthehoser.
  101. remove(alarm.operation);
  102. }
  103. }catch(RuntimeExceptione){
  104. Slog.w(TAG,"Failuresendingalarm.",e);
  105. }
  106. }
  107. }
  108. }
  109. }
  110. }
说明:上述代码中,AlarmThread会一直循环的跑着,一旦有新的alarm触发,它就会取出一个batch然后逐个发送PendingIntent,具体alarm的触发是由底层来完成的,我没法再继续分析下去。还有就是Alarm中有一些细节,我没有进行很具体的分析,实际上很简单,大家一看就懂。到此为止,Alarm机制的主要流程也分析完了。

总结

本文没有详细介绍如何使用Alarm,因为很简单,看一下官方文档或者网上搜一下,到处都是。关于Alarm,有一点需要强调一下:当手机重启或者应用被杀死的时候,Alarm会被删除,因此,如果想通过Alarm来完成长久定时任务是不可靠的,如果非要完成长久定时任务,可以这样:将应用的所有Alarm信息存到数据库中,每次应用启动的时候都重新注册Alarm并更新Alarm的触发时间,通过这种方式就不存在Alarm丢失的情况了。本文很长,耗时8个小时才完成的,感谢大家阅读本文,希望本文能给大家带来一点帮助。


免责声明:文章转载自《Android中Alarm的机制》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇网页设计中透明效果的使用技巧关于DevExpress的XtraTreeList使用方法总结【转载】下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

转:Android进阶:模拟闹钟 学习Alarm与Notification

无意间看到Alarm这个类 觉得挺有意思 这个用法应该会比较常用到 看了一些介绍 然后自己写了一个demo Alarm是在预定的时间上触发Intent的一种独立的方法。Alarm超出了应用程序的作用域,所以它们可以用于触发应用程序事件或动作,甚至在应用程序关闭之后,与Broadcast Receiver结合,它们可以变得尤其的强大,可以通过设置Alarm来...

在Android上常用的定时器 AlarmManager

AlarmManager的作用文档中的解释是:在特定的时刻为我们广播一个指定的Intent。简单的说就是我们设定一个时间,然后在该时间到来时,AlarmManager为我们广播一个我们设定的Intent,常用方法有五个: (1)set(int type,long startTime,PendingIntent pi); 该方法用于设置一次性闹钟,第一个参数...

Android中使用AlarmManager设置闹钟

场景 设置闹钟 闹钟提醒 注: 博客: https://blog.csdn.net/badao_liumang_qizhi 关注公众号 霸道的程序猿 获取编程相关电子书、教程推送与免费下载。 实现 新建一个MainActivity,在其布局文件中添加一个时间选择器和一个Button <?xml version="1.0" encoding="...

android 定时器的使用

1、android中通常是使用AlarmManager来定时启动一个单次或重复多次操作的。具体的说就是我们通过AlarmManager设定一个时间和注册一个intent到系统中,然后在该时间到来时,系统为我们发送一个广播,即执行我们设定的Intent(要执行的操作),通常我们使用 PendingIntent来实现“要执行的操作”,PendingIntent...

AlarmManager

AlarmManager 主要管理硬件时钟。 一些与时间相关的应用,如日历,闹钟等需要使用Alarm Manager的服务。Alarm manager功能相对比较简单,相关代码位于frameworks/base/core/jni/server/com_android_server_AlarmManagerService.cppframeworks/base...

Android 4 学习(19):Services

参考《Professional Android 4 Development》   Services Service是invisible的,因此其优先级不高于visible的Activity,之所以说不高于,是因为我们可以设置Service为在前台运行。 创建Service Android提供了Service抽象类,继承它便可以创建一个Service类:  ...