這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
一、前言
當手機啟動並執行時候,處理各種任務,硬體就會消耗電量。使用者可以通過手機內建的電量監測功能來查看電量使用方式:
電量使用方式(小米).png
一旦使用者發現你的APP耗電量特別大,那可就麻煩了。因此我們需要在設計app的時候,就應該下功夫,充分考慮電量最佳化的問題。
二、電量消耗理論與分析
寫出耗電量低的應用的關鍵是要透徹理解它的理論以及全部過程。下面將對電量消耗的相關理論知識進行介紹。
1、電量消耗的概念
首先要知道,電量的消耗,主要是指硬體的電量消耗(廢話),在電子世界,這種硬體消耗電量來執行任務的過程,叫做逾時電流消耗。
硬體消耗電量.png
不同情況下,相同時間內,消耗的電量是不同的。比如使用飛航模式待機,確實可以堅持10多天。但是我們一旦使用手機,比如使用蜂窩式無線資料交換(3G4G)、螢幕保持喚醒狀態等,電量就會消耗得很快:
耗電分析.png
作為開發人員,我們很想知道我的應用程式執行的哪些任務消耗的電量是最多的?這個問題確實會很棘手。
電量最佳化是方方面面的,比如說減少記憶體的開銷,減少介面的過度繪製,本身就是一種電量最佳化。
2、電量消耗計算
電量消耗的計算與統計是一件麻煩而且矛盾的事情,記錄電量消耗本身也是一個費電量的事情(所以很多手機的定製系統都把這個監測電量的功能閹割掉了)。
唯一可行的方案是使用第三方監測電量的裝置,這樣才能夠擷取到真實的電量消耗(因為第三方硬體監測的時候是用的自己的供電而不是用的手機的電量)。比如使用功耗儀。
開啟螢幕,所有要使用CPU/GPU工作的動作都會喚醒螢幕,都會消耗電量。這和應用程式代碼喚醒裝置還不一樣。比如使用叫醒鬧鐘(wake clock)、AlarmManager、JobSchedulerAPI。因此很難知道自己的應用程式的真實耗電情況。
3、裝置待機與喚醒電量消耗分析
為什麼要單獨拿這個出來講呢,就是因為,喚醒這個瞬間是非常耗電的,下面允許我慢慢介紹。
先來看看待機狀態的電量消耗:
待機狀態電量消耗.png
待機狀態下,電量的消耗是非常少的,這是毋庸置疑的。
使用和喚醒螢幕後:
螢幕喚醒.png
可以看到,螢幕喚醒的一瞬間是非常耗電的,這裡有一條電量使用高峰線。
下面來看看CPU喚醒的曲線(CPU喚醒,螢幕不一定會喚醒):
CPU喚醒.png
同樣的,CPU喚醒的時候也會有一條電量使用高峰線。
CPU喚醒之後:
喚醒之後.png
CPU喚醒之後,裝置的耗電不會出現喚醒的時候的高峰線。
值得注意的是當工作完成後,裝置會主動進行休眠,這非常重要,在不使用或者很少使用的情況下,長時間保持螢幕喚醒會迅速消耗電池的電量。
結論
裝置喚醒的瞬間是有消耗高峰的,因此,當你的工作需要持續的時候,可以考慮保持喚醒狀態。
4、無線蜂窩耗電分析
蜂窩式無線也是耗電量非常可怕的,甚至比WIFI更加耗電,因此這裡單獨拿出來進行分析。
Tips:不使用流量的時候,最好把資料關閉,這樣又省電又省流量。
下面開始分析無線蜂窩耗電的過程:
無線蜂窩耗電過程.png
如所示:
- 當裝置通過無線網發送資料的時候,為了使用硬體,這裡會出現一個喚醒高峰。
- 接下來還有一個高數值,這是發送資料包消耗的電量。
- 然後接受資料包也會消耗大量電量,也看到一個峰值。
- 保持喚醒狀態,耗電比較均衡,很少出現高峰點。
所以我們開啟無線模式這個過程非常耗電,那麼硬體這塊為了防止頻繁開啟關閉耗電,採取了一個無奈的辦法,會在一個小段時間內保持開啟模式,防止短時間內還有資料包需要接收。這些資料非常有用,可是不是所有開發人員都有這個第三方裝置跟蹤。但是使用Android L版本就可以利用到新的一系列的工具來最佳化應用程式的耗電。(這裡顯然不要考慮相容性問題,我只是想測電量消耗問題,同一款APP在不同版本的Android上耗電情況應該不會有太大影響,雖然不同Android版本對電量的最佳化不同,但是我們的分析對象是我們自己的APP本身)
三、電量分析工具Battery Historian的環境搭建與使用
Battery Historian(https://github.com/google/battery-historian)是一款電量使用記錄分析工具。通過ADB擷取的資料,使用Battery Historian工具分析處理後,得到的html結果檔案,用瀏覽器可以直接查看、分析的。
1、環境搭建
有兩種方式,通過Docker或者通過編譯源碼的方式來安裝。
通過Docker安裝(推薦在MAC或者Linux上面使用)
Docker是一種容器,一般用於雲端運算和大資料平台。提倡的一種思想就是:軟體即服務。這句話不是蓋的,一句話就可以將別人發布的docker服務環境一次全部copy過來(注意是整個軟體環境,相當於複製了一台一模一樣的主機,連軟體都不要安裝了,全有了。)
但是,對於Windows使用者來說,Docker只支援Windows10。一般推薦在MAC或者Linux上面使用。
開啟這個Docker服務的命令如下:
docker -- run -p <port>:9999 gcr.io/android-battery-historian/stable:3.0 --port 9999
上面這種是臨時開闢的程式服務,也可以加上-d參數,開啟一個單獨系統服務更正規的服務。
通過編譯源碼的方式安裝(推薦Windows上面使用)
到官網下載GO安裝包:https://golang.org/dl/,這裡給出1.9版本的下載路徑https://golang.org/doc/install?download=go1.9.windows-amd64.msi。
配置環境變數:
- GOROOT: GOROOT的作用是告訴Go 命令和其他相關工具,在哪裡去找到安裝在你系統上的Go包,所以這裡配置的是GO的安裝目錄。
- GOPATH:GOPATH可以簡單理解為是工程的目錄,所以需要手動建立一個GO的工程路徑。
- PATH:把Go的bin目錄放到PATH環境變數中。
安裝完之後,輸入下面的命令檢查GO環境是否正確安裝:
go version
這裡參考廖雪峰的部落格。
這裡參考廖雪峰的部落格,注意添加Python到PATH中。
這個沒什麼好說的。
通過下面的GO命令下載Battery Historian源碼並且編譯運行:
go get -d -u github.com/google/battery-historian/...
Tips:下載到GOPATH配置目錄下。
切換目錄:
cd $GOPATH/src/github.com/google/battery-historian
執行配置指令碼(編譯),注意這個過程可能需要翻牆:
go run setup.go
啟動battery historian:
go run cmd/battery-historian/battery-historian.go
這時候,開啟瀏覽器,輸入http://127.0.0.1:9999/,顯示如下頁面代表安裝成功(需要訪問Google的靜態資源,需要翻牆):
環境搭建好執行個體.png
2、資料擷取
battery-historian工具需要使用bugreport中的Battery History,因此需要如下的操作。
重啟adb服務:
adb kill-serveradb start-server
這一步很重要,因為當我們開發時做電量記錄時會開啟很多可能造成衝突的東西。為了保險起見我們重啟adb。
通過以下命令來開啟電池資料的擷取以及重設:
adb shell dumpsys batterystats --enable full-wake-historyadb shell dumpsys batterystats --reset
上面的操作很重要,因為可以過濾掉不需要的資料。然後斷開資料線(防止資料線造成充放電資料幹擾),運行自己的APP進行測試。
重新串連USB調試,通過下面的命令擷取資料:
adb bugreport bugreport.zip(6.0以及以下的,使用txt)
注意:7.0以下的,需要使用舊版本的adb工具,不然沒法採集,參考文章http://blog.csdn.net/mwq30123/article/details/53888449
注意:官方SDK文檔匯出檔案方式為:adb shell dumpsys batterystats > batterystats.txt。使用python historian.py batterystats.txt > batterystats.html查看資料。這是battery-historian老版本的使用方式. 目前Battery Historian已更新2.0版本, 推薦使用bugreport方式匯出資料分析, 可以看到更多資訊。
注意:模擬器可能擷取不到有用的電量資料,網頁沒有顯示電量資訊,如所示:(你可能需要一部實體手機)
電量分析.png
最後一步是開啟http://127.0.0.1:9999/,提交資料進行測試。
電量分析(基於V2.0版本的battery historian)
上面一路過來也是不容易啊,筆者也是踩了不少的坑才過來的,所以有什麼問題儘管在我的部落格下面留言,我盡量一一解答。
新版本的電量分析介面如所示:
電量分析介面.png
幾個我們需要關心的重要參數說明:
- CPU running:CPU的運行狀態,是否被喚醒。如果把滑鼠放到上面去,還能看到更多的資訊,如CPU喚醒的原因。
- Wakelock:喚醒鎖
- Screen:螢幕是否開啟
- Top APP:當前最上層的APP
- Mobile network type:網路類型,其中需要注意的是,“免費網路可能包括wifi、藍芽網際網路共用、USB網際網路共用”
- Mobile radio active:移動蜂窩訊號
- WiFi supplicant:wifi是否開啟
- WiFi signal strength:wifi強度
- Audio:音頻是否開啟
- Battery Level:電量
- Plugged:是否正在充電,以及滑鼠放在上面的時候可以看到充電類型,包括AC(充電器)、USB、其它(例如無線充電)
- 底部的橫座標是時間
分析的建議:
- 發現很密集的,不斷喚醒CPU的時候,就可能需要進行最佳化了(喚醒鎖)
- 某些服務是否在充上電以後才執行?
- 網路的使用與電量等等
- 螢幕是否一直常亮?
四、電量最佳化建議
當Android裝置空閑時,螢幕會變暗,然後關閉螢幕,最後會停止CPU的運行,這樣可以防止電池電量掉的快。在休眠過程中自訂的Timer、Handler、Thread、Service等都會暫停。但有些時候我們需要改變Android系統預設的這種狀態:比如玩遊戲時我們需要保持螢幕常亮,比如一些下載操作不需要螢幕常亮但需要CPU一直運行直到任務完成。從而防止因為喚醒的瞬間而耗更多的電。
1、判斷充電狀態
為了省電,有些工作(不需要及時地和使用者互動的操作)可以放當手機插上電源的時候去做。比如手機小幫手類的項目,自動清理手機垃圾,自動備份上傳圖片、連絡人等到雲端等代碼,可以等待使用者充電以及有網路的時候再執行,判斷是否充電的代碼如下:
private boolean checkForPower() { //擷取電池的充電狀態(註冊一個廣播) IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); Intent res = this.registerReceiver(null, filter); //通過使用BatteryManager的參數資訊判斷充電狀態 if (res != null) { int chargePlug = res.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); boolean usb = chargePlug == BatteryManager.BATTERY_PLUGGED_USB;//usb充電 boolean ac = chargePlug == BatteryManager.BATTERY_PLUGGED_AC;//交流電 //無線充電,這個需要API>=17 boolean wireless = false; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { wireless = chargePlug == BatteryManager.BATTERY_PLUGGED_WIRELESS; } return (usb || ac || wireless); } else { return false; }}
判斷網路是否串連的代碼如下:
private boolean isNetWorkConnected() { //判斷網路連接 ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); return (activeNetworkInfo != null && activeNetworkInfo.isConnected());}
需要添加許可權:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
2、螢幕保持常亮
為了防止螢幕喚醒一瞬間耗電過多,有一些應用,比如遊戲、支付頁面,需要保持螢幕常亮來節省電量:
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
也可以在布局檔案裡面使用,但是沒有那麼靈活:
android:keepScreenOn="true"
注意:一般不需要人為的去掉FLAG_KEEP_SCREEN_ON的flag,windowManager會管理好程式進入後台回到前台的的操作。如果確實需要手動清掉常亮的flag,使用getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
3.1、使用wake_lock
系統為了節省電量,CPU在沒有任務忙的時候就會自動進入休眠。有任務需要喚醒CPU高效執行的時候,就會給CPU加wake_lock鎖。wake_lock鎖主要是相對系統的休眠而言的,意思就是我的程式給CPU加了這個鎖那系統就不會休眠了,這樣做的目的是為了全力配合我們程式的運行。有的情況如果不這麼做就會出現一些問題,比如等及時通訊的心跳包會在熄屏不久後停止網路訪問等問題。所以裡面是有大量使用到了wake_lock鎖。
PowerManager這個系統服務的喚醒鎖(wake locks)特徵來保持CPU處於喚醒狀態。喚醒鎖允許程式控制宿主裝置的電量狀態。建立和持有喚醒鎖對電池的續航有較大的影響,所以,除非是真的需要喚醒鎖完成儘可能短的時間在後台完成的任務時才使用它。比如在Acitivity中就沒必要用了。一種典型的代表就是在螢幕關閉以後,後台服務繼續保持CPU運行。
如果不使用喚醒鎖來執行後台服務,不能保證因CPU休眠未來的某個時刻任務會停止,這不是我們想要的。(有的人可能認為以前寫的後台服務就沒掉過鏈子呀運行得挺好的,1.可能是你的任務時間比較短;2.可能CPU被手機裡面很多其他的軟體一直在喚醒狀態)。
其中,喚醒鎖有下面幾種類型:
喚醒鎖的類型.png
wake_lock兩種鎖(從釋放、使用的角度來看的話):
- 一種計數鎖
- 非計數鎖(鎖了很多次,只需要release一次就可以解除了)
Tips:請注意,自 API 等級17開始,FULL_WAKE_LOCK將被棄用,應使用FLAG_KEEP_SCREEN_ON代替。
綜上所述,為了防止CPU喚醒一瞬間耗電過多,在執行關鍵代碼的時候,為了防止CPU睡眠,需要使用喚醒鎖來節省電量:
//建立喚醒鎖PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);PowerManager.WakeLock wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "partial_lock");//擷取喚醒鎖wakeLock.acquire();//一些關鍵的代碼//釋放喚醒鎖wakeLock.release();
需要添加許可權:
<uses-permission android:name="android.permission.WAKE_LOCK"/>
Tips:擷取與釋放喚醒鎖需要成對出現
Tips:有一些意外的情況,比如小米手機是做了同步心跳包(心跳對齊)(如果超過了這個同步的頻率就會被屏蔽掉或者降頻),所有的app後台喚醒頻率不能太高,這時候就需要降頻,比如每隔2S中去請求。
3.2、使用WakefulBroadcastReceiver
上面提到,典型的使用情境就是後台服務需要保持CPU保持運行,推薦的方式是使用WakefulBroadcastReceiver:使用廣播和Service(典型的IntentService)結合的方式可以讓你很好地管理後台服務的生命週期。
WakefulBroadcastReceiver是BroadcastReceiver的一種特例。它會為你的APP建立和管理一個PARTIAL_WAKE_LOCK類型的WakeLock。WakefulBroadcastReceiver把工作交接給service(通常是IntentService),並保證交接過程中裝置不會進入休眠狀態。如果不持有WakeLock,裝置很容易在任務未執行完前休眠。最終結果是你的應用不知道會在什麼時候能把工作完成,相信這不是你想要的。
例子:
服務:
public class MyIntentService extends IntentService { public MyIntentService(String name) { super(name); } public MyIntentService() { super(MyIntentService.class.getSimpleName()); } @Override protected void onHandleIntent(@Nullable Intent intent) { if (intent != null) { //擷取參數 Bundle extras = intent.getExtras(); //執行一些需要CPU保持喚醒的代碼 //執行結束,釋放喚醒鎖 MyWakefulReceiver.completeWakefulIntent(intent); } }}
廣播接收者:
public class MyWakefulReceiver extends WakefulBroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Intent service = new Intent(context, MyIntentService.class); startWakefulService(context, service); }}
需要使用服務的時候,像一般的方式一樣即可:
Intent intent = new Intent(this, MyIntentService.class);//傳遞參數intent.setData(Uri.parse("xxx"));
Tips:注意添加許可權
Tips:注意服務與廣播的註冊
Tips:使用廣播來設計,就是為瞭解耦
3.3、大量高頻次的CPU喚醒及操作使用JobScheduler/GCM
大量高頻次的CPU喚醒及操作,我們可以採取一些演算法來解決,把這些操作安排在一個時間點集中處理,而不是分開處理(這樣就可以防止了喚醒的耗電)。
我們可以使用Google提供的JobScheduler或者GCM來實現這樣的功能。
下面舉一個頻繁請求網路的例子:
這是一個請求網路的服務:
public class MyJobService extends JobService { private static final String TAG = "MyJobService"; @Override public void onCreate() { super.onCreate(); Log.i(TAG, "MyJobService created"); } @Override public void onDestroy() { super.onDestroy(); Log.i(TAG, "MyJobService destroyed"); } /** * 開啟耗時操作 * @param params * @return */ @Override public boolean onStartJob(JobParameters params) { Log.i(TAG, "onStartJob:" + params.getJobId()); if (isNetworkConnected()) { new SimpleDownloadTask() .execute(params); return true; } else { Log.i(TAG, "No connection:" + params.getJobId()); } return false; } /** * jobFinish調用之前會回調 * @param params * @return */ @Override public boolean onStopJob(JobParameters params) { Log.i(TAG, "onStopJob:" + params.getJobId()); return false; } private boolean isNetworkConnected() { ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); return (networkInfo != null && networkInfo.isConnected()); } private class SimpleDownloadTask extends AsyncTask<JobParameters, Void, String> { protected JobParameters mJobParam; @Override protected String doInBackground(JobParameters... params) { mJobParam = params[0]; try { InputStream is = null; int len = 50; URL url = new URL("https://www.baidu.com"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setReadTimeout(10000); //10sec conn.setConnectTimeout(15000); //15sec conn.setRequestMethod("GET"); conn.connect(); int response = conn.getResponseCode(); Log.d(TAG, "The response is: " + response); is = conn.getInputStream(); Reader reader = new InputStreamReader(is, "UTF-8"); char[] buffer = new char[len]; reader.read(buffer); return new String(buffer); } catch (IOException e) { e.printStackTrace(); return null; } } @Override protected void onPostExecute(String result) { //結束任務 jobFinished(mJobParam, false); Log.i(TAG, result); } }}
下面通過迴圈來類比頻繁調用:
ComponentName serviceComponent = new ComponentName(this,MyJobService.class);//頻繁地喚醒JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);for (int i = 0; i < 500; i++) { JobInfo jobInfo = new JobInfo.Builder(i,serviceComponent) .setMinimumLatency(5000)//最小延時5秒 .setOverrideDeadline(60000)//最多執行時間60秒 //.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)//免費的網路---wifi 藍芽 USB .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)//任意網路---wifi .build(); jobScheduler.schedule(jobInfo);}
4、使用AlarmManager來喚醒
當機器一段時間不操作以後,就會進入睡眠狀態。向伺服器的輪詢就會停止、長串連就會斷開,為了防止這樣的情況,就可以使用AlarmManager:
Intent intent = new Intent(this, TestService.class);PendingIntent pi = PendingIntent.getService(this, 0, intent, 0);AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);am.cancel(pi);//鬧鐘在系統睡眠狀態下會喚醒系統並執行提示功能//模糊時間,在API-19中以及以前,setRepeating都是不準確的am.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 1000, 2000, pi);//準確時間,但是需要在API-17之後使用am.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 1000, pi);am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 1000, pi);
該定時器可以啟動Service服務、發送廣播、跳轉Activity,並且會在系統睡眠狀態下喚醒系統。所以該方法不用擷取電源鎖和釋放電源鎖。
關於AlarmManager的更多資訊,請參考其他文章。
在19以上版本,setRepeating中設定的頻率只是建議值(6.0 的源碼中最小值是60s),如果要精確一些的用setWindow或者setExact。
5、其他最佳化
當然,電量最佳化是包括很多方面的,例如:
- 渲染最佳化
- 定位策略最佳化
- 網路最佳化,例如網路緩衝處理,請求方式、次數最佳化、設定逾時時間等等
- 代碼執行效率最佳化
- 防止記憶體流失
等等,電量最佳化無處不在。
深化
首先Android手機有兩個處理器,一個叫Application Processor(AP),一個叫Baseband Processor(BP)。AP是ARM架構的處理器,用於運行Linux+Android系統;BP用於運行即時作業系統(RTOS),通訊協議棧運行於BP的RTOS之上。非通話時間,BP的能耗基本上在5mA左右,而AP只要處於非休眠狀態,能耗至少在50mA以上,執行圖形運算時會更高。另外LCD工作時功耗在100mA左右,WIFI也在100mA左右。一般手機待機時,AP、LCD、WIFI均進入休眠狀態,這時Android中應用程式的代碼也會停止執行。
- Android為了確保應用程式中關鍵代碼的正確執行,提供了Wake Lock的API,使得應用程式有許可權通過代碼阻止AP進入休眠狀態。但如果不領會Android設計者的意圖而濫用Wake Lock API,為了自身程式在背景正常工作而長時間阻止AP進入休眠狀態,就會成為待機電池殺手。比如前段時間的某應用,比如現在仍然乾著這事的某應用。
- AlarmManager 是Android 系統封裝的用於管理 RTC 的模組,RTC (Real Time Clock) 是一個獨立的硬體時鐘,可以在 CPU 休眠時正常運行,在預設的時間到達時,通過中斷喚醒 CPU。(極光推送就是利用這個來做的。)
總結:
- 關鍵邏輯的執行過程,就需要Wake Lock來保護。如斷線重連重新登陸
- 休眠的情況下如何喚醒來執行任務?用AlarmManager。如推送訊息的擷取
最後,通過Wakelock Detector(WLD)軟體可以看到手機中的Wakelock:
WLD.png
參考文章:
http://www.jianshu.com/p/ded0ed6fac3d
http://www.jianshu.com/p/5b8bfa6a6c37
http://www.jianshu.com/p/fc2a4d191e18