Android應用的電量消耗和優化的策略


 對於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);

010     }

011     final double averageCostPerByte = getAverageDataCost();

012     long uSecTime = mStats.computeBatteryRealtime(

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>




01 public 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  

3 int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS,-1);

4  

5 boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||status == BatteryManager.BATTERY_STATUS_FULL;




1 // 充電方式,usb還是電源

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">

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 == 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();  

    }  

}; 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM