在最近的开发过程中,需要完成一个本地闹钟的功能,需求是固定的3个闹钟,每日重复,可以手动开关,需要时间精准,闹钟响起时需弹出dialog和播放音乐。
在alarmmanager的使用过程中主要参考了github上的一个demo , Android-AlarmManagerClock ,遇到了一些比较难解决的问题,现在整理一下以防止以后再采坑。
1.关于不同手机版本精确闹钟的兼容
SDK API < 19
一般情况下,使用 AlarmManager
来执行重复定时任务的代码如下所示:
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), TIME_INTERVAL, pendingIntent);
或者
alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), TIME_INTERVAL, pendingIntent);
setRepeating
该方法用于设置重复定时任务。
- 第一个参数表示闹钟类型:一般为
AlarmManager.ELAPSED_REALTIME_WAKEUP
或者AlarmManager.RTC_WAKEUP
。它们之间的区别就是前者是从手机开机后的时间,包含了手机睡眠时间;而后者使用的就是手机系统设置中的时间。所以如果设置为AlarmManager.RTC_WAKEUP
,那么可以通过修改手机系统的时间来提前触发定时事件。另外,对于相似的AlarmManager.ELAPSED_REALTIME
和AlarmManager.RTC
来说,它们不会唤醒 CPU 。所以使用的频率较少; - 第二个参数表示任务首次执行时间:与第一个参数密切相关。第一个参数若为
AlarmManager.ELAPSED_REALTIME_WAKEUP
,那么当前时间就为SystemClock.elapsedRealtime()
;若为AlarmManager.RTC_WAKEUP
,那么当前时间就为System.currentTimeMillis()
; - 第三个参数表示两次执行的间隔时间:这个参数没什么好讲的,一般为常量;
- 第四个参数表示对应的响应动作:一般都是去发送广播,然后在广播接收
onReceive(Context context, Intent intent)
中做相关操作。
SDK API >= 19 && SDK API < 23
当你写好代码、满心欢喜地将程序跑在手机上的时候,傻眼了!你会发现在 Android 4.4 及以上版本的定时任务不是按照规定时间间隔来执行的。比如你设置了每隔 3 分钟发出一个 HTTP 请求,结果你一看莫名其妙地变成了隔 5 分钟发一次。
然后你查阅 Android 官网中关于 Android 4.4 API 会看到如下几句话:
恍然大悟!原来是 Google 为了追求系统省电,所以“偷偷加工”了一下唤醒的时间间隔。但也正如上面官网中所说的那样,如果在 Android 4.4 及以上的设备还要追求精准的闹钟定时任务,要使用 setExact()
方法。
所以,相应的代码就变成了这样:
// pendingIntent 为发送广播
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), pendingIntent);
} else {
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), TIME_INTERVAL, pendingIntent);
}
private BroadcastReceiver alarmReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 重复定时任务
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TIME_INTERVAL, pendingIntent);
}
// to do something
doSomething();
}
};
当你写好了“加强版”的 AlarmManager
之后,内心肯定无比小激动。这下总应该行了吧?运行一下,果然没错!在 Android 4.4 上的确按照规定的时间间隔在执行任务。哈哈,这下大功告成了!!!
SDK API >= 23
在 Android 4.4 上品尝到胜利的甜头后,你顺便在 Android 6.0 的设备上测试了一下。结果。。。。。。你又 TMD 傻眼了!
发现在设备关屏静止一段时间后, AlarmManager
又又又不能正常工作了。相必此时你连日狗的心都有了吧!强忍着泪水,再次打开 Android 官网中关于 Android 6.0 变更 ,发现在 Android 6.0 中引入了低电耗模式和应用待机模式。然后接着往下看 对低电耗模式和应用待机模式进行针对性优化 ,发现会有下面一段话:
啊啊啊啊啊啊!之前在 Android 4.4 上能用的 setExact()
方法在 Android 6.0 上因为低电耗模式又不能正常使用了。但是,Google 又又又提供了新的方法 setExactAndAllowWhileIdle()
来解决在低电耗模式下的闹钟触发。
所以,Attention!相关的代码又被改写为这样:
// pendingIntent 为发送广播
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), pendingIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), pendingIntent);
} else {
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), TIME_INTERVAL, pendingIntent);
}
private BroadcastReceiver alarmReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 重复定时任务
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TIME_INTERVAL, pendingIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TIME_INTERVAL, pendingIntent);
}
// to do something
doSomething();
}
};
2.Android 7.0 pendingIntent用intent传递的bug
问题比较少见,只有你在跨进程传递数据的时候会碰到,如pendingIntent中
在7.0中通过pendingIntent的bundle传递的数据时,你会发现serializable和parcelable的数据拿不到
如果你只传了string,那是没问题的,但是如果你传了string和一个serializable你会发现,不光serializable拿不到,连string也拿不到了,黑人问好脸吧
原因参考:
https://commonsware.com/blog/2016/07/22/be-careful-where-you-use-custom-parcelables.html
解决方案1:
他的解决方法很独特,将所有数据转为基本类型bytes再去传递,当然,这可以解决我们的问题
然后我就想既然byte能解决,只传string也没问题,那为什么不干脆直接传string嗯,所以另一个简单的修改方法就是把所有的对象全部转成jsonstring去传递,也可以解决问题
解决方案2:
继续寻找有没有更加合适的方案时发现google的issuetracker中有个大神发现了一个更简单的方法,直接把所有数据放到bundle里,然后将bundler作为参数传递即intent.putExtra("data",bundle)也可以解决问题,
详情参考:
https://issuetracker.google.com/issues/37097877
参考代码:
Intent intent = new Intent(ALARM_ACTION);
Bundle bundle = new Bundle();
bundle.putInt(AlarmConfig.DATA_BUNDLE_ALARM_ID,id);
bundle.putLong(AlarmConfig.DATA_BUNDLE_INTERVALMILLIS, intervalMillis);
intent.putExtra(AlarmConfig.DATA_BUNDLE, bundle);
// 这个pendingIntent是Intent的包装类, 在闹钟到点的时候能够发送广播给AlarmOnTimeReceiver
PendingIntent sender = PendingIntent.getBroadcast(context, id, intent, PendingIntent
.FLAG_CANCEL_CURRENT);
另外两个问题是关于魅族手机适配的问题:
发现在魅族手机上出现2个问题,魅族M6版本 Flyme 6.1.4.6A ,Android 版本 7.1.2 :
- 实现一个通知栏通知的功能,闹钟响起来就出现通知通知栏,setContentText()中的内容要是有 感叹号(!) 就通知就出现不了,代码如下 :
//发送通知
private void showNotification(AlarmClock clock) {
int NOTIFICATION_ID = clock.getId();
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setContentTitle
("闹钟响起了").setContentText("时间到了哦!").setAutoCancel(true).setColor(Color
.rgb(134, 177, 194)).setSmallIcon(R.mipmap.ic_launcher).setLargeIcon
(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)).setDefaults
(NotificationCompat.DEFAULT_ALL);
//要跳转到主页面
Intent intent = new Intent(this, HomeActivity.class);
PendingIntent operation = PendingIntent.getActivity(this, NOTIFICATION_ID, intent, 0);
builder.setFullScreenIntent(operation, false); // 悬挂式Notification(5.0 新增)
builder.setContentIntent(operation);
Notification notification = builder.build();
mNotificationManager.notify(NOTIFICATION_ID, notification);
}
2.使用AlarmManager ,在App中按Home键锁屏后闹钟失效(只有按Home键锁屏这一种情况,按返回键没有这个问题) , AlarmManager接收不到广播 ,导致闹钟失效
解决问题1最终还是没有解决
解决问题2:通过魅族手机-设置-关闭智能休眠模式
魅族手机的问题说明 手机厂商改动系统太多了, 给开发者带来了无数适配的坑.... = =
参考文章:
关于使用AlarmManager的注意事项
Android 7.0 pendingIntent bug(AlarmManager通过PendingIntent传递数据(跨进程数据传递)