Android应用的电量消耗和优化的策略

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

Android应⽤的电量消耗和优化的策略
对于Android移动应⽤的开发者来说,耗电量的控制⼀直是个⽼⼤难问题。

我们想要控制耗电量,必须要有⼯具或者⽅法⽐较准确的定位应⽤的耗电情况。

下⾯,我们先来分析下如何计算android应⽤的耗电量。

在android⾃带的设置⾥⾯有电量计算的界⾯,如下图:
<ignore_js_op>
我们看下是如何实现的:
在android framework⾥⾯有专门负责电量统计的Service:BatteryStatsSerive。

这个Service在ActivityManagerService中创建,代码如下:
1mBatteryStatsService = new BatteryStatsService(new File(systemDir, 'batterystats.bin').toString());
其他的模块⽐如WakeLock和PowerManagerService会向BatteryStatsService喂数据,数据是存放到系统⽬录batterystats.bin⽂件,然后交于BatteryStatsImpl这个数据分析器来进⾏电量数据的分析,系统的设置就是这样得到电量的统计信息的。

拿到相关的数据后,电量的计算⼜是如何得出的呢?这⾥⽤到了如下的计算公式:
应⽤运⾏总时间 = 应⽤在Linux内核态运⾏时间 + 应⽤在Linux⽤户态运⾏时间
CPU⼯作总时间 = 软件运⾏期间CPU每个频率下⼯作的时间之和⽐例
应⽤消耗的电量 = CPU每个频率等级下⼯作的时间⽐例/CPU⼯作总时间 * 应⽤运⾏总时间
* 不同频率下消耗的电量 + 数据传输消耗的电量(WI-FI或者移动⽹络)+ 使⽤所有传感器消耗的电量 + 唤醒锁消耗的电量。

相应的代码⽚段如下:
001private 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);
010 }
011 final double averageCostPerByte = getAverageDataCost();
012 long uSecTime = puteBatteryRealtime(
013 SystemClock.elapsedRealtime() * 1000, which);
014 mStatsPeriod = uSecTime;
015
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);
020 double power = 0;
021 double highestDrain = 0;
022 String packageWithHighestDrain = null;
023 Map<String, ? extends BatteryStats.Uid.Proc> proce ssStats = u.getProcessStats(); 024 long cpuTime = 0;
025 long cpuFgTime = 0;
026 long gpsTime = 0;
027 if (processStats.size() > 0) {
028 // Process CPU time
029 for (Map.Entry<String, ? extends BatteryStats.Uid.Proc> ent : processStats.entrySet()) { 030 if (DEBUG)
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];
043 }
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];
051 }
052 cpuTime += tmpCpuTime;
053 power += processPower;
054 if (highestDrain < processPower) {
055 highestDrain = processPower;
056 packageWithHighestDrain = ent.getKey();
057 }
058
059 }
060
061 }
062 }
063 cpuTime = cpuFgTime; // Statistics may not have been gathered yet.
064 }
065 power /= 1000;
066
067 // Add cost of data traffic
068 long tcpBytesReceived = u.getTcpBytesReceived(mStatsType);
069 long tcpBytesSent = u.getTcpBytesSent(mStatsType);
070 power += (tcpBytesReceived + tcpBytesSent) * averageCostPerByte;
071
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) {
081 case Uid.Sensor.GPS:
082 multiplier = mPowerProfile.getAveragePower(PowerProfile.POWER_GPS_ON);
083 gpsTime = sensorTime;
084 break;
085 default:
086 android.hardware.Sensor sensorData = sensorManager
087 .getDefaultSensor(sensorType);
088 if (sensorData != null) {
089 multiplier = sensorData.getPower();
090 }
091 }
092 }
093 power += (multiplier * sensorTime) / 1000;
094 }
095
096 // Add the app to the list if it is consuming power
097 if (power != 0) {
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;
104 mUsageList.add(app);
105 }
106 if (power > mMaxPower)
107 mMaxPower = power;
108 mTotalPower += power;
109 if (DEBUG)
110 Log.i(TAG, 'Added power = ' + power);
111 }
112 }
通过代码我们看到,每个影响电量消耗的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">
2 <intent-filter>
3 <action android:name="android.intent.action.ACTION_POWER_CONNECTED"/>
4 <action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/>
5 </intent-filter>
6</receiver>
01public class PowerConnectionReceiver extends BroadcastReceiver {
02 @Override
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;
07
08 int chargeFlag = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
09 boolean usbCharge = chargeFlag == BATTERY_PLUGGED_USB;
10 boolean acCharge = chargeFlag == BATTERY_PLUGGED_AC;
11 }
12}
1//获取程序是否充电
2
3int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS,-1); 4
5boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||status == BatteryManager.BATTERY_STATUS_FULL;
1// 充电⽅式,usb还是电源
2int chargeFlag = battery.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); 3boolean usbCharge = chargeFlag == BATTERY_PLUGGED_USB;
4boolean acCharge = chargeFlag == BATTERY_PLUGGED_AC;
1不断的检测电量也会影响电池的使⽤时间,我们可以这样做
1<receiver android:name=".BatteryLevelReceiver">
2<intent-filter>
3<action android:name="android.intent.action.ACTION_BATTERY_LOW"/>
<actionandroid:name="android.intent.action.ACTION_BATTERY_OKAY"/> </intent-filter>
4</receiver>
当电量低或者满时会触发
有时间再写确定和监测连接状态
测试结论:
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 == WORK_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("/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();
} };。

相关文档
最新文档