492
阿裏雲
技術社區[雲棲]
小談Android應用的電量消耗和優化策略
眾所周知,對於Android移動應用的開發者來說,耗電量的控製一直是個老大難問題。
工欲善其事,必先利其器,同樣的道理,想要控製耗電量,必須要有工具或者方法比較準確的定位應用的耗電情況。下麵,我們先來分析下如何計算android應用的耗電量。
在android自帶的設置裏麵有電量計算的界麵,如下圖:

我們看下是如何實現的:
在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" /> < action
android: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("https://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();
}
};
想了解更多內容,請直接查看Google IO 2009相關文檔
最後更新:2017-04-03 12:55:42