前言
电量优化,这个名词在传统PC时代,我们基本很少听见。然而到了诺基亚时代,我们也同样很少关注。直到了移动互联的智能机时代。电量优化才被慢慢的重视起来。可能的原因如下:
- 移动设备,不能一直使用电源供电,且电池容量有限
- 对于用户来说, 实际上App的电量损耗也是用户体验的一个方面。 特别是当今人们对移动设备的依赖度越来越高
曾经我们一个BOSS发现使用APP,挂在后台。然后手机插着充电,然后睡了个觉。起床发现电量只充满了70-80%,然后... ...
最后发现是一个重要原因由于内测版本的长连接开启了日志记录,频繁的进行IO操作引起.
这个时候你还不能狡辩了,因为用户最简单能通过系统设置来查看应用的耗电量排名。
在电子编程世界,这种硬件消耗电量 来执行任务的过程,叫做超时电流消耗,
任何电子编程专业的人都会告诉你,你的设备的各项活动在相同时间内,消耗的电量是不同的。比如,很多手机号称待机好几天,这个确实是真的,不过就是使用飞行模式放在家里什么都不干,确实可以甚至可以坚持10多天。但是我们一旦使用它,比如使用蜂窝式无线数据交换(3G4G)、屏幕保持唤醒状态等。作为开发者,我们很想知道我的应用执行的哪些任务消耗的电量是最多的?这个问题确实会很棘手。电量消耗的计算与统计是一件麻烦而且矛盾的事情,记录电量消耗本身也是一个费电量的事情(所以很多设备都把这个监测电量的功能阉割掉了。)。唯一可行的方案是使用第三方监测电量的设备,这样才能够获取到真实的电量消耗(因为第三方硬件监测的时候是用的自己的供电而不是用的手机的电量)。
手机那些地方最耗电?
唤醒屏幕
当用户电量屏幕的时候,意味着系统的各组件要开始进行工作,界面也需要开始执行渲染。
待机状态的电量消耗:
使用和唤醒屏幕后:
当设备从休眠状态中,被应用程序假面唤醒时,你会看到在第一次唤醒时,这里有一条电量使用高峰线
CPU唤醒使用
CUP唤醒时的高峰线:
接下来就是后续的一些执行的消耗了:
当工作完成后,设备会主动进行休眠,这非常重要,在不使用或者很少使用的情况下,长时间保持屏幕唤醒会迅速消耗电池的电量
蜂窝式无线
当设备通过无线网发送数据的时候,为了使用硬件,这里会出现一个唤醒好点高峰。接下来还有一个高数值,这是发送数据包消耗的电量,然后接受数据包也会消耗大量电量 也看到一个峰值。
通常情况下,使用3G移动网络传输数据,电量的消耗有三种状态:
- Full power: 能量最高的状态,移动网络连接被激活,允许设备以最大的传输速率进行操作。
- Low power: 一种中间状态,对电量的消耗差不多是Full power状态下的50%。
- Standby: 最低的状态,没有数据连接需要传输,电量消耗最少。
如何进行电量使用分析?
电量使用优化, 基本上是我们最不怎么关注的一项优化。可能很多公司连QA/Tester也不会关注测试App电量的使用。一般来说开发和测试的测试设备也一直是连着USB处于充电状态的,感官上也体会不到电量的损耗。要进行电量优化,我们首先得知道电都消耗到那里去了,不然如何进行针对性的优化呢?答案是通过google开源的Battery Historian来进行分析。
电量数据收集
Android 5.0及以上的设备, 允许我们通过adb命令dump出电量使用统计信息.
- 因为电量统计数据是持续的, 会非常大, 统计我们的待测试App之前先reset下, 连上设备, 命令行执行:
$ adb shell dumpsys batterystats --reset Battery stats reset.
- 断开测试设备, 操作我们的待测试App.
- 重新连接设备, 使用adb命令导出相关统计数据:
// 此命令持续记录输出, 想要停止记录时按Ctrl+C退出.
$ adb bugreport > bugreport.txt
导出的统计数据存储到bugreport.txt, 此时我们可以借助如下工具来图形化展示电池的消耗情况.
注意, 官方SDK文档导出文件方式为:
adb shell dumpsys batterystats > batterystats.txt
使用python historian.py batterystats.txt > batterystats.html查看数据
是battery-historian老版本的使用方式. 目前Battery Historian已更新2.0版本, 推荐使用bugreport方式导出数据分析, 可以看到更多信息.
安装工具
工具开源地址: Battery Historian
根据gitbub上面介绍,Battery History工具的安装有两种方式:
- 通过安装Docker环境来安装。(这种方式很简单,Docker真心好用,太彪悍了!)
Docker是一种容器,一般用于云计算和大数据平台。提倡的一种思想就是:软件即服务。这句话不是盖的,一句话就可以将别人发布的docker服务环境一次全部copy过来(注意是整个软件环境哦,相当于复制了一台一模一样的主机,连软件都不要安装了,全有了。彪悍吧!)
Docker只支持Windows10
由于笔者使用的window,无法通过Docker的方式,所以是采用第2点进行安装
2.通过编译gitbub上面的源码来安装
这真是一个虐心的过程,因为Battery History是Go语言开发的。我们需要安装Go环境、Pytho环境、Git环境,并配置好相关的环境变量。具体的软件安装教程就不附上了,这里贴一下工具的下载地址。
Go下载地址:GO环境安装
Python下载地址:Python环境安装
Git下载地址:Git环境安装
需要注意的是, Battery Historian是Go语言的, 安装Go的时候需要配置其bin的环境变量.
Python环境需要是2.7的(3.x不行), 建议使用pyenv管理本地的python环境.
另外, 因为Battery Historian是一个网页版工具, 涉及一些JS引用, 有时需要FQ.
安装及成果配置环境变量后
1.输入命令行go get -d -u github.com/google/battery-historian/…
**下载到GOPATH配置目录下
2.进入到$GOPATH/src/github.com/google/battery-historian目录下方
$ cd $GOPATH/src/github.com/google/battery-historian
3.运行Battery Historian
1) go run setup.go
Compile Javascript files using the Closure compiler
$ go run setup.go
等待数分钟或者10分钟左右,如果仍然没有下载成功,可以手动下载,如下操作
**下载【closure-library】和【closure-compiler】和【flot-axislabels】,解压放到GOROOT目录下third_party文件夹下方的的closure-compiler和closure-library和flot-axislabels文件夹 ../battery-historian hird_party;如果没有均手动创建
2)go run cmd/battery-historian/battery-historian.go
Run Historian on your machine (make sure $PATH contains $GOBIN)
$ go run cmd/battery-historian/battery-historian.go [--port <default:9999>]
4.检查/battery-historian是否运行,登录网址 http://localhost:9999查看
千呼万唤使出来,不容易。这里建议使用Chrome浏览器,并且一定要使用VPN,不然各种莫名的问题
分析指标
工具安装完成后,我们将adb采集的数据上传至Battery Historian,就可以得到电量的分析情况。
那么关键来了,这些指标具体代表什么含义呢?这里我们来做一下解释。
- 横坐标
横坐标就是一个时间范围,咱们的例子中统计的数据是以重置为起点,获取bugreport内容时刻为终点。我们一共采集了多长时间的数据
- 纵坐标
关键的数据点我们用表格来汇总一下。
数据项 | 含义 |
---|---|
battery_level | 电量,可以看出电量的变化 |
plugged | 充电状态,这一栏显示是否进行了充电,以及充电的时间范围 |
screen | 屏幕是否点亮,这一点可以考虑到睡眠状态和点亮状态下电量的使用信息 |
top | 该栏显示当前时刻哪个app处于最上层,就是当前手机运行的app,用来判断某个app对手机电量的影响,这样也能判断出该app的耗电量信息。该栏记录了应用在某一个时刻启动,以及运行的时间,这对我们比对不同应用对性能的影响有很大的帮助 |
wake_lock | wake_lock 该属性是记录wake_lock模块的工作时间。是否有停止的时候等 |
running | 界面的状态,主要判断是否处于idle的状态。用来判断无操作状态下电量的消耗 |
Job | 后台的工作,比如服务service的运行 |
data_conn | 数据连接方式的改变,上面的edge是说明采用的gprs的方式连接网络的。此数据可以看出手机是使用2g,3g,4g还是wifi进行数据交换的。这一栏可以看出不同的连接方式对电量使用的影响 |
status | 电池状态信息,有充电,放电,未充电,已充满,未知等不同状态 |
phone_signal_strength | 手机信号状态的改变。 这一栏记录手机信号的强弱变化图,依次来判断手机信号对电量的影响 |
health | 电池健康状态的信息,这个信息一定程度上反映了这块电池使用了多长时间 |
plug | 充电方式,usb或者插座,以及显示连接的时间 |
Sync | 是否跟后台同步 |
phone_in_call | 是否进行通话 |
gps | gps是否开启 |
如何进行电量优化?
关键的地方来了。了解了手机关键耗电的地方及分析耗电的工具后。接下来就是我们的核心,如何来进行电量的优化呢?首先我们先简单总结汇总一下耗电的相关因素
- 屏幕亮暗相关
- 设备awake,sleep的切换,尤其是唤醒.
- CPU运行相关
- 网络
- 传感器
我们接下来根据因素来逐一进行优化建议。
点滴积累
我们都知道屏幕的渲染及CPU的运行是耗电的主要因素之一。所以其实当我们在做内存优化、渲染优化、计算优化的时候,就已然在做电量优化。所以平时的开发中,我们要注意点滴性能的优化积累,实际上当我们来做电量分析的时候,也是在找自己挖的坑。所以尽量有意识在项目的开发过程中尽量少挖坑。所以这一点是我们在分析其他优化项首先要提到的一个点。
监听手机充电状态
我们可以通过下面的代码来获取手机的当前充电状态:
// 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!”);
}
在上面的例子演示了如何立即获取到手机的充电状态,得到充电状态信息之后,我们可以有针对性的对部分代码做优化。比如我们可以判断只有当前手机为AC充电状态时 才去执行一些非常耗电的操作。
/**
* This method checks for power by comparing the current battery state against all possible
* plugged in states. In this case, a device may be considered plugged in either by USB, AC, or
* wireless charge. (Wireless charge was introduced in API Level 17.)
*/
private boolean checkForPower() {
// 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);
// There are currently three ways a device can be plugged in. We should check them all.
int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
boolean usbCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_USB);
boolean acCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_AC);
boolean wirelessCharge = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
wirelessCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_WIRELESS);
}
return (usbCharge || acCharge || wirelessCharge);
}
这里我们就需要思考,根据我们自己的业务,那些为了省电,可以放当手机插上电源的时候去做。
往往这样的情况非常多。像这些不需要及时地和用户交互的操作可以放到后面处理。
比如:360手机助手,当充上电的时候,才会自动清理手机垃圾,自动备份上传图片、联系人等到云端;再比如我们自己的APP,其中有一块业务是相册备份,这个时候有一个选项控制让用户选择是否在低于15%的电量时还继续进行备份,从而避免当用户手机低电量时,任然继续进行耗电操作。
屏幕唤醒
当Android设备空闲时,屏幕会变暗,然后关闭屏幕,最后会停止CPU的运行,这样可以防止电池电量掉的快。但有些时候我们需要改变Android系统默认的这种状态:比如玩游戏时我们需要保持屏幕常亮,比如一些下载操作不需要屏幕常亮但需要CPU一直运行直到任务完成。
保持屏幕常亮
最好的方式是在Activity中使用FLAG_KEEP_SCREEN_ON 的Flag。
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}
这个方法的好处是不像唤醒锁(wake locks),需要一些特定的权限(permission)。并且能正确管理不同app之间的切换,不用担心无用资源的释放问题。
另一个方式是在布局文件中使用android:keepScreenOn属性:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
...
</RelativeLayout>
android:keepScreenOn = ” true “的作用和FLAG_KEEP_SCREEN_ON一样。使用代码的好处是你允许你在需要的地方关闭屏幕。
注意:一般不需要人为的去掉FLAG_KEEP_SCREEN_ON的flag,windowManager会管理好程序进入后台回到前台的的操作。如果确实需要手动清掉常亮的flag,使用getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
所以这里我们自己的APP需要根据业务来控制好是否保持屏幕常量。比如我们的APP需要支持视频播放。那么在播放的界面需要控制好不熄屏。当退出播放时,当然就没有了这个设置。
WakeLock
wake_lock锁主要是相对系统的休眠而言的,意思就是我的程序给CPU加了这个锁那系统就不会休眠了,这样做的目的是为了全力配合我们程序的运行。有的情况如果不这么做就会出现一些问题。
需要使用PowerManager这个系统服务的唤醒锁(wake locks)特征来保持CPU处于唤醒状态。唤醒锁允许程序控制宿主设备的电量状态。创建和持有唤醒锁对电池的续航有较大的影响,所以,除非是真的需要唤醒锁完成尽可能短的时间在后台完成的任务时才使用它。比如在Acitivity中就没必要用了。如果需要关闭屏幕,使用上述的FLAG_KEEP_SCREEN_ON。
只有一种合理的使用场景,是在使用后台服务在屏幕关闭情况下hold住CPU完成一些工作。 要使用唤醒锁,如果不使用唤醒锁来执行后台服务,不能保证因CPU休眠未来的某个时刻任务会停止,这不是我们想要的。
有的人可能认为我以前写的后台服务就没掉过链子呀运行得挺好的,1.可能是你的任务时间比较短;2.可能CPU被手机里面很多其他的软件一直在唤醒状态。
唤醒锁可划分为并识别四种用户唤醒锁:
标记值 | CPU | 屏幕 | 键盘 |
---|---|---|---|
PARTIAL_WAKE_LOCK | 开启 | 关闭 | 关闭 |
SCREEN_DIM_WAKE_LOCK | 开启 | 变暗 | 关闭 |
SCREEN_BRIGHT_WAKE_LOCK | 开启 | 变亮 | 关闭 |
FULL_WAKE_LOCK | 开启 | 变亮 | 变亮 |
请注意,自 API 等级 17 开始,FULL_WAKE_LOCK 将被弃用。 应用应使用 FLAG_KEEP_SCREEN_ON。
第一步就是添加唤醒锁权限:
<uses-permission android:name="android.permission.WAKE_LOCK" />
直接使用唤醒锁:
PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"MyWakelockTag");
wakeLock.acquire();
注意:在使用该类的时候,必须保证acquire和release是成对出现的。不然当我们业务已经不需要时,当CPU处于唤醒状态,这个时候就会损耗多余的电量。
JobScheduler
自 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);
//设置手机充电状态下触发
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();
}
GPS
选择合适的Location Provider
Android系统支持多个Location Provider:
- GPS_PROVIDER:
GPS定位,利用GPS芯片通过卫星获得自己的位置信息。定位精准度高,一般在10米左右,耗电量大;但是在室内,GPS定位基本没用。
- NETWORK_PROVIDER:
网络定位,利用手机基站和WIFI节点的地址来大致定位位置,这种定位方式取决于服务器,即取决于将基站或WIF节点信息翻译成位置信息的服务器的能力。
- PASSIVE_PROVIDER:
被动定位,就是用现成的,当其他应用使用定位更新了定位信息,系统会保存下来,该应用接收到消息后直接读取就可以了。比如如果系统中已经安装了百度地图,高德地图(室内可以实现精确定位),你只要使用它们定位过后,再使用这种方法在你的程序肯定是可以拿到比较精确的定位信息。
例如你的App只是需要一个粗略的定位那么就不需要使用GPS进行定位,既耗费电量,定位的耗时也久。
及时注销定位监听
在获取到定位之后或者程序处于后台时,注销定位监听,此时监听GPS传感器相当于执行no-op(无操作指令),用户不会有感知但是却耗电。
public void onPause() {
super.onPause();
locationManager.removeListener(locationListener);
}
public void onResume(){
super.onResume();
locationManager.requestLocationUpdates(locationManager.getBestProvider(criteria, true),6000,100,locationListener);
}
多模块使用定位尽量复用
多个模块使用定位,尽量复用上一次的结果,而不是都重新走定位的过程,节省电量损耗;例如:在应用启动的时候获取一次定位,保存结果,之后再用到定位的地方都直接去取。
传感器
使用传感器,选择合适的采样率,越高的采样率类型则越费电;
-
SENSOR_DELAY_NOMAL (200000微秒)
-
SENSOR_DELAY_UI (60000微秒)
-
SENSOR_DELAY_GAME (20000微秒)
-
SENSOR_DELAY_FASTEST (0微秒)
在后台时注意及时注销传感器监听
Doze and App Standby
最后提这一点,理论上不是电量优化,而是做电量优化要注意的一个坑。Doze and App Standby是Android 6.0以后,提供了两种省电延长电池寿命的功能。
具体可参考google官方介绍文档。
https://developer.android.google.cn/training/monitoring-device-state/doze-standby.html
这个东西目前已基本无解,特别是国内的不支持google的GCM。这个地方只能控制让用户授权加入白名单来解除限制。
总结
参考资料
推荐资料(Google官方教程)