安卓电量优化之AlarmManager使用全部解析
版权声明:本文出自汪磊的博客,转载请务必注明出处。
一、AlarmManager概述
AlarmManager是安卓系统中一种系统级别的提示服务,可以在我们设定时间或者周期性的执行一个intent,这个intent可以是启动Service服务、发送广播、跳转Activity,看到这里是不是会想这不就是定时器Timer吗,Timer确实是一般定时需求的最便捷实现方式,但是试想一下手机空闲状态下,屏幕会变暗,最后CPU会停止运行,这样可以防止电池电量掉的快。在长时间休眠情况下自定义的Timer、Handler、Thread、Service等都会暂停,因为它们没有唤醒CPU的能力,但是AlarmManager可以唤醒CPU,到达规定的时间就会大吼一声:”小U,别装睡了,起来干活”,CPU就会乖乖起来干活了。AlarmManager最重要的特性就是能在手机休眠情况下唤醒CPU来工作。
二、AlarmManager重点API讲解
使用AlarmManager我们首先获取AlarmManager系统服务,如下:
1 am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
很简单,没有什么特殊需要说明。
API 19之前AlarmManager的常用方法:
(1)set(int type,long startTime,PendingIntent pi)//该方法用于设置一次性定时器,到达时间执行完就完蛋了。
(2)setRepeating(int type,long startTime,long intervalTime,PendingIntent pi)//该方法用于设置可重复执行的定时器。
(3)setInexactRepeating(int type,long startTime,long intervalTime,PendingIntent pi)//该方法用于设置可重复执行的定时器。与setRepeating相比,这个方法更加考虑系统电量,比如系统在低电量情况下可能不会严格按照设定的间隔时间执行闹钟,因为系统可以调整报警的交付时间,使其同时触发,避免超过必要的唤醒设备。
参数说明:
int type:闹钟类型,常用有五个类型,说明如下:
AlarmManager.ELAPSED_REALTIME | 表示闹钟在手机睡眠状态下不可用,就是睡眠状态下不具备唤醒CPU的能力(跟普通Timer差不多了),该状态下闹钟使用相对时间,相对于系统启动开始。 |
AlarmManager.ELAPSED_REALTIME_WAKEUP | 表示闹钟在睡眠状态下会唤醒系统并执行提示功能,该状态下闹钟也使用相对时间 |
AlarmManager.RTC | 表示闹钟在睡眠状态下不可用,该状态下闹钟使用绝对时间,即当前系统时间 |
AlarmManager.RTC_WAKEUP | 表示闹钟在睡眠状态下会唤醒系统并执行提示功能,该状态下闹钟使用绝对时间 |
AlarmManager.POWER_OFF_WAKEUP | 表示闹钟在手机关机状态下也能正常进行提示功能,5个状态中用的最多的状态之一,该状态下闹钟也是用绝对时间 |
long startTime:闹钟的第一次执行时间,以毫秒为单位。需要注意的是,本属性与第一个属性(type)密切相关,如果第一个参数对应的闹钟使用的是相对时间(ELAPSED_REALTIME和ELAPSED_REALTIME_WAKEUP),那么本属性就得使用相对时间,比如当前时间就表示为:SystemClock.elapsedRealtime();如果第一个参数对应的闹钟使用的是绝对时间 (RTC、RTC_WAKEUP、POWER_OFF_WAKEUP),那么本属性就得使用绝对时间,当前时间就表示 为:System.currentTimeMillis()。
long intervalTime:表示两次闹钟执行的间隔时间,也是以毫秒为单位。
PendingIntent pi:到时间后执行的意图。PendingIntent是Intent的封装类。需要注意的是,如果是通过启动服务来实现闹钟提 示的话,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)方法。关于PendingInten不是本文重点,请自行查阅使用方法。
使用举例:需求,定义一个在CPU休眠情况下也能执行的闹钟,每隔5秒发送一次广播,代码如下:
1 Intent intent = new Intent("WANG_LEI");
2 intent.putExtra("msg","起床了啊");
3 PendingIntent pi = PendingIntent.getBroadcast(this,0,intent,0);
4
5 AlarmManager am = (AlarmManager)getSystemService(ALARM_SERVICE);
6 // 每隔5秒后通过PendingIntent pi对象发送广播
7 am.setRepeating(AlarmManager.RTC_WAKEUP,System.currentTimeMillis(),5*1000,pi);
三、AlarmManager的版本适配
以上讲解在API<19的情况下能正常运行,但是在API>=19和API<=23手机上运行会发现尼玛怎么不好使了,比如我们设置1分钟执行一次,真正运行起来却变成3分钟执行一次,这不是坑爹吗。这是为什么呢?查阅谷歌文档会发现,关于4.4版本有如下描述:
看到了吧,4.4及以上版本谷歌进行了优化,怎么优化的呢?这样说吧之前版本比如手机上装了两个应用A,B均使用了AlarmManager,A应用设定5秒唤醒一次CPU执行任务,B应用设定7秒唤醒一次CPU执行任务,在API<19手机上这样运行没问题的,5秒一次,7秒一次轮着唤醒CPU干活,但是到了4.4及以上版本这样就不行了,谷歌一想老子出的这功能都被你们玩坏了,照这样下去小刘5秒一次,小徐6秒一次,小江7秒一次CPU不停地被唤醒这用户电量都被消耗没了(唤醒CPU是很耗电的),好,老子直接优化一下,针对这种情况老子统一进行批处理了,你们都给我7秒唤醒一次CPU,这一次你们三个活都干了。大体优化逻辑就是这样子。
BUT,凡是都有但是啊,你要想在API>=19和API<=23手机上照样能正常运行咋办,谷歌还是很贴心的提供额外API,使用setExact(int type, long triggerAtMillis, PendingIntent operation)就可以了。(具体使用代码文章下面会有,别急)
满心欢舞的我们修改完后继续运行了。
BUT,在6.0及以上手机又出问题了,手机在进入休眠状态一段时间后AlarmManager不工作了,真是服了,继续找问题吧,发现6.0中谷歌对
低电耗模式和应用待机模式(6.0开始引入)进行了优化,描述如下(原文链接:https://developer.android.google.cn/training/monitoring-device-state/doze-standby.html):
看到了吧,描述很清楚了,仔细读一下就明白了,但是同样为我们提供了对应API解决:setExactAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)。到此AlarmManager的版本适配就完了,但是还有一个问题setExact(int type, long triggerAtMillis, PendingIntent operation)以及setExactAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)方法都没有重复提醒的设置,没有setRepeating类似API,都是一次性的闹钟,我们怎么实现每隔一段时间执行一次任务的需求呢?很简单,重复注册就可以了,这么说不明白的话请继续看下文,Demo会讲到。
四、AlarmManager实例Demo讲解(包含版本适配以及高版本设置重复闹钟)
好了经过上面讲解,我相信你是似懂非懂的,因为没看到具体代码啊,简单,一个小Demo就全都明白了。
实现功能:在CPU休眠情况下依然可以每隔五秒发送一次广播,在广播接收者中执行相应逻辑(Demo中只是打印Log),适配各个版本。
先看一下最核心的AlarmManagerUtils类:
1 public class AlarmManagerUtils {
2
3 private static final long TIME_INTERVAL = 5 * 1000;//闹钟执行任务的时间间隔
4 private Context context;
5 public static AlarmManager am;
6 public static PendingIntent pendingIntent;
7 //
8 private AlarmManagerUtils(Context aContext) {
9 this.context = aContext;
10 }
11
12 //饿汉式单例设计模式
13 private static AlarmManagerUtils instance = null;
14
15 public static AlarmManagerUtils getInstance(Context aContext) {
16 if (instance == null) {
17 synchronized (AlarmManagerUtils.class) {
18 if (instance == null) {
19 instance = new AlarmManagerUtils(aContext);
20 }
21 }
22 }
23 return instance;
24 }
25
26 public void createGetUpAlarmManager() {
27 am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
28 Intent intent = new Intent("WANG_LEI");
29 intent.putExtra("msg", "赶紧起床");
30 pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);//每隔5秒发送一次广播
31 }
32
33 @SuppressLint("NewApi")
34 public void getUpAlarmManagerStartWork() {
35 //版本适配
36 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {// 6.0及以上
37 am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,
38 System.currentTimeMillis(), pendingIntent);
39 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {// 4.4及以上
40 am.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),
41 pendingIntent);
42 } else {
43 am.setRepeating(AlarmManager.RTC_WAKEUP,
44 System.currentTimeMillis(), TIME_INTERVAL, pendingIntent);
45 }
46 }
47
48 @SuppressLint("NewApi")
49 public void getUpAlarmManagerWorkOnReceiver() {
50 //高版本重复设置闹钟达到低版本中setRepeating相同效果
51 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {// 6.0及以上
52 am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,
53 System.currentTimeMillis() + TIME_INTERVAL, pendingIntent);
54 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {// 4.4及以上
55 am.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis()
56 + TIME_INTERVAL, pendingIntent);
57 }
58 }
59 }
AlarmManagerUtils就是将与AlarmManager有关的操作都封装起来了,方便解耦。很简单,主要就是版本适配了,上面已经讲解够仔细了,这里就是判断不同版本调用不同API了。
MainActivity代码:
1 public class MainActivity extends Activity {
2
3 private AlarmManagerUtils alarmManagerUtils;
4
5 @Override
6 protected void onCreate(Bundle savedInstanceState) {
7 super.onCreate(savedInstanceState);
8 setContentView(R.layout.activity_main);
9 //
10 alarmManagerUtils = AlarmManagerUtils.getInstance(this);
11 alarmManagerUtils.createGetUpAlarmManager();
12 //
13 findViewById(R.id.am).setOnClickListener(new OnClickListener() {
14
15 @SuppressLint("NewApi")
16 @Override
17 public void onClick(View v) {
18 //
19 alarmManagerUtils.getUpAlarmManagerStartWork();
20 }
21 });
22 }
23 }
MainActivity中就是调用AlarmManagerUtils中已经封装好的代码进行初始化以及点击Button的时候调用getUpAlarmManagerStartWork方法完成第一次触发AlarmManager。
最后看下广播接收者中具体做了什么。
MyBroadcastReceiver类:
1 public class MyBroadcastReceiver extends BroadcastReceiver {
2
3 private static final String TAG = "MyBroadcastReceiver";
4
5 @SuppressLint("NewApi")
6 @Override
7 public void onReceive(Context context, Intent intent) {
8 //高版本重复设置闹钟达到低版本中setRepeating相同效果
9 AlarmManagerUtils.getInstance(context).getUpAlarmManagerWorkOnReceiver();
10 //
11 String extra = intent.getStringExtra("msg");
12 Log.i(TAG, "extra = " + extra);
13 }
14 }
在onReceive方法中再次注册一下AlarmManager达到低版本中setRepeating相同效果。
好了,Demo中核心就是AlarmManagerUtils类,看懂了就全懂了,还需要自己去慢慢研究明白。
四、AlarmManager疑难问题总结
1:进程被杀死,AlarmManager停止工作
在Demo运行的过程中发现我们主动杀死进程AlarmManager也就停止运行了,Log停止打印。我们只能在应用打开或者应用中存在服务在服务重启的时候重新注册一下AlarmManager,我还没有发现什么其余好的办法,如果你有好的解决办法,请留言给与解答,向您请教。
2:手机重启,AlarmManager停止工作
其实这个问题和上面进程被杀死情况差不多,这种情况我们可以注册一个监听手机重启的广播,在收到广播的时候重新注册一下AlarmManager就可以了。
3:各厂商的“心跳对齐”
小米,华为等手机厂商,都有“心跳对齐”机制,比如我们开发一个APP,在后台2s就要唤醒一次CPU执行任务,
上面说过唤醒CPU是耗电的(2s就唤醒一次,我只能说产品经理有问题,实现者需求的程序更有问题)。
各大厂商检测到你APP这么频繁唤醒CPU对用户来说是很耗电的,所以系统会将你应用强制”心跳对齐”,使你的APP不那么频繁唤醒CPU。比如你APP设定
的是2s执行一次,但是实际在各大厂商运行起来是10s一次。
五、总结
好了,本文到此就该结束了,相信经过以上讲述你对AlarmManager有了更进一步全面了解,在我们使用的时候请不要滥用,考虑一下用户电量,尽量优化自己APP。
声明:文章将会陆续搬迁到个人公众号,以后文章也会第一时间发布到个人公众号,及时获取文章内容请关注公众号