一、Alarm简介

当我们在开发与闹钟定时有关的Android程序时,我们一般选择使用AlarmManager来实现,但实际上Android中与定时闹钟有关的方法功都在AlarmManagerService中,AlarmManager需要调用AlarmManagerService中的方法来实现定时服务。下面是定时任务的时序图,开发者在应用程序中获取AlarmManager对象,通过它的set方法设置闹钟,然后AlarmManager通过Binder远程调用AlarmManagerService中的相关方法,最后将定时任务的时间设置到底层驱动。而底层RTC触发后,会传到AlarmManagerService中,进而通过消息(PendingIntent)发送给应用程序的广播接受者,广播接受者接收到广播之后执行相应的操作。

二、 AlarmManager用法介绍

  1. 首先我们要获取AlarmManager的实例

 AlarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);

2、调用AlarmManager的set方法。

Android 6.0之前:

  •  public void set()

功能:用于设置一次性alarm,第一个参数表示闹钟类型,第二个参数表示触发这个闹钟要等待的时间,与type相关

② public void setRepeating()

功能:该方法用于设置重复闹钟。

③ Public void setExact()设置精确alarm

Android 6.0之后增加了doze休眠模式,在设备处于休眠模式下,在这种模式下,一些任务会被挂起暂停操作,直到下一个活跃期才执行,包括alarm定时操作,所以之前的方法都会被延迟, 所以如果想在休眠模式下触发,又增加了以下两个方法

setExactAndAllowWileIdle()

⑤ setAndAllowWhileIdle()

⑥ setAlarmClock()

在android中共有4种type的闹钟

1 . RTC_WAKEUP 在指定的时刻发送广播,唤醒设备,type值为0

2. RTC 在指定的时刻发送广播,不唤醒设备,type值为1

3. ELAPSED_REALTIME_WAKEUP 在指定的延时后发送广播,唤醒设备,type值为2

4. ELAPSED_REALTIME 在指定的延时后发送广播,不唤醒设备,type值为3

其中,1和2这两种类型的闹钟使用的是绝对时间(RtcTime),可以通过System.currentTimeMillion()方法获得,而3和4使用的是相对时间(elapsedTime),从系统启动开始计时,改时间可以通过systemClock.elapsedRealtime()方法获得。在闹钟设置时,需要注意这两种时间的转化。

除此之外还有四种Flag:

(1)FLAG_STANDALONE = 1;用于标识该alarm不会被加入到其他alarm集合中去(在Android4.4以上是非准确传递的,对时间相近的alarm会进行批处理),单独进行处理;

(2)FLAG_WAKE_FROM_IDLE = 2;用于标识设备即使处于idle状态,也会被唤醒处理alarm;

(3)FLAG_ALLOW_WHILE_IDLE = 4;用于标识设备即使处于idle状态也会处理alarm,并且设备不会退出idle状态;

(4)FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED = 8;类似于3,但是它的运行不会受到任何约束,仅适用于系统alarm;

(5)FLAG_IDLE_UNTIL = 16;仅适用于系统,用于告诉AlarmManager在什么时候退出idle模式,只能在DeviceIdleController使用,当DeviceIdleController设置该flag的时候,说明系统已经进入到了idle状态;

三、 AlarmManagerService分析    

3.1. Batch和Alarm

在AlarmManagerService中有两个非常重要的数据结构Alarm和Batch,如图2和图3所示。

图2. Alarm类

图3. Batch类

图4. 系统中Batch列表

Alarm是android提供的用于完成定时任务的类,其成员包括定时闹钟类型,触发时间,重复触发时间间隔,触发后的操作等信息,一个Alarm的实例Alarm实际上就表示一个定时任务。

Batch这个数据结构实际上是用来存放一组触发时间非常相近的Alarm的一种数据结构,它的成员包括一个Alarm类型的List,一个开始时间start和一个截止时间end等信息。如上图所示,系统中维护了一个Batch列表,每个Batch中存储了多个出发时间接近的alarm,同一个Batch中的alarm是同时触发的。

从android4.4开始,Android中的Alarm默认采用非精准模式,在非精准模式下,Alarm是分批量提醒的,即每个Alarm根据其触发时间和最大触发时间的不同会被加入到不同的Batch中,在同一个Batch中的Alarm是同时被触发的,这是一种非精确定时器,之所以这样做,官方的解释是批量处理可以减少设备被唤醒的次数以及可以节约电量。

3.2. Alarm的设置和触发

Android中的Alarm处理主要包括两个部分,即Alarm的设置和Alarm的触发。设置Alarm就是把一个定时任务的触发时间设置到底层驱动中,而Alarm触发就是当触发时间到来时获取到需要触发的Alarm,继而去执行相应的操作。

下面我们通过流程图来详细的了解Android中Alarm的设置和触发过程:

Alarm的设置:

  • 首先,系统获取用户的定时任务信息,然后对所有参数进行修正,封装成一个Alarm对象。

② 然后判断当前alarm是不是一个AlarmManager.FLAG_IDLE_UNTIL类型的(该类型的alarm是一个特殊的alarm,它会使系统进入空闲模式,直到该alarm被触发,所以如果检测到有一个AlarmManager.FLAG_IDLE_UNTIL类型的alarm被设置,说明当前正处于空闲状态下。)如果是,如果当前系统中已经有了mNextWakeFromIdle(即一个即将把系统唤醒的alarm),而且mNextWakeFromIdle时间更早,那么就把mNextWakeFromIdle的时间赋给这个AlarmManager.FLAG_IDLE_UNTIL类型的alarm的触发事件。

如果该alarm不是AlarmManager.FLAG_IDLE_UNTIL类型的,则判断当前mPendingIdleUntil是否为null,如果不为null,说明系统中有一个会在将来某个时间点把系统唤醒的alarm,说明当前处于空闲状态下,则有以下操作:

if ((a.flags&(AlarmManager.FLAG_ALLOW_WHILE_IDLE

                | AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED

                | AlarmManager.FLAG_WAKE_FROM_IDLE))

                == 0) {

            mPendingWhileIdleAlarms.add(a);

            return;

   }

mPendingWhileIdleAlarms即为一个在系统空闲状态下被挂起的alarm任务列表,在该列表中的alarm都暂停处理。

③根据待机应用群组standby机制调整alarm的触发时间(再adjustDeliveryTimeBasedOnBucketLocked(Alarm alarm))。

应用待机群组:Android 9引入了一项新的电池管理功能,即应用待机群组。应用待机群组可以基于应用最近使用时间和使用频率帮助系统排定应用请求资源的优先级。根据该模式每个应用都会被归类到五个优先级之一,系统会根据应用所属的群组限制每个应用可以访问的设备资源。其中存在以下五个群组:

(1)活跃:用户正在使用;

(2)工作集:应用经常在运行,但是并未处于活跃状态;

(3)常用:应用会被定期使用,但不是每天都必须使用;

(4)极少使用:应用不经常使用;

(5)从未使用。

根据应用standby bucket类型应用alarm依次被延迟0min,6min,30min,2h,10d。

④然后遍历Batch列表,寻找一个合适的Batch将该Alarm插入到Batch中,如果没有找到合适的Batch,则需要创建一个Batch,然后把Alarm添加进去,在把新建的Batch通过二分法添加到Batch列表中,如果在Batch列表中找到了合适的插入位置,则直接把该Alarm以二分法的方式添加到指定位置的Batch中。需要注意的是很多情况下,新加入的Alarm会改变当前Batch的起止时间,进而影响Batch列表中Batch的触发顺序,因此我们需要将所有的Batch重新进行一个排序,确保最先触发的Batch是距离触发时间最近的。

⑤然后如下代码处理,如果该alarm是个AlarmManager.FLAG_IDLE_UNTIL,那么把它赋给mPendingIdleUntil,然后将alarm列表重新处理。

如果设置的alarm是AlarmManager.FLAG_WAKE_FROM_IDLE类型的,并且该alarm的触发时间早于mNextWakeFromIdle的触发时间,则把该alarm赋给mNextWakeFromIdle,然后因为mNextWakeFromIdle触发时间的改变有可能会改变与系统某些alarm的触发操作,所以还要重新处理一下alarm列表(举个例子,本来触发时间比mNextWakeFromIdle时间晚的alarm不会被设置到rtc,但是当mNextWakeFromIdle改变了之后,有可能它可以设置了)。

最后我们要分别获取Batch列表中firstBatchfirstWakeup然后 Batch的start通过JNI接口设置到底层驱动中,需要注意的是firstRtcWakeup使用的是绝对时间,而Batch的start用的是elapsed时间,因此在将firstRtcWakeup的start设置到底层驱动之前需要通过时间转化转化成绝对时间。图4详细描述了Alarm的设置流程。

5.  Alarm设置流程图

Alarm的触发:Alarm的触发过程则是开启一个线程AlarmThread,在该线程中创建一个循环,在循环中等待底层Alarm的触发,当waitThread检测到底层Alarm被触发以后,就要根据当前触发时间,从Batch列表中找出符合触发条件的Batch(正常来说是第一个Batch),然后将该Batch添加到触发列表中,然后对加入触发列表中的alarm进行分析

如果检测到要触发的alarm是mPendingIdleUntil,则需要重新批处理所有的alarm,而且因为系统空闲而被挂起的alarm列表mPendingWhileIdleAlarms也要重新被处理。

if (mPendingIdleUntil == alarm) {

      mPendingIdleUntil = null;

       rebatchAllAlarmsLocked(false);

       // 由于mPendingIdleUntil变量改变,所以需要重新存储空闲时延迟执行的alarm

     restorePendingWhileIdleAlarmsLocked();

 }

如果检测到被触发的Alarm有重复性闹钟,则需要重新设置。

如果监测到要触发的alarm是非wakeup类型(被触发列表中只要有一个alarm是wakeup类型的,那么整个触发列表就是wakeup类型的),并且允许被延迟(即当前非亮屏状态,而且上一次计算的延迟触发时间还没到),则计算下一次非wakeup类型alarm的延迟时间,然后把触发列表中的alarm加到mPendingNonWakeupAlarms中,重新设置,结束。

如果触发列表中的alarm是wakeup类型的,那么就可以触发,而且这个时候也要判断一下之前的mPendingNonWakeupAlarms(毕竟人家上次已经被延迟了,这一轮设备肯定会被唤醒,那么就把之前挂起的也触发了吧,再继续延迟就太惨了)如果之前有被挂起的非wakeup alarm,则要把它加到出发列表中进行触发。之前的延迟列表清空。

而触发列表中的Alarm就会根据pendingIntent中的动作去执行,如图5所示。

需要注意的是, 在android系统中,如果触发列表中所有的Alarm都是非唤醒(nowakeup)类型的,那么如果Alarm触发时手机正处于休眠(熄屏)状态,这些Alarm是可以被添加到延迟触发列表中延迟触发的,那如果手机长时间不唤醒,这些nowakeup类型的Alarm是不是一直不触发呢,肯定不是,在系统在挂起那些nowakeup类型的Alarm时,会根据熄屏时间计算一个noWakeup Alarm的延迟触发时间,如果Alarm等待的时间超过了这个延迟触发时间,那么不管手机屏幕有没有被唤醒,系统都会执行触发操作。

图6. Alarm触发流程图

3.3 总结一

Alarm定时不准的原因

  1. 采用的set方法,非精确触发,触发不准。
  2. 没有用setAndAllowWhileIdle()或者setExactAndAllowWileIdle方法,设备一休眠就被挂起
  3. Alarm触发时间被待机应用群组standby这个机制重新调整了,被延迟了
  4. 设备重启了
  5. 定时应用进程被杀死

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐