背景
刚开始听到需求要做闹钟功能的时候,我是拒绝的,虽然闹钟程序功能很简单,但是要在Android系统中(特别是国产手机)开发自己的闹钟,这会面临许多不可抗拒的问题,吓得我冷汗直流。然而经过一段时间的研究,把在这个过程中收获的小小经验分享给大家。
面临的问题
1.闹钟需要后台常驻,手机关机重启后需要自启动,这两点在国产手机中如果不手动设置是不可能实现的;
2.需要和AlarmManager打交道,程序完全退出后,AlarmManager设置的定时任务也将被清除,App重新启动后需要重新设置;
3.无法查看AlarmManager已经设置好的定时任务,对闹钟的增删改查需要同时维护AlarmManager中的定时任务;
4.用户修改手机时间造成的影响;
这些问题就够让你在未来的一段时间里夜不能寐了。
创建表
首先我们在本地需要创建一个闹钟表T_CLOCK,类似下表,主要记录闹铃时间,具体字段看你的闹钟功能自行增加。
AlarmManager的使用
AlarmManager中提供的方法比较简单,常用的主要有:
public void set(int type, long triggerAtMillis, PendingIntent operation)
public void setRepeating(int type, long triggerAtMillis,long intervalMillis, PendingIntent operation)
public void cancel(PendingIntent operation)
在Android4.4(API19)后又增加了:
public void setExact(int type, long triggerAtMillis, PendingIntent operation)
public void setWindow(int type, long windowStartMillis, long windowLengthMillis, PendingIntent operation)
除了上述方法以外还有一些重载的方法,这里就不介绍了,这些set方法最终都是调用内部setImpl实现的:
private void setImpl(int type, long triggerAtMillis, long windowMillis, long intervalMillis, int flags, PendingIntent operation, final OnAlarmListener listener, String listenerTag, Handler targetHandler, WorkSource workSource, AlarmClockInfo alarmClock) { if (triggerAtMillis < 0) { /* NOTYET if (mAlwaysExact) { // Fatal error for KLP+ apps to use negative trigger times throw new IllegalArgumentException("Invalid alarm trigger time " + triggerAtMillis); } */ triggerAtMillis = 0; } ListenerWrapper recipientWrapper = null; if (listener != null) { synchronized (AlarmManager.class) { if (sWrappers == null) { sWrappers = new ArrayMap<OnAlarmListener, ListenerWrapper>(); } recipientWrapper = sWrappers.get(listener); // no existing wrapper => build a new one if (recipientWrapper == null) { recipientWrapper = new ListenerWrapper(listener); sWrappers.put(listener, recipientWrapper); } } final Handler handler = (targetHandler != null) ? targetHandler : mMainThreadHandler; recipientWrapper.setHandler(handler); } try { mService.set(mPackageName, type, triggerAtMillis, windowMillis, intervalMillis, flags, operation, recipientWrapper, listenerTag, workSource, alarmClock); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } }Java
可以看到最终是调用了mService.set()方法,而这个mService是什么呢?
private final IAlarmManager mService;
可以知道这是个Binder类,调用AlarmManagerService来实现闹钟的设置,在AlarmManagerService中可以找到它的具体实现类。
接下来我们对set方法的参数逐个介绍:
type: 闹钟类型,主要就是分为两类,系统相对时间和绝对时间和是否唤醒CPU;
ELAPSED_REALTIME:使用相对时间,可以通过SystemClock.elapsedRealtime() 获取(从开机到现在的毫秒数,包括手机的睡眠时间),设备休眠时并不会唤醒设备。
ELAPSED_REALTIMEWAKEUP:与ELAPSEDREALTIME基本功能一样,只是会在设备休眠时唤醒设备。
RTC:使用绝对时间,可以通过 System.currentTimeMillis()获取,设备休眠时并不会唤醒设备。
RTC_WAKEUP: 与RTC基本功能一样,只是会在设备休眠时唤醒设备。
triggerAtMillis:触发闹钟的时间;
windowMillis: 这个参数只有在setWindow方法中用到,意思是给触发闹钟指定一个时间范围,可以说是误差范围的意思;
intervalMillis:重复时间间隔,在setRepeating中用到;
operation: 设置一个PendingIntent,当闹钟到来的时候会启动,可以是一个Broadcast,Service或者Activity,具体用法就不说了;
这里需要注意的是,从Android4.4(API19)开始,为了节能省电(减少系统唤醒和电池使用),使用AlarmManager.set()和AlarmManager.setRepeating()已经不保证精确性,系统会对唤醒顺序进行优化,会将唤醒时刻接近的安排在一起唤醒。我们看看AlarmManager的set方法是怎么判断的:
public void set(int type, long triggerAtMillis, String tag, OnAlarmListener listener, Handler targetHandler) { setImpl(type, triggerAtMillis, legacyExactLength(), 0, 0, null, listener, tag, targetHandler, null, null); }Java
可以看到windowMillis传入了legacyExactLength()的返回值,我们看看返回了什么:
private long legacyExactLength() { return (mAlwaysExact ? WINDOW_EXACT : WINDOW_HEURISTIC); }mAlwaysExact = (mTargetSdkVersion < Build.VERSION_CODES.KITKAT); Java
这里我们可以看到,这里是通过mAlwaysExact 字段来判断闹钟是否精确,mAlwaysExact是根据sdkVersion来判断的,小于Build.VERSION_CODES.KITKAT则为精确的,这里就验证了上面官方的说法。那现在我们知道了,是否精确是通过给windowMillis传入WINDOW_EXACT来设置的,我们可以看看AlarmManager.setExact方法,这个方法是API19后提供精确闹铃的:
public void setExact(int type, long triggerAtMillis, PendingIntent operation) { setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, 0, operation, null, null, null, null, null); }Java
可以看到它这里的windowMillis传入的是WINDOW_EXACT。
所以,我们的闹钟程序首选的方法当然就是精确的setExact()了,但是根据我的实际测试,这个方法并不能每次都精确到达,有点时候会偏离几秒或者十几秒。之后我测试setWindow()方法,设置intervalMillis为100,意思是误差在100millis,基本上能精确到达,所以我们这里最终选择使用setWindow().
使用的时候我们需要根据API版本来判断调用,比如:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { alarmManager.setWindow(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), 100, sender); } else { alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), INTERVAL, sender); }Java
开始
介绍完AlarmManager的使用之后,那么我们就来针对开篇提到的问题进行解答:
问题1: 闹钟需要后台常驻,手机关机重启后需要自启动,这两点在国产手机中如果不手动设置是不可能实现的;Markup
想要实现App的后台常驻这是一个大难题,特别是在国产手机的定制系统中,任何应用都是可以被杀掉的,参考了其他闹钟的解决方案,大部分是通过强提示和引导用户打开App后台保护。
问题2:需要和AlarmManager打交道,程序完全退出后,AlarmManager设置的定时任务也将被清除,App重新启动后需要重新设置; 问题3:无法查看AlarmManager已经设置好的定时任务,对闹钟的增删改查需要同时维护AlarmManager中的定时任务; 问题4:用户修改手机时间造成的影响; Markup
问题2-4主要是通过AlarmManager对闹铃时间点进行设置,但是在国产手机中退出应用后会清除掉之前已经设置的闹钟,同时无法通过AlarmManager来获取之前所有设置好的闹钟(只能获取到下一次的),那么有没有其他好的方法呢?
终于在一个夜黑风高的夜晚,灵光一闪,我们可以每分钟扫描一下T_CLOCK闹钟表,比较当前时间和表中的闹铃时间,这样我们仅需要维护我们本地的T_CLOCK表的状态,同时也没有以上三个问题了,这样开发起来就简单多了,一分钟扫描一次也是可以接受的。
查询发现Android有个ACTION_TIME_TICK广播,分钟改变的时候会发送一次,很适合我们的需求,在文档中有注明该广播只能通过动态注册获取,我们可以在一个Service去注册和反注册,这样就可以实现后台长期监听了。
这样我们开发起来甚至都不需要使用到AlarmManager了,但是理想总是美好的,经过一系列的机型测试,发现ACTION_TIME_TICK广播在OPPO手机中,如果App切换到后台是收不到该广播的,只有App在前台的时候才能收到广播,这就很尴尬了,怎么办呢?那就只能我们自己手动设置一个每分钟扫描的定时器,依然还是得通过AlarmManager。
public static void startTimer(Context context) { Intent intent = new Intent(ACTION); PendingIntent sender = PendingIntent.getBroadcast(context, CLOCK_ID, intent, PendingIntent.FLAG_CANCEL_CURRENT); AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); Calendar calendar = Calendar.getInstance(); int second = calendar.get(Calendar.SECOND); int delay = 60 - second + 1; calendar.add(Calendar.SECOND, delay); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { alarmManager.setWindow(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), 100, sender); } else { alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), INTERVAL, sender); } }Java
代码很简单,以上代码是使用AlarmManager的典型代码,其中需要额外处理闹铃的时间,要在下一分钟开始的时候。通过AlarmManager.setWindow是没有周期重复的,那要实现周期重复的话,只需要在广播接收器中再次设置就可以实现周期提醒了,代码如下:
public static class TimeChangeReceiver extends BroadcastReceiver { private static final String TAG = TimeChangeReceiver.class.getName(); @Override public void onReceive(Context context, Intent intent) { Intent i = new Intent(ACTION); AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); PendingIntent sender = PendingIntent.getBroadcast(context, CLOCK_ID, i, PendingIntent.FLAG_CANCEL_CURRENT); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { am.setWindow(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + INTERVAL, 100, sender); } } }Java
总结
本文实现闹钟的思路很简单,就是通过每分钟扫描T_CLOCK表来实现闹铃。应用运行了一段时间,只要应用不被后台清除,都是可以正常闹铃。本篇是我对Android闹铃开发的一些见解,如果有不对的地方或者有很好的方案欢迎提出。
引用 http://blog.csdn.net/editor1994/article/details/50610429
共同学习,写下你的评论
评论加载中...
作者其他优质文章