你开发过alarm相关的应用吗?
你测试过alarm相关的应用吗?
如果答案是肯定的,建议看官停下来拍拍砖。
手机管家新年运营功能有一个招牌:红包闹钟。
功能发布后,最常见的质疑当属:闹钟准吗?
为了回答这个问题,才有了这篇文章,也希望此文对那些闹钟的开发和测试者,有点用。
1.关于闹钟的4个最常用api以及精准性:
(1)set(int type,long startTime,PendingIntent pi);
Beginning in API 19, the trigger time passed to this method is treated as inexact: the alarm will not be delivered before this time, but may be deferred and delivered some time later. The OS will use this policy in order to "batch" alarms together across the entire system, minimizing the number of times the device needs to "wake up" and minimizing battery use.
(2)setExact(int type, long triggerAtMillis, PendingIntent operation);
The alarm will be delivered as nearly as possible to the requested trigger time.
only alarms for which there is a strong demand for exact-time delivery (such as an alarm clock ringing at the requested time) should be scheduled as exact. Applications are strongly discouraged from using exact alarms unnecessarily as they reduce the OS's ability to minimize battery use.
4.4后才提供,正是由于4.4后引入batch机制导致上述set方法精准度降低,从提供了这个替代方法来提高闹钟精准性,然而它也不能保证完全精准。
(3)setRepeating(int type,long startTime,long intervalTime,PendingIntent pi);
As of API 19, all repeating alarms are inexact. If your application needs precise delivery times then it must use one-time exact alarms, rescheduling each time as described above.
(4)cancel(PendingIntent operation);
英文原文解释的很好,在此就不再翻译了。
总的来说,别指望能百分白精准。
但是,会不准到什么程度呢?
待会告诉你。
2. 关于第一个参数int type的五种类型
(注意这个参数会决定第二个参数的使用方法)
(1)AlarmManager.ELAPSED_REALTIME当系统进入睡眠状态时,这种类型的闹铃不会唤醒系统。直到系统下次被唤醒才传递它,该闹铃所用的时间是相对时间,是从系统启动后开始计时的,包括睡眠时间,可以通过调用SystemClock.elapsedRealtime()获得。系统值是3 (0x00000003)
(2)AlarmManager.ELAPSED_REALTIME_WAKEUP表示闹钟在睡眠状态下会唤醒系统并执行提示功能,该状态下闹钟也使用相对时间,用法同ELAPSED_REALTIME,系统值是2 (0x00000002) 。
以上两种类型,和日期的时间无关,不受时区,地区影响。 适用于以固定间隔重复的alarm,比如每半小时触发一次。
(3)AlarmManager.RTC表示闹钟在睡眠状态下,这种类型的闹铃不会唤醒系统。直到系统下次被唤醒才传递它,该闹铃所用的时间是绝对时间,所用时间是UTC时间,可以通过调用 System.currentTimeMillis()获得。系统值是1 (0x00000001)
(4)AlarmManager.RTC_WAKEUP表示闹钟在睡眠状态下会唤醒系统并执行提示功能,该状态下闹钟使用绝对时间,系统值为0(0x00000000);
以上两种闹钟,用户可以修改,受时区,地区影响. 适用于在一天某个特定的时间点触发的alarm. (5)AlarmManager.POWER_OFF_WAKEUP表示闹钟在手机关机状态下也能正常进行提示功能(关机闹钟)。该状态下闹钟也是用绝对时间,系统值为4(0x00000004);不过本状态好像受SDK版本影响,某些版本并不支持;
3. 关于PendingIntent pi
这是闹钟触发时所要求的执行动作,比如发送一个广播、给出提示等等。PendingIntent是Intent的封装类。表示闹钟时间到时系统会触发这个PendingIntent所代表的事件(如启动服务/activity/发广播)
然而需要注意的是,如果是通过启动服务来实现闹钟提示的话,PendingIntent对象的获取就应该采用Pending.getService(Context c,int i,Intent intent,int j)方法;
如果是通过广播来实现闹钟提示的话,PendingIntent对象的获取就应该采用PendingIntent.getBroadcast(Context c,int i,Intent intent,int j)方法;
如果是采用Activity的方式来实现闹钟提示的话,PendingIntent对象的获取就应该采用PendingIntent.getActivity(Context c,int i,Intent intent,int j)方法。
一旦这三种方法错用了的话,虽然不会报错,但是看不到闹钟提示效果。
其中第4个参数可以为以下4个常量(一般使用前两个):
(1)FLAG_CANCEL_CURRENT:如果描述的PendingIntent对象已经存在时,会先取消当前的PendingIntent对象。
(2)FLAG_UPDATE_CURRENT:如果描述的PendingIntent对象存在,则保留它,并将新的PendingIntent对象的数据替换进去(也仅仅就是intent中的extra内容)。实际使用时效果和第一个基本一致,据说这个FLAG性能会好点喔。
(3)FLAG_NO_CREATE:这个FLAG决定了如果在系统中匹配不到相同的pendingintent,是否需要创建一个。如果设置了这个FLAG,表示描述的PendingIntent对象不存在,它会返回null而不是去创建它。
(4)FLAG_ONE_SHOT:创建的PendingIntent对象只使用一次。在pendingitent的匹配的源码中未找到生效的地方,作用不明,极少用到。
(5)如果直接设置为0,则采取系统默认的行为:能匹配到相同的pendingintent则返回此对象,如果匹配不到则创建一个新的返回。
接下来我们来研究下闹钟可能存在的延迟。
上层Android源码我就不贴了,有兴趣的可自行差异。
在此只贴com.android.server.AlarmManagerService.maxTriggerTime(..)方法。
这个方法定义了在设置闹钟时系统可能已经为你的闹钟分配延迟了。
1. 闹钟设置逻辑
(1)精确型闹钟的设置逻辑(exact)
没什么好说,你好像是最乖的,虽然你不省电。
(2)非精确性闹钟的设置逻辑(非exact)
是的你没看错,就有一个0.75*N的误差允许存在,而且误差这个值可能很大!
不过放心,Android系统中无时不刻存在大量的闹钟batch(最后一章给你看),正是这些batch把长跨度误差的闹钟不断向起点处拟合合并,最终将误差控制在[alart-start,batch-end]的误差范围内(见下面的示意图)。
上图解释了闹钟合并batch的过程:更新这一batch的start为这批闹钟中启动时间的最大值,end为这批闹钟中延迟时间的最小值。而batch-end 减去alarm-start得到的误差,就是这个alarm必将发生的延迟值(之一)了。
2. 闹钟生效逻辑
接下来简单看看闹钟触发生效时,是否还有会有延迟产生。
我们发现唤醒型闹钟在触发时不会有延迟,但是非唤醒型闹钟是会产生新的延迟的!
那么什么情况下会产生延迟呢?
很明显,”非亮屏”,”已分发过闹钟”,”触发保护逻辑”,”当前距离上次分发闹钟时间小于规则值”,这四种情况,系统会为非唤醒闹钟分配一个延迟。
接下来问题就是,延迟有多大?
mNextNonWakeupDeliveryTime = nowELAPSED + ((currentNonWakeupFuzzLocked(nowELAPSED)*3)/2);
不要问我为什么。
最后,Android注册了屏幕亮灭屏广播,实现了亮屏时发送所有非唤醒的闹钟。
最后还是画个草图给看官。
至此,结论你也看得差不多了。
非精确型闹钟可能会产生延迟,决定于前后batch的跨度大小,你没法控制。
非wakeup性闹钟可能产生延迟,决定于手机状态和闹钟分发情况,你也没法控制。
呵呵。。。
二. Android Alarm测试招式宝典
上回讲到 闹钟延迟是存在的,而且不可控,那我们还需要测试吗?
当然需要,不过怎么维护精准性,先让开发伤脑筋去吧。
相信很多人都做过闹钟相关功能的测试,基本都是调节时间看闹钟是否生效。
这里以手机管家企业红包闹钟的功能为例(简单讲就是到预设的时间则触发红包提醒事件),介绍几个特别一点的姿势。
1. 利用API接口设计测试场景用例
了解开发使用的接口原型和参数类型,可以指导我们设计出类似白盒的场景用例。
下面以这个api为例: set(int type,long startTime,PendingIntent pi)
(1)从上面的基础知识,我们知道type有五种不同的类型,且决定了第二个参数的使用方式。因此我们可以针对性设计出与其相关的用例:
-调节时间是否会生效?(type)
-手机休眠/关屏/飞行模式/关机/省电模式等是否影响闹钟生效?(type)
-设置的闹钟时间在当前时间之前是否会异常?(startTime)
-闹钟生效时间是否正常?(type+ startTime)
只要你熟悉开发api,以上的用例信手拈来。
(2)除了以上的基础用例,你还需要进阶了解一下闹钟本身的其他相关逻辑,才能设计出进阶用例如:
-闹钟能否正常取消?(依赖于cancle闹钟时requestCode的正确配对)
-闹钟是否需要重复生效?(依赖于api选择或pendingIntent的处理)
-关机重启后闹钟是否能生效?(依赖于在开机广播中进行闹钟注册set)
-其他事件是否会异常触发闹钟响应?(依赖于闹钟广播接收器的条件配置)
(3)其他高级用例,需要深入了解alarm实现原理与wakelock机制。
-闹钟触发的事件处理如果含有耗时任务,是否有可能被提前终止或未完全生效?
(这取决于代码开发闹钟广播receiver中处理闹钟事件时,是否为耗时任务分配了wakelock。如果没有wakelock,很可能出现onReceive事件处理完后,耗时任务未处理完,但此时任务未持有wakelock因此系统可能进入休眠,从而导致任务中止的现象。)
这种BUG在黑盒下很难测出,测出了也只能列入机型问题或难重现问题,而测试建议一般就是尽量使用低端手机来进行测试了吧。但是这个BUG在很多闹钟业务上都出现过。
2. 整理异常场景下的闹钟测试 (敞开你的脑洞吧)
以“闹钟所在进程挂掉(并重新启动)后,闹钟功能是否正常“为例,设计用例集合。
(部分用例原理相同,无需重复执行)
(本人是为了挖掘闹钟不准的各种场景才如此丧心病狂,在敏捷项目中只需抽取用例)
-管家前台进程被清(adb kill)
-管家后台进程被清(adb kill)
-清除管家栈顶
-从设置中大退管家
-停止并回收管家闹钟插件
-从设置中强制停止管家
-执行小米的清理进程
-高内存挤压管家进程被kill(使用内存填充)
3. 模拟实现闹钟核心逻辑进行实地记录
为了避免闹钟测试过程中频繁出现闹钟不准且不方便定位的问题,我建议使用以下方法:
(1) 在闹钟触发的Receiver中通过日志输出具体环境信息协助定位
比如输出当前时间,距离目标时间的误差值,以及其他产品信息等。
在红包闹钟中出现的闹钟不生效的实例,基本都可以通过这种方式快速定位到问题所在。
情形 | 初步分析 | LOG表现 |
---|---|---|
企业红包时刻,管家后台处在非运行状态。 | 用户内存小被系统强行停止被加速类APP强行停止 | 无日志输出 |
企业红包可弹出时间段,用户一直处在非桌面应用 | 产品策略 | 输出主动判定为闹钟失效的日志 |
对应企业红包APK未安装或版本不对 | 产品策略 | 输出主动判定为闹钟失效的日志 |
企业红包可弹出时间段,管家后台处理弹出时发现企业红包弹出时间已错过。 | 系统闹钟严重不准自身错误逻辑导致 | 输出相关环境信息协助定位 |
(2) 模拟开发实现核心闹钟功能
结合第一种方法,我们可以在开发源代码中进行插桩或要求打log,或者最好是自行模拟开发的实现方式,手动创建一个模拟闹钟的工程,利用这个简约版来长时间观察闹钟的相关逻辑是否正常。
这种方式一方面更符合用户的使用场景(比我们不断去调节时间看产品表现更接近用户场景),一方面可以更直观集中地发现的问题(如多触发/不触发/触发不准时/触发后数据不正常等)。
以下是简约闹钟的日志输出,启动完正常使用一天就可以直接从日志文件中统计当天闹钟生效情况相关的日志了,省力直观。
4. Dumpsys Alarm辅助功能的使用
最后介绍一个Android自带的关于Alarm的测试辅助功能。我们通过dumpsys alarm可以获取当前系统中存在的所有闹钟信息。
想想都激动,使用这个方法我们可以直接知道闹钟设置是否成功,误差多少等信息。(但是要注意这个信息是dumpsys时系统实时提供的,而Alarm batch 是不断动态变化的。)
这里以Android5.0的dumpsys Alarm解析下结果内容,这部分参考外部链接:
http://blog.csdn.net/memoryjs/article/details/48709183
(1)全局展示当前Alarm的数量和各个Batch情况
Pending alarm batches: 48
4293d3a8: 批处理模式下的内部ID号
num = 1:表示在该batch中,只有一个闹钟
start和end后的时间,表示自系统启动后,流逝的时间,该段时间粗略的表示,该闹钟会在start和end之间的时间触发
Android4.4之后,通过Batch机制,以时间为维度聚合了alarm来触发,减少系统耗电。
上面的字段则表示,有48个ALARM将被触发,以及每个batch的信息。
(2)每个Batch内的具体闹钟信息
RTC #0: Alarm{4293d358 type 1 com.android.chrome}
type=1 whenElapsed=1369361 when=+19s304ms window=-1 repeatInterval=0 count=0
operation=PendingIntent{429e4500: PendingIntentRecord{429dbbc8 com.android.chrome broadcastIntent}}
RTC:表示ALARM的类型,上文已提过
#0:表示在该批量模式中,该ALARM的标号,取值0~n-1,n为该batch中alarm个数
4293d358 :闹钟的内部编号
com.android.chrome:设置该闹钟的应用包名
type=1:闹钟的类型,即第一条中提到的几个闹钟类型
whenElapsed=1369361:该闹钟会在系统开机后,大概1369361后触发,这是大概时间
when=+19s304ms:该闹钟会在执行完这条dumpsys alarm命令后,19秒304ms后触发
window=:根据该alarm被调度的不同方法,设置不同的值,如果该alarm是 setExact()或setAlarmClock()方法调用的,该值为 AlarmManager.WINDOW_EXACT(=0),如果是 setInexactRepeating(),则赋值为AlarmManager.WINDOW_HEURISTIC(=-1),然而A PI的level不同该值也不同,API小于19(KITKAT)的是WINDOW_EXACT,大于19的是WINDOW_HEURISTIC 。
repeatInterval=900000:改闹钟的重复频率,900000ms后重复,0表示不重复
count=:表示该alarm因为某些原因而被忽略了的次数,0表示没有被忽略过
operation=PendingIntent{...}:与pendingIntent相关,该intent被实例化后,可以发送广播,启动服务,或者启动Activity,说白了就是唤醒应用的操作。
(3) 闹钟分发广播的即时状态
Broadcast Ref Count: 0
为了使得系统在醒来后,发送必要的闹钟广播帧,并且保证在所有的广播帧没有发送出去之前,系统不要进入睡眠状态,内部定义了一个变量:mBroadcastRefCount ,它的初始值是0,并且当需要发送的广播在队列配对的时候,该变量的值就会递增,发送一个广播后则递减,当减到0的时候,就会释放它持有的wakelock,而让系统进入休眠状态。Broadcast Ref Count: 0 表示在运行dumpsys alarm的时刻,该时刻并没有广播要发送。
也就是保证闹钟分发过程中系统不休眠,与之类似的可以启发到我们在闹钟任务中执行耗时任务时需要对唤醒锁进行获取。
(4)唤醒系统的闹钟排行
根据应用的唤醒系统的时间排行,取最长时间的前十名,然后按照降序列出,有助于找出第三方app因为编程不规范,而导致极度耗电的应用
(5) 以包名聚合系统中的Alarm来显示相关信息
Alarm Stats
com.example.someapp +1s857ms running, 0 wakeups: +1s817ms 0 wakes 83 alarms: cmp={com.example.someapp/com.example.someapp.someservice} +40ms 0 wakes 1 alarms: cmp={com.example.someapp/com.example.someapp.someotherservice}
com.example.someap:设置alarm的应用包名
+1s857ms running:系统总体被该应用所有的alarm消耗的时间
0 wakeups:设备被闹钟唤醒的次数
83 alarms:alarm被触发的次数,重复闹钟,该值大于1
cmp={...}:alarm被触发,则启动该服务,服务实例位置在{}中声明
如果触发的是广播,则格式如:
android +4m51s566ms running, 281 wakeups:
+2m46s583ms 0 wakes 1224 alarms: act=android.intent.action.TIME_TICK
+1m25s624ms 89 wakes 89 alarms: act=android.content.syncmanager.SYNC_ALARM
+52s898ms 0 wakes 41 alarms: act=com.android.server.action.NETWORK_STATS_POLL
act=...:发送广播的名称
通过这个方法,可以很快的解答这个问题:
如果已设置了闹钟的应用,被强制停止,那么时间到了之后,Alarm还会生效吗?
补充在最后的是关于Alarm开发的Best practice:
1)每次只set一个闹钟,重复类型的闹钟重复set(这也是按Android闹钟的推荐用法)。
2)补充保护逻辑:如在接收到闹钟广播时,再判断一次当前时间是否合理等。根据是否延迟或提前的程度来决定是否进行下一步操作。
3)对定时重复的alarm,特别是触发网络操作,应该用一个随机的时间点,而不是固定在同一个时间点,以免服务器负载过重。
4)建议采用alarm+handler的方式来控制定时任务。其中alarm可以set为目标时间之前(如提早5分钟); 当闹钟生效之后,会复查当前时间是否正常处于目标时间之前(如五分钟之内),并换用handler的sendMessageDelayed来设置闹钟时间,这种方式失效的几率较低(除非进程挂掉那闹钟就失效了),从而提高闹钟的准确度。
要注意这种handler的方式是无法通过调节系统时间来提早触发的。(如果超过闹钟设定后15分钟还没触发,那才出现闹钟失效的情况,不过目前统计来说,还没闹钟延迟超过五分钟的。)
5)将alarm的频率尽量降低 .如非必要不要唤醒设备 。除非必要,不使用精准型闹钟,减少耗电。
看完文章的朋友真有耐心,也不知道你学到了什么。不过以后接到定时闹钟相关的测试任务,记得挺起胸来:
“用的是服务器时间还是本地时间?“
“绝对时间还是相对时间?“
“唤醒型还是非唤醒型闹钟?”
...然后对方就不敢忽悠你了…
其实关于闹钟还有很多没细究,比如wakelock等。在此就不再展开了,各位有兴趣或业务有需要,继续探索吧!