1.减少CPU消耗即省电
CPU进入休眠状态,耗电就会减少
软件层面阻止CPU休眠的几个因素:
1.wake_lock
2.AlarmManager
可以使用电池分析工具Battery Historian分析软件耗电情况:
测试前准备工作:
dumpsys batterystats --reset
dumpsys batterystats --enable full-wake-history
然后拔掉USB开始休眠测试,进入休眠后停止测试,然后通过以下指令获取日志:
adb bugreport bugreport.zip
注意:使用该分析工具时,被分析设备不能连接USB,也不要连接adb
1.wake_lock对CPU休眠的影响分析:
在后台有某些任务必须要做,不希望进入休眠时,可以申请wake_lock锁,阻止CPU进入休眠:
public static void requestWakeLock() {
if (wakeLock == null) {
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "TESTAPP");
wakeLock.setReferenceCounted(false);
}
wakeLock.acquire();
}
看一下wake_lock未释放状态下对CPU休眠的影响:
可以看到测试时没有释放wake_lock的状态下,灭屏后Long Wakelocks中统计到wake_lock锁一直被持有未释放,CPU一直在running状态,不能进入休眠。
看看释放wake_lock的状态下的统计情况:
public static void releaseWakeLock() {
if (wakeLock != null) {
wakeLock.release();
wakeLock = null;
}
}
上图的Long Wakelocks可以看到时间正好是设置任务处理的时间,任务处理完后释放了wake_lock锁,所以CPU才能进入休眠状态。因此申请wake_lock锁时,在任务执行完毕后必须释放wake_lock锁,避免CPU耗电。
2.Alarm任务对CPU休眠的影响分析:
为了分析AlarmManager对CPU休眠的影响,通过如下测试代码测试:
package com.example.allen.testapp;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.SystemClock;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
public class MainActivity extends AppCompatActivity {
private final static String TAG = "TESTAPP";
private static Context mContext = null;
private final static long TASK_TIME = 3 * 60 * 1000; //假设任务需要3分钟时间处理
private final static long ALARM_TIME = 5 * 60 * 1000; //每隔5分钟测试一次唤醒
private static PowerManager.WakeLock wakeLock;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
mContext = this;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.start_wakelock) {
requestWakeLock();
}else if(id == R.id.cancle_wakelock){
releaseWakeLock();
}else if(id == R.id.startAlarm){
setAlarm(ALARM_TIME);
}else if(id == R.id.cancleAlarm){
cancelAlarm();
}
return super.onOptionsItemSelected(item);
}
public static void requestWakeLock() {
if (wakeLock == null) {
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "TESTAPP");
wakeLock.setReferenceCounted(false);
}
wakeLock.acquire();
doTask();
}
public static void releaseWakeLock() {
if (wakeLock != null) {
wakeLock.release();
wakeLock = null;
}
}
public static void setAlarm(long delay) {
AlarmManager alarmManager = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE);
/*
Intent dailyIntent = new Intent(mContext, TestBroadcast.class);
PendingIntent dailyOperation = PendingIntent.getBroadcast(mContext, 0, dailyIntent, PendingIntent.FLAG_CANCEL_CURRENT);
*/
Intent dailyIntent = new Intent(mContext, TestService.class);
PendingIntent dailyOperation = PendingIntent.getService(mContext,0,dailyIntent, PendingIntent.FLAG_CANCEL_CURRENT);
alarmManager.cancel(dailyOperation);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP,SystemClock.elapsedRealtime() + delay, dailyOperation);
//AlarmManager.AlarmClockInfo clock = new AlarmManager.AlarmClockInfo(System.currentTimeMillis() + delay, dailyOperation);
//alarmManager.setAlarmClock(clock,dailyOperation);
} else
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager.setExact(android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + delay, dailyOperation);
} else {
alarmManager.set(android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + delay, dailyOperation);
}
Log.d(TAG,"setAlarm in");
}
public static void cancelAlarm(){
AlarmManager alarmManager = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE);
/*
Intent dailyIntent = new Intent(mContext, TestBroadcast.class);
PendingIntent dailyOperation = PendingIntent.getBroadcast(mContext, 0, dailyIntent, PendingIntent.FLAG_CANCEL_CURRENT);
*/
Intent dailyIntent = new Intent(mContext, TestService.class);
PendingIntent dailyOperation = PendingIntent.getService(mContext,0,dailyIntent, PendingIntent.FLAG_CANCEL_CURRENT);
alarmManager.cancel(dailyOperation);
Log.d(TAG,"cancelAlarm in");
}
public static void doTask(){
Log.d(TAG,"Test task in");
new Thread(new Runnable() {
@Override
public void run() {
Log.d(TAG,"Thread task in");
try{
long startTime = System.currentTimeMillis();
long count = 0;
while(true) {
Log.d(TAG,"retry task here :"+System.currentTimeMillis());
long enableTime = System.currentTimeMillis() - startTime;
if (enableTime > TASK_TIME) {
break;
}
count++;
Thread.sleep(10);
}
}catch (Exception e){
e.printStackTrace();
}
Log.d(TAG,"Thread task finish");
setAlarm(ALARM_TIME);
}
}).start();
}
public static class TestBroadcast extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
doTask();
}
}
public static class TestService extends Service{
@Override
public void onCreate() {
super.onCreate();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
doTask();
return super.onStartCommand(intent, flags, startId);
}
}
}
进入测试应用后配置Alarm任务,在Alarm任务开始之前灭屏准备休眠(在这个测试中是配置Alarm任务后的5分钟内):
阶段1:
有这么几个时刻:
第一次设置Alarm任务的时间:
03-22 16:10:47.450 15051 15051 D TESTAPP : setAlarm in
灭屏时刻:
(图中的时间和日志的时间有时区差别需要+8)可以看出是 16:10:52
Alarm任务的时刻:
03-22 16:20:33.243 15051 15051 D TESTAPP : Test task in
程序Alarm设置的5分钟,而执行间隔了10分钟,可见Alarm任务并没有唤醒CPU,而且这个任务在16:20:33.243的执行还是因为人为的点亮了屏幕唤醒CPU:
从这里推测,CPU在16:15:47时刻之前已经休眠了。
阶段2:
点亮屏幕后CPU被唤醒,Alarm任务继续进行。
第二次设置Alarm任务时刻:
03-22 16:23:33.259 15051 15620 D TESTAPP : setAlarm in
刚好是任务执行3分钟,符合程序预期。
第二次Alarm任务执行时刻:
03-22 16:24:50.259 15051 15051 D TESTAPP : Test task in
03-22 16:24:50.267 15051 27844 D TESTAPP : Thread task in
一分钟后Alarm任务开始执行,符合程序预期。
03-22 16:31:10.271 15051 27844 D TESTAPP : Thread task finish
03-22 16:31:10.277 15051 27844 D TESTAPP : setAlarm in
本来应该在3分钟后结束的Alarm任务,并没有结束,这里也是推测CPU已经休眠。
Alarm任务也是等到下一次亮屏CPU被唤醒后继续进行,这么来看还是符合程序设计预期的。
为了做个对比把Alarm设置为type:ELAPSED_REALTIME_WAKEUP进行测试:
阶段1:
第一次Alarm任务在14:49:12设置:
屏幕在14:49:19关闭:
从时间点上看到14:49:19 到 14:54:16期间屏幕是关闭的,CPU是陆陆续续running状态,在14:54:16时刻距离Alarm设置时间基本为5分钟(set接口没有setExtra接口精确,可参考Android接口介绍):
/** * Schedule an alarm to be delivered precisely at the stated time. * *
* This method is like {@link #set(int, long, PendingIntent)}, but does not permit * the OS to adjust the delivery time. The alarm will be delivered as nearly as * possible to the requested trigger time. * *
* Note: 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. * * @param type type of alarm. * @param triggerAtMillis time in milliseconds that the alarm should go * off, using the appropriate clock (depending on the alarm type). * @param operation Action to perform when the alarm goes off; * typically comes from {@link PendingIntent#getBroadcast * IntentSender.getBroadcast()}. * * @see #set * @see #setRepeating * @see #setWindow * @see #cancel * @see android.content.Context#sendBroadcast * @see android.content.Context#registerReceiver * @see android.content.Intent#filterEquals * @see #ELAPSED_REALTIME * @see #ELAPSED_REALTIME_WAKEUP * @see #RTC * @see #RTC_WAKEUP */ public void setExact(@AlarmType int type, long triggerAtMillis, PendingIntent operation) { setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, 0, operation, null, null, null, null, null); }
Alarm任务执行开始的时间是:
03-22 14:54:16.288 5219 5897 D TESTAPP : Thread task in
第一个Alarm任务开始执行的时间间隔5分钟,符合程序预期。
但是结束时间却是:
03-22 15:03:04.279 5219 5897 D TESTAPP : Thread task finish
为什么呢?
而且在这个时间点是我按电源键点亮了屏幕:
03-22 15:03:04.278 2249 2416 D WindowManager: interceptKeyTq keycode=285 interactive=false keyguardActive=false policyFlags=2000000
这么看来,在线程执行的某个时间点CPU休眠了,导致线程挂起了,待下次CPU恢复时,线程继续处理任务。
在15:03:04这个时间点,线程处理完任务后设置下一个Alarm任务
03-22 15:04:04.382 5219 5219 D TESTAPP : Test task in
03-22 15:04:04.383 5219 9610 D TESTAPP : Thread task in
间隔1分钟。符合程序预期。
任务在15:07:04正常结束,这个时间点配合Battery Historian图表分析,CPU刚好是正常running状态:
03-22 15:07:04.385 5219 9610 D TESTAPP : Thread task finish
从前面两个阶段看来,Alarm设置的type:ELAPSED_REALTIME_WAKEUP也并没有唤醒CPU来完成Alarm任务。有什么意义呢?
在Android6.0及以上的平台引入了低电耗模式:
https://developer.android.com/training/monitoring-device-state/doze-standby.html#restrictions
根据文档说明,在低电耗模式下AlarmManager闹铃(包括setExact()和setWindow())推迟到下一维护时段,这倒是符合之前的现象了。
那么在低电耗模式下触发的Alarm,需要使用使用 setAndAllowWhileIdle() 或 setExactAndAllowWhileIdle()。
setAndAllowWhileIdle测试效果:
根据程序配置,第一次Alarm在触发后5分钟开始进行,测试结果符合预期,但是第二次的Alarm事件在第一次Alarm任务执行完毕后的15分钟后,在测试结果表格中的 07:46:13时刻并没有唤醒动作(从 07:28:13开始,包括任务执行需要的3分钟和Alarm配置的任务时间15分钟),不符合预期结果。
setExactAndAllowWhileIdle测试效果也是如此,并没有唤醒CPU。
根据google的说明,setAlarmClock可以唤醒设备。
setAlarmClock接口测试:
这张是使用另外一个厂商的设备测试的结果。
按这么推理,Alarm任务并不可靠,ROM经过定制后可能导致在休眠状态下无法响应Alarm任务。