对于Android移动应用的开发者来说,耗电量的控制一直是个老大难问题。
我们想要控制耗电量,必须要有工具或者方法比较准确的定位应用的耗电情况。下面,我们先来分析下如何计算android应用的耗电量。
在android自带的设置里面有电量计算的界面,如下图:
<ignore_js_op>
我们看下是如何实现的:
在android framework里面有专门负责电量统计的Service:BatteryStatsSerive。这个Service在ActivityManagerService中创建,代码如下:
1 | mBatteryStatsService = new BatteryStatsService(new File(systemDir, 'batterystats.bin').toString()); |
其他的模块比如WakeLock和PowerManagerService会向BatteryStatsService喂数据,数据是存放到系统目录batterystats.bin文件,然后交于BatteryStatsImpl这个数据分析器来进行电量数据的分析,系统的设置就是这样得到电量的统计信息的。
拿到相关的数据后,电量的计算又是如何得出的呢?这里用到了如下的计算公式:
应用运行总时间 = 应用在Linux内核态运行时间 + 应用在Linux用户态运行时间
CPU工作总时间 = 软件运行期间CPU每个频率下工作的时间之和比例
应用消耗的电量 = CPU每个频率等级下工作的时间比例/CPU工作总时间 * 应用运行总时间
* 不同频率下消耗的电量 + 数据传输消耗的电量(WI-FI或者移动网络)+ 使用所有传感器消耗的电量 + 唤醒锁消耗的电量。
相应的代码片段如下:
001 | private void processAppUsage() { |
002 | SensorManager sensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); |
003 | final int which = mStatsType; |
004 | final int speedSteps = mPowerProfile.getNumSpeedSteps(); |
005 | final double[] powerCpuNormal = new double[speedSteps]; |
006 | final long[] cpuSpeedStepTimes = new long[speedSteps]; |
007 | for (int p = 0; p < speedSteps; p++) { |
008 | powerCpuNormal[p] = mPowerProfile.getAveragePower |
009 | PowerProfile.POWER_CPU_ACTIVE, p); |
011 | final double averageCostPerByte = getAverageDataCost(); |
012 | long uSecTime = mStats.computeBatteryRealtime( |
013 | SystemClock.elapsedRealtime() * 1000, which); |
014 | mStatsPeriod = uSecTime; |
016 | SparseArray<? extends Uid> uidStats = mStats.getUidStats(); |
017 | final int NU = uidStats.size(); |
018 | for (int iu = 0; iu < NU; iu++) { |
019 | Uid u = uidStats.valueAt(iu); |
021 | double highestDrain = 0; |
022 | String packageWithHighestDrain = null; |
023 | Map<String, ? extends BatteryStats.Uid.Proc> proce ssStats = u.getProcessStats(); |
027 | if (processStats.size() > 0) { |
029 | for (Map.Entry<String, ? extends BatteryStats.Uid.Proc> ent : processStats.entrySet()) { |
031 | Log.i(TAG, 'Process name = ' + ent.getKey()); |
032 | Uid.Proc ps = ent.getValue(); |
033 | final long userTime = ps.getUserTime(which); |
034 | final long systemTime = ps.getSystemTime(which); |
035 | final long foregroundTime = ps.getForegroundTime(which); |
036 | cpuFgTime += foregroundTime * 10; // convert to millis |
037 | final long tmpCpuTime = (userTime + systemTime) * 10; // convert to millis |
038 | int totalTimeAtSpeeds = 0; |
039 | // Get the total first |
040 | for (int step = 0; step < speedSteps; step++) { |
041 | cpuSpeedStepTimes[step] = ps.getTimeAtCpuSpeedStep(step, which); |
042 | totalTimeAtSpeeds += cpuSpeedStepTimes[step]; |
044 | if (totalTimeAtSpeeds == 0) |
045 | totalTimeAtSpeeds = 1; |
046 | // Then compute the ratio of time spent at each speed |
047 | double processPower = 0; |
048 | for (int step = 0; step < speedSteps; step++) { |
049 | double ratio = (double) cpuSpeedStepTimes[step]/ totalTimeAtSpeeds; |
050 | processPower += ratio * tmpCpuTime* powerCpuNormal[step]; |
052 | cpuTime += tmpCpuTime; |
053 | power += processPower; |
054 | if (highestDrain < processPower) { |
055 | highestDrain = processPower; |
056 | packageWithHighestDrain = ent.getKey(); |
063 | cpuTime = cpuFgTime; // Statistics may not have been gathered yet. |
067 | // Add cost of data traffic |
068 | long tcpBytesReceived = u.getTcpBytesReceived(mStatsType); |
069 | long tcpBytesSent = u.getTcpBytesSent(mStatsType); |
070 | power += (tcpBytesReceived + tcpBytesSent) * averageCostPerByte; |
072 | // Process Sensor usage |
073 | Map<Integer, ? extends BatteryStats.Uid.Sensor> sensorStats = u.getSensorStats(); |
074 | for (Map.Entry<Integer, ? extends BatteryStats.Uid.Sensor> sensorEntry : sensorStats.entrySet()) { |
075 | Uid.Sensor sensor = sensorEntry.getValue(); |
076 | int sensorType = sensor.getHandle(); |
077 | BatteryStats.Timer timer = sensor.getSensorTime(); |
078 | long sensorTime = timer.getTotalTimeLocked(uSecTime, which) / 1000; |
079 | double multiplier = 0; |
080 | switch (sensorType) { |
082 | multiplier = mPowerProfile.getAveragePower(PowerProfile.POWER_GPS_ON); |
083 | gpsTime = sensorTime; |
086 | android.hardware.Sensor sensorData = sensorManager |
087 | .getDefaultSensor(sensorType); |
088 | if (sensorData != null) { |
089 | multiplier = sensorData.getPower(); |
093 | power += (multiplier * sensorTime) / 1000; |
096 | // Add the app to the list if it is consuming power |
098 | BatterySipper app = new BatterySipper(packageWithHighestDrain,0, u, new double[] { power }); |
099 | app.cpuTime = cpuTime; |
100 | app.gpsTime = gpsTime; |
101 | app.cpuFgTime = cpuFgTime; |
102 | app.tcpBytesReceived = tcpBytesReceived; |
103 | app.tcpBytesSent = tcpBytesSent; |
106 | if (power > mMaxPower) |
108 | mTotalPower += power; |
110 | Log.i(TAG, 'Added power = ' + power); |
通过代码我们看到,每个影响电量消耗的base值其实是事先配置好的,在系统res下power_profile.xml,所以通过这个方式计算出来的电量消耗值也只能作为一个经验值或者是参考值,和物理上的耗电值应该还是有所偏差的。
那我们还能用啥方式去比较准确的去获取耗电量呢?我们想到了曹冲称象的故事,可以用差值的方式进行尝试。在相同时间单位内,在没有安装应用的手机上和安装了应用的手机上记录耗电量,取差值为该应用的耗电量。在测试过程中注意几点,保证该手机相对“干净”,开始前需要结束所有的后台程序,将手机电量冲满,保证每次的起步点相同,这里推荐电量监控程序Battery Monitor Widget,这款软件功能比较强大,可以看到历史的电量变化。这两种测试方式可以同时使用,互为印证,已经应用到在Agoo Android SDK的测试中。
拿到电量数据后,紧接着就是如何优化电量的问题了。通过电量的计算公式我们可以看到影响电量的因子无非就是CPU的时间和网络数据以及Wakelock,GPS的使用。
在09年Google IO大会Jeffrey Sharkey的演讲(Coding for Life — Battery Life, That Is)中就探讨了这个问题,指出android应用的耗电主要在以下三个方面:
- 大数据量的传输。
- 不停的在网络间切换。
- 解析大量的文本数据。
并提出了相关的优化建议:
- 在需要网络连接的程序中,首先检查网络连接是否正常,如果没有网络连接,那么就不需要执行相应的程序。
- 使用效率高的数据格式和解析方法,推荐使用JSON和Protobuf。
- 目在进行大数据量下载时,尽量使用GZIP方式下载。
- 其它:回收java对象,特别是较大的java对像,使用reset方法;对定位要求不是太高的话尽量不要使用GPS定位,可能使用wifi和移动网络cell定位即可;尽量不要使用浮点运算;获取屏幕尺寸等信息可以使用缓存技术,不需要进行多次请求;使用AlarmManager来定时启动服务替代使用sleep方式的定时任务。
作为app开发者,或许很少有人会注意app对电量的损耗,但是用户对电量可是很敏感的,app做好电量损耗的优化会为自己的app加分不少。
如果是一个好的负责任的开发者,就应该限制app对电量的影响,当没有网络连接的时候,禁用后台服务更新,当电池电量低的时候减少更新的频率,确保自己的app对电池的影响降到最低。当电池充电或者电量比较饱和时,可以最大限度的发挥app的刷新率
1 | <receiver android:name=".PowerConnectReceiver"> |
3 | <action android:name="android.intent.action.ACTION_POWER_CONNECTED"/> |
4 | <action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/> |
01 | public class PowerConnectionReceiver extends BroadcastReceiver { |
03 | public void onReceive(Context context, Intent intent) { |
04 | int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1); |
05 | boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || |
06 | status == BatteryManager.BATTERY_STATUS_FULL; |
08 | int chargeFlag = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); |
09 | boolean usbCharge = chargeFlag == BATTERY_PLUGGED_USB; |
10 | boolean acCharge = chargeFlag == BATTERY_PLUGGED_AC; |
3 | int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS,-1); |
5 | boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||status == BatteryManager.BATTERY_STATUS_FULL; |
2 | int chargeFlag = battery.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); |
3 | boolean usbCharge = chargeFlag == BATTERY_PLUGGED_USB; |
4 | boolean acCharge = chargeFlag == BATTERY_PLUGGED_AC; |
1 | 不断的检测电量也会影响电池的使用时间,我们可以这样做 |
1 | <receiver android:name=".BatteryLevelReceiver"> |
3 | <action android:name="android.intent.action.ACTION_BATTERY_LOW"/> <actionandroid:name="android.intent.action.ACTION_BATTERY_OKAY"/> </intent-filter> |
当电量低或者满时会触发
有时间再写确定和监测连接状态
测试结论:
1)灭屏待机最省电:
a)任何App包括后台Service应该尽可能减少唤醒CPU的次数,比如IM类业务的长连接心跳、QQ提醒待机闹钟类业务的alarm硬时钟唤醒要严格控制;
b)每次唤醒CPU执行的代码应该尽可能少,从而让CPU迅速恢复休眠,比如申请wake lock的数量和持有时间要好好斟酌;
2)Wi-Fi比蜂窝数据,包括2G(GPRS)、3G更省电:
a)尽量在Wi-Fi下传输数据,当然这是废话,不过可以考虑在有Wi-Fi的时候做预加载,比如应用中心的zip包、手Q web类应用的离线资源等;
b)非Wi-Fi下,尽量减少网络访问,每一次后台交互都要考虑是否必须。虽然WiFi接入方式已经占到移动互联网用户的50%,但是是有些手机设置为待机关闭WiFi连接,即便有Wi-Fi信号也只能切换到蜂窝数据;
测试分析:
1)灭屏的情况:
a)灭屏待机,CPU处于休眠状态,最省电(7mA);
b)灭屏传输,CPU被激活,耗电显著增加,即便是处理1K的心跳包,电量消耗也会是待机的6倍左右(45mA);
c)灭屏传输,高负载download的时候WiFi最省电(70mA),3G(270mA)和2G(280mA)相当,是WiFi的4倍左右;
2)亮屏的情况:
a)亮屏待机,CPU处于激活状态,加上屏幕耗电,整机电量消耗不小(140mA);
b)亮屏传输,如果只是处理1K的心跳包,耗电增加不多(150mA),即便是很大的心跳包(64K),消耗增加也不明显(160mA);
c)亮屏传输,高负载download的时候WiFi最省电(280mA),3G(360mA)和2G(370mA)相当,是WiFi的1.3倍左右;
3)Alarm唤醒频繁会导致待机耗电增加:
手机灭屏后会进入待机状态,这时CPU会进入休眠状态。Android的休眠机制介绍的文章很多,这里引用一段网络文章:
Early suspend是android引进的一种机制,这种机制在上游备受争议,这里 不做评论。这个机制作用在关闭显示的时候,在这个时候,一些和显示有关的 设备,比如LCD背光,比如重力感应器,触摸屏,这些设备都会关掉,但是系统可能还是在运行状态(这时候还有wake lock)进行任务的处理,例如在扫描SD卡上的文件等.在嵌入式设备中,背光是一个很大的电源消耗,所以android会加入这样一种机制.
Late Resume是和suspend配套的一种机制,是在内核唤醒完毕开始执行的.主要就是唤醒在Early Suspend的时候休眠的设备.
Wake Lock在Android的电源管理系统中扮演一个核心的角色. Wake Lock是一种锁的机制,只要有人拿着这个锁,系统就无法进入休眠,可以被用户态程序和内核获得.这个锁可以是有超时的或者是没有超时的,超时的锁会在时间过去以后自动解锁.如果没有锁了或者超时了,内核就会启动休眠的那套机制来进入休眠.
当用户写入mem或者standby到/sys/power/state中的时候, state_store()会被调用,然后Android会在这里调用request_suspend_state()而标准的Linux会在这里进入enter_state()这个函数.如果请求的是休眠,那么early_suspend这个workqueue就会被调用,并且进入early_suspend
简单的说,当用户按power键,使得手机进入灭屏休眠状态,Android系统其实是做了前面说的一些工作:关闭屏幕、触摸屏、传感器、dump当前用户态和内核态程序运行上下文到内存或者硬盘、关闭CPU供电,当然为了支持语音通讯,modern等蜂窝信令还是工作的。
这种情况下,应用要唤醒CPU,只有两种可能:
a)通过服务器主动PUSH数据,通过网络设备激活CPU;
b)设置alarm硬件闹钟唤醒CPU;
这里我们重点分析第二种情况。首先来看看什么是alarm硬件闹钟。Google官方提供的解释是:Android提供的alarm services可以帮助应用开发者能够在将来某一指定的时刻去执行任务。当时间到达的时候,Android系统会通过一个Intent广播通知应用去完成这一指定任务。即便CPU休眠,也不影响alarm services的服务,这种情况下可以选择唤醒CPU。
显然唤醒CPU是有电量消耗的,CPU被唤醒的次数越多,耗电量会越大。现在很多应用为了维持心跳、拉取数据、主动PUSH会不同程度地注册alarm服务,导致Android系统被频繁唤醒。这就是为什么雷军说Android手机在安装了TOP100的应用后,待机时间会大大缩短的重要原因。
比较简单评测CPU唤醒次数的方法是看dumpsys alarm,这里会详细记录从开机到当前的各个进程和服务唤醒CPU的次数和时间。通过对比唤醒次数和唤醒时间可以帮助我们分析后台进程和服务的耗电情况。Dumpsys alarm的输出看起来像这样:
其中544代表唤醒次数,38684ms代表唤醒时间。
4)Wake locks持有时间过长会导致耗电增加:
Wake locks是一种锁机制,有些文献翻译成唤醒锁。简单说,前面讲的灭屏CPU休眠还需要做一个判断,就是看是否还有任何应用持有wake locks。如果有,CPU将不会休眠。有些应用不合理地申请wake locks,或者申请了忘记释放,都会导致手机无法休眠,耗电增加。
原始数据:
测试方法:硬件设备提供稳压电源替代手机电池供电,在不同场景下记录手机平均电流。
测试设备:Monsoon公司的Power Monitor TRMT000141
测试机型:Nexus One
灭屏benchmark(CPU进入休眠状态):7mA
灭屏WiFi:70 mA
灭屏3G net:270 mA
灭屏2G net GPRS:280mA
亮屏benchmark:140mA
亮屏Wi-Fi:280mA
亮屏3G net:360mA
亮屏2G:370mA
亮屏待机:140mA
亮屏Wi-Fi ping 1024包:150mA
亮屏Wi-Fi ping 65500包:160mA
灭屏 屏1024:45mA
灭屏ping 65500:55mA
关闭所有数据网络待机:7mA
显而易见,大部分的电都消耗在了网络连接、GPS、传感器上了。
简单的说也就是主要在以下情况下耗电比较多:
1、 大数据量的传输。
2、 不停的在网络间切换。
3、 解析大量的文本数据。
那么我们怎么样来改善一下我们的程序呢?
1、 在需要网络连接的程序中,首先检查网络连接是否正常,如果没有网络连接,那么就不需要执行相应的程序。
检查网络连接的方法如下:
ConnectivityManager mConnectivity;
TelephonyManager mTelephony;
……
// 检查网络连接,如果无网络可用,就不需要进行连网操作等
NetworkInfo info = mConnectivity.getActiveNetworkInfo();
if (info == null ||
!mConnectivity.getBackgroundDataSetting()) {
return false;
}
//判断网络连接类型,只有在3G或wifi里进行一些数据更新。
int netType = info.getType();
int netSubtype = info.getSubtype();
if (netType == ConnectivityManager.TYPE_WIFI) {
return info.isConnected();
} else if (netType == ConnectivityManager.TYPE_MOBILE
&& netSubtype == TelephonyManager.NETWORK_TYPE_UMTS
&& !mTelephony.isNetworkRoaming()) {
return info.isConnected();
} else {
return false;
}
2、 使用效率高的数据格式和解析方法。
通过测试发现,目前主流的数据格式,使用树形解析(如DOM)和流的方式解析(SAX)对比情况如下图所示:
很明显,使用流的方式解析效率要高一些,因为DOM解析是在对整个文档读取完后,再根据节点层次等再组织起来。而流的方式是边读取数据边解析,数据读取完后,解析也就完毕了。
在数据格式方面,JSON和Protobuf效率明显比XML好很多,XML和JSON大家都很熟悉,Protobuf是Google提出的,一种语言无关、平台无关、扩展性好的用于通信协议、数据存储的结构化数据串行化方法。有兴趣的可以到官方去看看更多的信息。
从上面的图中我们可以得出结论就是尽量使用SAX等边读取边解析的方式来解析数据,针对移动设备,最好能使用JSON之类的轻量级数据格式为佳。
3、 目前大部门网站都支持GZIP压缩,所以在进行大数据量下载时,尽量使用GZIP方式下载。
使用方法如下所示:
import java.util.zip.GZIPInputStream;
HttpGet request =
new HttpGet("http://example.com/gzipcontent");
HttpResponse resp =
new DefaultHttpClient().execute(request);
HttpEntity entity = response.getEntity();
InputStream compressed = entity.getContent();
InputStream rawData = new GZIPInputStream(compressed);
使用GZIP压缩方式下载数据,能减少网络流量,下图为使用GZIP方式获取包含1800个主题的RSS对比情况。
4、 其它一些优化方法:
回收java对象,特别是较大的java对像
XmlPullParserFactory and BitmapFactory
Matcher.reset(newString) for regex
StringBuilder.sentLength(0)
对定位要求不是太高的话尽量不要使用GPS定位,可能使用wifi和移动网络cell定位即可。GPS定位消耗的电量远远高于移动网络定位。
尽量不要使用浮点运算。
获取屏幕尺寸等信息可以使用缓存技术,不需要进行多次请求。
很多人开发的程序后台都会一个service不停的去服务器上更新数据,在不更新数据的时候就让它sleep,这种方式是非常耗电的,通常情况下,我们可以使用AlarmManager来定时启动服务。如下所示,第30分钟执行一次。
AlarmManager am = (AlarmManager)
context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, MyService.class);
PendingIntent pendingIntent =
PendingIntent.getService(context, 0, intent, 0);
long interval = DateUtils.MINUTE_IN_MILLIS * 30;
long firstWake = System.currentTimeMillis() + interval;
am.setRepeating(AlarmManager.RTC,firstWake, interval, pendingIntent);
最后一招,在运行你的程序前先检查电量,电量太低,那么就提示用户充电之类的,使用方法:
public void onCreate() {
// Register for sticky broadcast and send default
registerReceiver(mReceiver, mFilter);
mHandler.sendEmptyMessageDelayed(MSG_BATT, 1000);
}
IntentFilter mFilter =
new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
BroadcastReceiver mReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
// Found sticky broadcast, so trigger update
unregisterReceiver(mReceiver);
mHandler.removeMessages(MSG_BATT);
mHandler.obtainMessage(MSG_BATT, intent).sendToTarget();
}
};