安卓电量优化之AlarmManager-WakeLock锁机制全面解析

一、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版本有如下描述:

安卓电量优化之AlarmManager-WakeLock锁机制全面解析_第1张图片

看到了吧,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):

安卓电量优化之AlarmManager-WakeLock锁机制全面解析_第2张图片

安卓电量优化之AlarmManager-WakeLock锁机制全面解析_第3张图片

看到了吧,描述很清楚了,仔细读一下就明白了,但是同样为我们提供了对应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。

 

==========================================================

 

一、WakeLock概述

wakelock是一种锁的机制,只要有应用拿着这个锁,CPU就无法进入休眠状态,一直处于工作状态。比如,手机屏幕在屏幕关闭的时候,有些应用依然可以唤醒屏幕提示用户消息,这里就是用到了wakelock锁机制,虽然手机屏幕关闭了,但是这些应用依然在运行着。手机耗电的问题,大部分是开发人员没有正确使用这个锁,成为"待机杀手"。

Android手机有两个处理器,一个叫Application Processor(AP),一个叫Baseband Processor(BP)。AP是ARM架构的处理器,用于运行Linux+Android系统;BP用于运行实时操作系统(RTOS),通讯协议栈运行于BP的RTOS之上。非通话时间,BP的能耗基本上在5mA左右,而AP只要处于非休眠状态,能耗至少在50mA以上,执行图形运算时会更高。另外LCD工作时功耗在100mA左右,WIFI也在100mA左右。一般手机待机时,AP、LCD、WIFI均进入休眠状态,这时Android中应用程序的代码也会停止执行。

Android为了确保应用程序中关键代码的正确执行,提供了Wake Lock的API,使得应用程序有权限通过代码阻止AP进入休眠状态。但如果不领会Android设计者的意图而滥用Wake Lock API,为了自身程序在后台的正常工作而长时间阻止AP进入休眠状态,就会成为待机电池杀手。

那么Wake Lock API具体有啥用呢?心跳包从请求到应答,断线重连重新登陆等关键逻辑的执行过程,就需要Wake Lock来保护。而一旦一个关键逻辑执行成功,就应该立即释放掉Wake Lock了。两次心跳请求间隔5到10分钟,基本不会怎么耗电。

二、WakeLock使用

获取WakeLock实例代码如下:

1

2

PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); 

WakeLock wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyWakelockTag");

newWakeLock(int levelAndFlags, String tag)中PowerManager.PARTIIAL_WAKE_LOCK是一个标志位,标志位是用来控制获取的WakeLock对象的类型,主要控制CPU工作时屏幕是否需要亮着以及键盘灯需要亮着,标志位说明如下:

levelAndFlags CPU是否运行 屏幕是否亮着 键盘灯是否亮着
PARTIAL_WAKE_LOCK
SCREEN_DIM_WAKE_LOCK 低亮度
SCREEN_BRIGHT_WAKE_LOCK 高亮度
FULL_WAKE_LOCK

特殊说明:自API等级17开始,FULL_WAKE_LOCK将被弃用。应用应使用FLAG_KEEP_SCREEN_ON

WakeLock类可以用来控制设备的工作状态。使用该类中的acquire可以使CPU一直处于工作的状态,如果不需要使CPU处于工作状态就调用release来关闭。

(1)、自动release

如果我们调用的是acquire(long timeout)那么就无需我们自己手动调用release()来释放锁,系统会帮助我们在timeout时间后释放。

(2)、手动release

如果我们调用的是acquire()那么就需要我们自己手动调用release()来释放锁。

最后使用WakeLock类记得加上如下权限:

1    

注意:在使用该类的时候,必须保证acquirerelease是成对出现的。

三、保持屏幕常亮

最好的方式是在Activity中使用FLAG_KEEP_SCREEN_ON的Flag。

复制代码

1 public class MainActivity extends Activity {
2     @Override
3     protected void onCreate(Bundle savedInstanceState) {
4         super.onCreate(savedInstanceState);
5         setContentView(R.layout.activity_main);
6         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
7     }
8 }

复制代码

这个方法的好处是不像唤醒锁(wake locks),需要一些特定的权限(permission)。并且能正确管理不同app之间的切换,不用担心无用资源的释放问题。 
另一个方式是在布局文件中使用android:keepScreenOn属性:

复制代码

1 
6     ...
7 

复制代码

android:keepScreenOn = ”true“的作用和FLAG_KEEP_SCREEN_ON一样。使用代码的好处是你允许你在需要的地方关闭屏幕。

注意:一般不需要人为的去掉FLAG_KEEP_SCREEN_ON的flag,windowManager会管理好程序进入后台回到前台的的操作。如果确实需要手动清掉常亮的flag,使用getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

四、WakefulBroadcastReceiver + IntentService实例

IntentService使用请参照我之前博客:Android IntentService使用介绍以及源码解析

WakefulBroadcastReceiver是BroadcastReceiver的一种特例。它会为你的APP创建和管理一个PARTIAL_WAKE_LOCK类型的WakeLock。WakefulBroadcastReceiver把工作交接给service(通常是IntentService),并保证交接过程中设备不会进入休眠状态。如果不持有WakeLock,设备很容易在任务未执行完前休眠。最终结果是你的应用不知道会在什么时候能把工作完成,相信这不是你想要的。

使用startWakefulService()方法来启动服务,与startService()相比,在启动服务的同时,并启用了唤醒锁。

当后台服务的任务完成,要调用WLWakefulReceiver.completeWakefulIntent()来释放唤醒锁。

WLWakefulReceiver类如下:

复制代码

 1 public class WLWakefulReceiver extends WakefulBroadcastReceiver {
 2 
 3     private static final String TAG = "myTag";
 4 
 5     @Override
 6     public void onReceive(Context context, Intent intent) {
 7         //
 8         String extra = intent.getStringExtra("msg");
 9         Log.i(TAG, "onReceive:"+extra);
10         Intent serviceIntent = new Intent(context, MyIntentService.class);
11         serviceIntent.putExtra("msg", extra);
12        startWakefulService(context, serviceIntent);
13     }
14 }

复制代码

很简单,就是打印一下信息以及调用startWakefulService方法来启动服务。

MyIntentService类如下:

复制代码

 1 public class MyIntentService extends IntentService {
 2 
 3     private static final String TAG = "myTag";
 4     
 5     public MyIntentService() {
 6         super("MyIntentService");
 7     }
 8 
 9     @Override
10     protected void onHandleIntent(Intent intent) {
11         //子线程中执行
12         Log.i(TAG, "onHandleIntent");
13         for (int i = 0; i < 10; i++) {
14             try {
15                 Thread.sleep(3000);
16                 String extra = intent.getStringExtra("msg");
17                 Log.i(TAG, "onHandleIntent:"+extra);
18             } catch (InterruptedException e) {
19                 e.printStackTrace();
20             }
21         }
22         //调用completeWakefulIntent来释放唤醒锁。
23         WLWakefulReceiver.completeWakefulIntent(intent); 
24     }
25 }

复制代码

同样很简单,也是打印信息进行耗时操作,但是执行完自己业务逻辑后一点记得调用completeWakefulIntent来释放唤醒锁。

最后就是启动广播接收者以及加入权限和声明了:

复制代码

Intent intent = new Intent("WANG_LEI");
intent.putExtra("msg", "学习WAKE_LOCK。。。");
sendBroadcast(intent);




            
                
            

复制代码

好了,编写好程序运行发现及时按电源键屏幕关闭依然有LOG打印出。

本篇到此结束,wakelock锁主要是相对系统的休眠而言的,意思就是我的程序给CPU加了这个锁那系统就不会休眠了,这样做的目的是为了全力配合我们程序的运行。有的情况如果不这么做就会出现一些问题,比如微信等及时通讯的心跳包会在熄屏不久后停止网络访问等问题。所以微信里面是有大量使用到了wake_lock锁。希望经过上述共同学习你能正确使用WakeLock,不要做电池杀手。

 

你可能感兴趣的:(安卓电量优化之AlarmManager-WakeLock锁机制全面解析)