八、Android性能优化之电量优化(二)

基于V2.0版本的battery historian请先看 battery historian安装与使用
##### (1).横坐标

横坐标就是一个时间范围,咱们的例子中统计的数据是以重置为起点,获取bugreport内容时刻为终点。我们一共采集了多长时间的数据

(2).纵坐标

关键的数据点我们用表格来汇总一下。

参数名 作用
CPU running CPU的运行状态,是否被唤醒。如果把鼠标放到上面去,还能看到更多的信息,如CPU唤醒的原因。
Screen 亮屏状态,可以看到图表中该项着色有间隔,这是因为实验期间我关闭过屏幕,每关闭一次屏幕,着色就被打断。
Top app 当前最上层的app
Mobile network type 网络类型,其中需要注意的是,“免费网络可能包括wifi、蓝牙网络共享、USB网络共享”
Mobile radio active 移动蜂窝信号 BP侧耗电,通常是指SIM卡,数据链接。该栏过多着色,间隔多。表示功耗也会高。
WiFi supplicant wifi是否开启
WiFi signal strength wifi强度
Wifi Running wifi连接情况下的耗电情况
Audio 音频是否开启
Battery Level 电量
Plugged 是否正在充电,以及鼠标放在上面的时候可以看到充电类型,包括AC(充电器)、USB、其它(例如无线充电)
Battery Level 开始测试时的电量,之前抓取的图可以看到电量是100,满电状态。
Top app 前台应用,如果要分析应用的耗电情况,那么在测试期间,就该保证应用一直处于前台。
Userspace wakelock 记录wake_lock模块的工作时间

ps:系统为了节省电量,CPU在没有任务忙的时候就会自动进入休眠。有任务需要唤醒CPU高效执行的时候,就会给CPU加wake_lock锁。

电量优化建议:

当Android设备空闲时,屏幕会变暗,然后关闭屏幕,最后会停止CPU的运行,这样可以防止电池电量掉的快。在休眠过程中自定义的Timer、Handler、Thread、Service等都会暂停。但有些时候我们需要改变Android系统默认的这种状态:比如玩游戏时我们需要保持屏幕常亮,比如一些下载操作不需要屏幕常亮但需要CPU一直运行直到任务完成。从而防止因为唤醒的瞬间而耗更多的电。

1、判断充电状态

这里我们就需要思考,根据我们自己的业务,那些为了省电,可以放当手机插上电源的时候去做。
往往这样的情况非常多。像这些不需要及时地和用户交互的操作可以放到后面处理。
比如:360手机助手,当充上电的时候,才会自动清理手机垃圾,自动备份上传图片、联系人等到云端;再比如我们自己的APP,其中有一块业务是相册备份,这个时候有一个选项控制让用户选择是否在低于15%的电量时还继续进行备份,从而避免当用户手机低电量时,任然继续进行耗电操作。

我们可以通过下面的代码来获取手机的当前充电状态:


// It is very easy to subscribe to changes to the battery state, but you can get the current
// state by simply passing null in as your receiver.  Nifty, isn't that?
IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent batteryStatus = this.registerReceiver(null, filter);
int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
boolean acCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_AC);
if (acCharge) {
    Log.v(LOG_TAG,“The phone is charging!”);
}
复制代码

  private boolean checkForPower() {
        //获取电池的充电状态(注册一个广播)
        IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
        Intent res = this.registerReceiver(null, filter);

        //通过使用BatteryManager的参数信息判断充电状态
        if (res != null) {
            int chargePlug = res.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
            boolean usb = chargePlug == BatteryManager.BATTERY_PLUGGED_USB;//usb充电
            boolean ac = chargePlug == BatteryManager.BATTERY_PLUGGED_AC;//交流电
            //无线充电,这个需要API>=17
            boolean wireless = false;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                wireless = chargePlug == BatteryManager.BATTERY_PLUGGED_WIRELESS;
            }
            return (usb || ac || wireless);
        } else {
            return false;
        }
    }
复制代码

调用示例


    private void applyFilter() {
        //是否在充电
        if(!checkForPower()){
            mPowerMsg.setText("请充上电,再处理!");
            return;
        }
        mCheyennePic.setImageResource(R.drawable.pink_cheyenne);
        mPowerMsg.setText(R.string.photo_filter);
    }
复制代码

2、屏幕保持常亮

为了防止屏幕唤醒一瞬间耗电过多,有一些应用,比如游戏、支付页面,需要保持屏幕常亮来节省电量:


getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
复制代码

也可以在布局文件里面使用,但是没有那么灵活:


android:keepScreenOn="true"
复制代码

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


getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
复制代码
android:keepScreenOn = ” true “的作用和FLAG_KEEP_SCREEN_ON一样。使用代码的好处是你允许你在需要的地方关闭屏幕。

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

3.1、使用wake_lock

系统为了节省电量,CPU在没有任务忙的时候就会自动进入休眠。有任务需要唤醒CPU高效执行的时候,就会给CPU加wake_lock锁。wake_lock锁主要是相对系统的休眠而言的,意思就是我的程序给CPU加了这个锁那系统就不会休眠了,这样做的目的是为了全力配合我们程序的运行。有的情况如果不这么做就会出现一些问题,比如微信等及时通讯的心跳包会在熄屏不久后停止网络访问等问题。所以微信里面是有大量使用到了wake_lock锁。
PowerManager这个系统服务的唤醒锁(wake locks)特征来保持CPU处于唤醒状态。唤醒锁允许程序控制宿主设备的电量状态。创建和持有唤醒锁对电池的续航有较大的影响,所以,除非是真的需要唤醒锁完成尽可能短的时间在后台完成的任务时才使用它。比如在Acitivity中就没必要用了。一种典型的代表就是在屏幕关闭以后,后台服务继续保持CPU运行。
如果不使用唤醒锁来执行后台服务,不能保证因CPU休眠未来的某个时刻任务会停止,这不是我们想要的。(有的人可能认为以前写的后台服务就没掉过链子呀运行得挺好的,
1.可能是你的任务时间比较短;
2.可能CPU被手机里面很多其他的软件一直在唤醒状态)。
其中,唤醒锁有下面几种类型:

wake_lock两种锁(从释放、使用的角度来看的话):计数锁和非计数锁(锁了很多次,只需要release一次就可以解除了)
请注意,自 API 等级17开始,FULL_WAKE_LOCK将被弃用,应使用FLAG_KEEP_SCREEN_ON代替。

综上所述,为了防止CPU唤醒一瞬间耗电过多,在执行关键代码的时候,为了防止CPU睡眠,需要使用唤醒锁来节省电量:

**

//创建唤醒锁
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLock wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "partial_lock");

//获取唤醒锁
wakeLock.acquire();

//一些关键的代码

//释放唤醒锁
wakeLock.release();
复制代码

需要添加权限:


<uses-permission android:name="android.permission.WAKE_LOCK"/>
复制代码

Tips:获取与释放唤醒锁需要成对出现
Tips:有一些意外的情况,比如小米手机是做了同步心跳包(心跳对齐)(如果超过了这个同步的频率就会被屏蔽掉或者降频),所有的app后台唤醒频率不能太高,这时候就需要降频,比如每隔2S中去请求。

3.2、使用WakefulBroadcastReceiver

上面提到,典型的使用场景就是后台服务需要保持CPU保持运行,但推荐的方式是使用WakefulBroadcastReceiver:使用广播和Service(典型的IntentService)结合的方式可以让你很好地管理后台服务的生命周期。

WakefulBroadcastReceiver是BroadcastReceiver的一种特例。它会为你的APP创建和管理一个PARTIAL_WAKE_LOCK 类型的WakeLock。一个WakeBroadcastReceiver接收到广播后将工作传递给Service(一个典型的IntentService),直到确保设备没有休眠。如果你在交接工作给服务的时候没有保持唤醒锁,在工作还没完成之前就允许设备休眠的话,将会出现一些你不愿意看到的情况。


public class MyIntentService extends IntentService {

    public MyIntentService(String name) {
        super(name);
    }

    public MyIntentService() {
        super(MyIntentService.class.getSimpleName());
    }

    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        if (intent != null) {
            //获取参数
            Bundle extras = intent.getExtras();

            //执行一些需要CPU保持唤醒的代码

            //执行结束,释放唤醒锁
            MyWakefulReceiver.completeWakefulIntent(intent);
        }
    }
}
复制代码

广播接收者:


public class MyWakefulReceiver extends WakefulBroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        Intent service = new Intent(context, MyIntentService.class);
        startWakefulService(context, service);
    }

}
复制代码

需要使用服务的时候,像一般的方式一样即可:


Intent intent = new Intent(this, MyIntentService.class);
//传递参数
intent.setData(Uri.parse("xxx"));
复制代码
分析WakefulBroadcastReceiver 源码(查看源码记得引入v4包)

public abstract class WakefulBroadcastReceiver extends BroadcastReceiver {
    private static final String EXTRA_WAKE_LOCK_ID = "android.support.content.wakelockid";

    private static final SparseArray<PowerManager.WakeLock> mActiveWakeLocks
            = new SparseArray<PowerManager.WakeLock>();
    private static int mNextId = 1;

    /**
     * Do a {@link android.content.Context#startService(android.content.Intent)
     * Context.startService}, but holding a wake lock while the service starts.
     * This will modify the Intent to hold an extra identifying the wake lock;
     * when the service receives it in {@link android.app.Service#onStartCommand
     * Service.onStartCommand}, it should pass back the Intent it receives there to
     * {@link #completeWakefulIntent(android.content.Intent)} in order to release
     * the wake lock.
     *
     * @param context The Context in which it operate.
     * @param intent The Intent with which to start the service, as per
     * {@link android.content.Context#startService(android.content.Intent)
     * Context.startService}.
     */
    public static ComponentName startWakefulService(Context context, Intent intent) {
        synchronized (mActiveWakeLocks) {
            int id = mNextId;
            mNextId++;
            if (mNextId <= 0) {
                mNextId = 1;
            }

            intent.putExtra(EXTRA_WAKE_LOCK_ID, id);
            ComponentName comp = context.startService(intent);
            if (comp == null) {
                return null;
            }

            PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
            PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
                    "wake:" + comp.flattenToShortString());
            wl.setReferenceCounted(false);
            wl.acquire(60*1000);
            mActiveWakeLocks.put(id, wl);
            return comp;
        }
    }

    /**
     * Finish the execution from a previous {@link #startWakefulService}.  Any wake lock
     * that was being held will now be released.
     *
     * @param intent The Intent as originally generated by {@link #startWakefulService}.
     * @return Returns true if the intent is associated with a wake lock that is
     * now released; returns false if there was no wake lock specified for it.
     */
    public static boolean completeWakefulIntent(Intent intent) {
        final int id = intent.getIntExtra(EXTRA_WAKE_LOCK_ID, 0);
        if (id == 0) {
            return false;
        }
        synchronized (mActiveWakeLocks) {
            PowerManager.WakeLock wl = mActiveWakeLocks.get(id);
            if (wl != null) {
                wl.release();
                mActiveWakeLocks.remove(id);
                return true;
            }
            // We return true whether or not we actually found the wake lock
            // the return code is defined to indicate whether the Intent contained
            // an identifier for a wake lock that it was supposed to match.
            // We just log a warning here if there is no wake lock found, which could
            // happen for example if this function is called twice on the same
            // intent or the process is killed and restarted before processing the intent.
            Log.w("WakefulBroadcastReceiver", "No active wake lock id #" + id);
            return true;
        }
    }
}
复制代码
我们发现WakefulBroadcastReceiver 本质上还是基于 PowerManager.WakeLock
注意:
1.注意添加权限
2.注意服务与广播的注册
3.使用广播来设计,就是为了解耦

网上采集的一些问题坑点及解决如下:

1.向服务器轮询的代码不执行。

曾经做一个应用,利用Timer和TimerTask,来设置对服务器进行定时的轮询,但是发现机器在某段时间后,轮询就不再进行了。查了很久才发 现是休眠造成的。后来解决的办法是,利用系统的AlarmService来执行轮询。因为虽然系统让机器休眠,节省电量,但并不是完全的关机,系统有一部 分优先级很高的程序还是在执行的,比如闹钟,利用AlarmService可以定时启动自己的程序,让cpu启动,执行完毕再休眠。

解决:利用系统的AlarmService来替代Timer和TimerTask 执行轮询
2.后台长连接断开。

最近遇到的问题。利用Socket长连接实现QQ类似的聊天功能,发现在屏幕熄灭一段时间后,Socket就被断开。屏幕开启的时候需进行重连,但 每次看Log的时候又发现网络是链接的,后来才发现是cpu休眠导致链接被断开,当你插上数据线看log的时候,网络cpu恢复,一看网络确实是链接的, 坑。最后使用了PARTIAL_WAKE_LOCK,保持CPU不休眠。

解决:使用了PARTIAL_WAKE_LOCK,保持CPU不休眠。
3.调试时是不会休眠的。

让我非常郁闷的是,在调试2的时候,就发现,有时Socket会断开,有时不会断开,后来才搞明白,因为我有时是插着数据线进行调试,有时拔掉数据线,这 时Android的休眠状态是不一样的。而且不同的机器也有不同的表现,比如有的机器,插着数据线就会充电,有的不会,有的机器的设置的充电时屏幕不变暗 等等,把自己都搞晕了。其实搞明白这个休眠机制,一切都好说了。

通过Wakelock Detector(WLD)软件可以看到手机中的Wakelock:

3.3、大量高频次的CPU唤醒及操作使用JobScheduler/GCM

自 Android 5.0 发布以来,JobScheduler 已成为执行后台工作的首选方式,其工作方式有利于用户。应用可以在安排作业的同时允许系统基于内存、电源和连接情况进行优化。JobSchedule的宗旨就是把一些不是特别紧急的任务放到更合适的时机批量处理。这样做有两个好处:

避免频繁的唤醒硬件模块,造成不必要的电量消耗。
避免在不合适的时间(例如低电量情况下、弱网络或者移动网络情况下的)执行过多的任务消耗电量;

JobScheduler的简单使用,首先自定义一个Service类,继承自JobService


public class JobSchedulerService extends JobService{
    private String TAG = JobSchedulerService.class.getSimpleName();

    @Override
    public boolean onStartJob(JobParameters jobParameters) {
        Log.d(TAG, "onStartJob:" + jobParameters.getJobId());

        if(true) {
            // JobService在主线程运行,如果我们这里需要处理比较耗时的业务逻辑需单独开启一条子线程来处理并返回true,
            // 当给定的任务完成时通过调用jobFinished(JobParameters params, boolean needsRescheduled)告知系统。

            //假设开启一个线程去下载文件
            new DownloadTask().execute(jobParameters);

            return true;

        }else {
            //如果只是在本方法内执行一些简单的逻辑话返回false就可以了
            return false;
        }
    }

    /**
     * 比如我们的服务设定的约束条件为在WIFI状态下运行,结果在任务运行的过程中WIFI断开了系统
     * 就会通过回掉onStopJob()来通知我们停止运行,正常的情况下不会回掉此方法
     *
     * @param jobParameters
     * @return
     */
    @Override
    public boolean onStopJob(JobParameters jobParameters) {
        Log.d(TAG, "onStopJob:" + jobParameters.getJobId());

        //如果需要服务在设定的约定条件再次满足时再次执行服务请返回true,反之false
        return true;
    }

    class DownloadTask extends AsyncTask<JobParameters, Object, Object> {
        JobParameters mJobParameters;

        @Override
        protected Object doInBackground(JobParameters... jobParameterses) {
            mJobParameters = jobParameterses[0];

            //比如说我们这里处理一个下载任务
            //或是处理一些比较复杂的运算逻辑
            //...

            try {
                Thread.sleep(30*1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            return null;
        }

        @Override
        protected void onPostExecute(Object o) {
            super.onPostExecute(o);
            //如果在onStartJob()中返回true的话,处理完成逻辑后一定要执行jobFinished()告知系统已完成,
            //如果需要重新安排服务请true,反之false
            jobFinished(mJobParameters, false);
        }
    }
}
复制代码

记得在Manifest文件内配置Service


 <service android:name=".JobSchedulerService" android:permission="android.permission.BIND_JOB_SERVICE"/>
复制代码

创建工作计划


public class MainActivity extends Activity{
    private JobScheduler mJobScheduler;
    private final int JOB_ID = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.mai_layout);

        mJobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE );

        //通过JobInfo.Builder来设定触发服务的约束条件,最少设定一个条件
        JobInfo.Builder jobBuilder = new JobInfo.Builder(JOB_ID, new ComponentName(this, JobSchedulerService.class));

        //循环触发,设置任务每三秒定期运行一次
        jobBuilder.setPeriodic(3000);

        //单次定时触发,设置为三秒以后去触发。这是与setPeriodic(long time)不兼容的,
        // 并且如果同时使用这两个函数将会导致抛出异常。
        jobBuilder.setMinimumLatency(3000);

        //在约定的时间内设置的条件都没有被触发时三秒以后开始触发。类似于setMinimumLatency(long time),
        // 这个函数是与 setPeriodic(long time) 互相排斥的,并且如果同时使用这两个函数,将会导致抛出异常。
        jobBuilder.setOverrideDeadline(3000);

        //在设备重新启动后设置的触发条件是否还有效
        jobBuilder.setPersisted(false);

        // 只有在设备处于一种特定的网络状态时,它才触发。
        // JobInfo.NETWORK_TYPE_NONE,无论是否有网络均可触发,这个是默认值;
        // JobInfo.NETWORK_TYPE_ANY,有网络连接时就触发;
        // JobInfo.NETWORK_TYPE_UNMETERED,非蜂窝网络中触发;
        // JobInfo.NETWORK_TYPE_NOT_ROAMING,非漫游网络时才可触发;
        jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);


         /**
             设置重试/退避策略,当一个任务调度失败的时候执行什么样的测量采取重试。
             initialBackoffMillis:第一次尝试重试的等待时间间隔ms
             *backoffPolicy:对应的退避策略。比如等待的间隔呈指数增长。
             */
      // jobBuilder .setBackoffCriteria(long initialBackoffMillis, int backoffPolicy)
        jobBuilder .setBackoffCriteria(JobInfo.MAX_BACKOFF_DELAY_MILLIS, JobInfo.BACKOFF_POLICY_LINEAR)
        //设置手机充电状态下触发
        jobBuilder.setRequiresCharging(true);

        //设置手机处于空闲状态时触发
        jobBuilder.setRequiresDeviceIdle(true);

        //得到JobInfo对象
        JobInfo jobInfo = jobBuilder.build();

        //设置开始安排任务,它将返回一个状态码
        //JobScheduler.RESULT_SUCCESS,成功
        //JobScheduler.RESULT_FAILURE,失败
        if (mJobScheduler.schedule(jobInfo) == JobScheduler.RESULT_FAILURE) {
            //安排任务失败
        }

        //停止指定JobId的工作服务
        mJobScheduler.cancel(JOB_ID);
        //停止全部的工作服务
        mJobScheduler.cancelAll();
    }
复制代码

量高频次的CPU唤醒及操作,我们最好把这些操作集中处理。我们可以采取一些算法来解决。
可以借鉴谷歌的精髓,JobScheduler/GCM。

4、使用AlarmManager来唤醒

当机器一段时间不操作以后,就会进入睡眠状态。向服务器的轮询就会停止、长连接就会断开,为了防止这样的情况,就可以使用AlarmManager:


Intent intent = new Intent(this, TestService.class);
PendingIntent pi = PendingIntent.getService(this, 0, intent, 0);

AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
am.cancel(pi);

//闹钟在系统睡眠状态下会唤醒系统并执行提示功能
//模糊时间,在API-19中以及以前,setRepeating都是不准确的
am.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 1000, 2000, pi);
//准确时间,但是需要在API-17之后使用
am.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 1000, pi);
am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 1000, pi);
复制代码

该定时器可以启动Service服务、发送广播、跳转Activity,并且会在系统睡眠状态下唤醒系统。所以该方法不用获取电源锁和释放电源锁。
关于AlarmManager的更多信息,请参考其他文章。
在19以上版本,setRepeating中设置的频率只是建议值(6.0 的源码中最小值是60s),如果要精确一些的用setWindow或者setExact。

5、其他优化

当然,电量优化是包括很多方面的,例如:

渲染优化
定位策略优化
网络优化,例如网络缓存处理,请求方式、次数优化、设置超时时间等等
代码执行效率优化
防止内存泄漏
等等,电量优化无处不在。

深化

首先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进入休眠状态,就会成为待机电池杀手。比如前段时间的某应用,比如现在仍然干着这事的某应用。

AlarmManager 是Android 系统封装的用于管理 RTC 的模块,RTC (Real Time Clock) 是一个独立的硬件时钟,可以在 CPU 休眠时正常运行,在预设的时间到达时,通过中断唤醒 CPU。(极光推送就是利用这个来做的。)

总结:

1.关键逻辑的执行过程,就需要Wake Lock来保护。如断线重连重新登陆
2.休眠的情况下如何唤醒来执行任务?用AlarmManager。如推送消息的获取

其他参考资料:
alarmManager在手机休眠时无法唤醒Service的问题?( 为了对付的频繁唤醒的”流氓”app,有的厂家都开发了心跳对齐。)
www.zhihu.com/question/36…
微信 Android 版 6.2 为什么设置了大量长时间的随机唤醒锁?
www.zhihu.com/question/31…

Guess you like

Origin juejin.im/post/7034751791866576904