效能優化總綱:
大概會花一個月左右的時間出7-8個專題來分享一下在工作和學習中積累下來的Android效能優化經驗。
希望大家會持續關注。
現在是專題三:電池電量優化
但這也僅僅是為大家提供一些思路與較為全面的總結,算不上什麼,希望有錯誤或問題在下面評論。
也歡迎點開我的掘金、簡書、CSDN主頁看看其他文章。
最後完結以後會將思維導圖與優化框架整理出來,請期待。
題記
電池雖小,地位卻非常重要。移動裝置使用電池,做任何事情都要費電。而大多數情況下,白天很少有機會給電池充電,哪怕你帶了電寶,也可能出現不夠用的情況。
而作為開發者,如果你的程式被使用者發現耗電量過多很容易被解除安裝,再也不用,是非常致命的,因此我們要制定一系列解決方案,防止此類事情發生。
本章帶領大家探討如何測量電池的使用量,以及即可以省電,又不影響使用者體驗的方法。
一、電池
一般來說,充滿電的狀態可以保證手機正常使用1-2天,除去螢幕和CPU所消耗的電量以外,裝置使用多少電量嚴重以來所有應用都做了什麼,也就是取決於你的應用是如何設計和實現的。
一般有如下功能:
- 執行程式碼(顯而易見)
- 資料傳輸(上傳和下載,使用WiFi,2G,3G,4G)
- 定位
- 感測器
- 渲染影像
- 喚醒任務
在學習如何最大限度的減少耗電量之前,我們想要有辦法來測量。
測量電池用量
1、Battery Historian
Battery Historian是Android 5.0開始引入的新API。通過下面的指令,可以得到裝置上的電量消耗資訊:
$ adb shell dumpsys batterystats > xxx.txt //得到整個裝置的電量消耗資訊
$ adb shell dumpsys batterystats > com.package.name > xxx.txt //得到指定app相關的電量消耗資訊複製程式碼
得到了原始的電量消耗資料之後,我們需要通過Google編寫的一個python指令碼把資料資訊轉換成可讀性更好的html檔案:
$ python historian.py xxx.txt > xxx.html複製程式碼
開啟這個轉換過後的html檔案,可以看到類似TraceView生成的列表資料,這裡的資料資訊量很大,這裡就不展開了。
3) Track Battery Status & Battery Manager
我們可以通過下面的程式碼來獲取手機的當前充電狀態:
IntentFilter filter=new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent batteryStatus=this.registerReceiver(null,filter);
int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED,-1);
boolean acCharge=(chargePlug==BatteryManager.BATTERY_PLUGGED_AC);
if(acCharge){
Log.v(LOG_TAG,“Thephoneischarging!”);
}複製程式碼
在上面的例子演示瞭如何立即獲取到手機的充電狀態,得到充電狀態資訊之後,我們可以有針對性的對部分程式碼做優化。比如我們可以判斷只有當前手機為AC充電狀態時 才去執行一些非常耗電的操作。
private boolean checkForPower(){
IntentFilter filter=new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent batteryStatus=this.registerReceiver(null,filter);
int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED,-1);
boolean usbCharge=(chargePlug==BatteryManager.BATTERY_PLUGGED_USB);
boolean acCharge=(chargePlug==BatteryManager.BATTERY_PLUGGED_AC);
boolean wirelessCharge=false;
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.JELLY_BEAN_MR1){
wirelessCharge=(chargePlug==BatteryManager.BATTERY_PLUGGED_WIRELESS);
}
return(usbCharge||acCharge||wirelessCharge);
}複製程式碼
2、另一種方法得到耗電量
APP獲取電量演算法。
經過檢視原始碼,我們看到app計算電量的演算法如下:
- 在主Activity裡面 info.getBatteryStats() 就搞定了。 首先 load(),如果load失敗,走CPU時間計算,通過getAppListCpuTime這樣函式。
CPU的時間計算,有3個核心步驟:
- ActivityManager遍歷runningApp程式,獲取對應pid
- getAppProcessTime(pid)通過讀取/proc/pid/stat檔案,拿取APP在CPU的執行時間。
- 重新為BatterySipper附值:+time;
- 獲取APP消耗 processAppUsage();也分三步走:
- 通過PowerProfile 獲取cpu的速度層次(speedsteps),方便後面使用
- 根據不同CPU的速度等級,計算cpu在某個速度下的電量,mA毫安
- mPowerProfile.getAveragePower(PowerProfile.POWERCPUACTIVE,p)
很多地方都用到這個API獲取power。
那它究竟做了些什麼呢?
檢視系統原始碼可以知道:
實際上這句話是獲取1個叫PowerMap的資料結果,獲得電量。而PoweMap的賦值,是來源於com.android.internal.R.xml.powerprofile 的檔案。
關於該檔案的獲取 android-版本號/core/res/res/xml/powerprofile.xml
計算各種耗電量的詳細演算法是:
來自深入淺出Android App耗電量統計
總結App耗電量計算公式:(
Uid_Power(App耗電量,單位:mAh) = Uid_Power1 + Uid_Power2 + Uid_Power3 + Uid_Power4 + Uid_Power5
Uid_Power1 = (Process1_Power + … + ProcessN_Power);
Process_Power = (CPUSpeed_Time * POWER_CPU_ACTIVE);
Uid_Power2 = PartialWakeLock_Time * POWER_CPU_WAKE
Uid_Power3 = ( tcpBytesReceived + tcpBytesSent ) * averageCostPerByte
Uid_Power4 = wifiRunningTimeMs * POWER_WIFI_ON
Uid_Power5 = (Sensor1_Power + … + SensorN_Power)
Sensor_Power = Sensor_Time * Power_Sensor
二、禁用電池廣播
系統除了定義了ACTION_BATTERY_CHANGED包含了電池資訊,還定義了應用可以使用的4個廣播Intent:
- ACTION_BATTERY_LOW
- ACTION_BATTERY_OKAY
- ACTION_POWER_CONNECRED
- ACTION_POWER_DISCONNECTED
當我們在一個廣播接收器接收系統傳送的這四個廣播時,只要有一個發生,應用就會啟動。這樣有一個嚴重的缺陷,如果你在前臺執行時,是沒有問題的,但是如果在後臺時,還出現Toast訊息(非系統提醒),就有可能干擾其他應用,損害使用者體驗。
所以我們有一個很好的解決:
- 只有當應用在前臺執行時才可以啟用廣播
- 步驟:
1、廣播接收器預設必須是禁用
2、廣播接收器必須在onResume()中啟用,在onPause()被禁用
三、控制網路
現在基本所有的應用都必須在裝置和伺服器之間傳遞資料,就像獲取電池狀態一樣,應用需要獲取裝置商的網路連結資訊。ConnectivityManager類提供了API。供應用呼叫以此訪問網路資訊。
Android裝置通常由多個資料連線:
- Bluetooth
- Ethernet
- Wi_Fi
- WiMax
- 行動網路(EDGE、UMTS、LTE)
為了最大限度延長電池的使用時間,我們需要直到如下事情:
- 後臺資料設定
- 資料傳輸頻度
後臺資料
可以通過下面這個方法來獲取後臺資料的設定,不過在4.0以後,這個方法始終返回為true,當強行不允許時,網路會斷開。
ConnectivityManager的getBackgroundDataSetting()複製程式碼
資料傳輸
傳輸速率的差異非常大,從小於每秒100Kb的GPRS資料連線到每秒幾Mb的LTE或Wifi都有。除了連線型別,NetWorkInfo類還指定了連線的子型別,例如:
-
NETWORK_TYPE_GPRS(API 1)
-
NETWORK_TYPE_LTE(API 11)
-
NETWORK_TYPE_HSPAP(API 13)
如果建立和部署了新技術,也會增加新的子型別。留意一下每個SDK版本的改動。
人們都習慣更快的連線,即使WiFi晶片耗電量比較多,但Wifi的速率和免費可以讓資料在最短時間最小成本完成傳輸,從而降低電池消耗。
如果能控制資料的傳輸型別,就可以現壓縮資料,再傳輸到裝置上。雖然解壓縮資料耗費CPU,也多用了些電量,但傳輸速度大大加快,資料通訊裝置可以很快關閉,從而延長了電池壽命。通常我們這麼做:
- 使用GZIP壓縮文字資料,使用GZIPInputStream類訪問資料;
- 使用匹配裝置解析度的資源(比如:不必為320480的螢幕下載19201080的圖片)
四、定位
現在很多的App都會做一件事:獲取你的定位資訊。一些使用者可能願意犧牲電池壽命來頻繁的更新位置,而其他人定遠縣誌更新次數,以確保裝置點亮不會很快消耗完,所以需要提供不同的昔陽縣來滿足使用者需求。
1、登出監聽器
還是和處理廣播那樣,在onPause()中呼叫removeUpdates()可以登出監聽器。
2、並且可以用requestLocationUpdates()調整更新頻率。選擇合適的時間間隔和最小間隔距離可以適應不同的場景。
3、通過選擇不同的位置服務來控制,如下:
- GPS定位
- WIFI定位
- 基站定位
- AGPS定位
這四種定位的概念想必大多數都知道了,不知道的戳這裡
五、感測器
感測器是個很有意思的東西,與定位服務有點類似:應用向特定的感測器註冊監聽器,獲得更新通知。
也是可以通過降低通知頻率來省點,由於,每個裝置不同,應用可以測量這4種延遲通知的頻率,選擇兼顧使用者體驗和省點的那一個。另一種策略是,當發現值不變化時,使用NORMAL或UI延遲,當發現有突然變化的時候,且話到GAME或FASTEST延遲。如下:
- SENSOR_DELAY_NORMAL
- SENSOR_DELAY_UI
- SENSOR_DELAY_GAME
- SENSOR_DELAY_FASTEST
六、圖形
應用花費了很多時間在螢幕上畫東西,無論是使用GPU渲染的3D遊戲還是使用CPU的日曆程式,都想只以最少的代價賴在螢幕上展示期望的結果,以延長電池壽命。
如前所述,CPU非全速時使用的電量相對少一些。現代的CPU使用動態調頻喝點呀來節省電力和減少發熱量。這兩種技術通常一起使用,成為DVFS技術(動態電壓和頻率調整),Linux的核心、Android以及現代處理器都支援這種技術。
雖然你不能直接控制電壓和頻率,或將內部組建斷電,但可以控制應用渲染的方式。流暢的幀速率還是要達到的。雖然在Android上幀率有上線(每秒60幀),但優化渲染還是有效果的。除了可能降低能耗,你可可以為後臺執行的應用留出更多的空間,提供了更好的整體使用者體驗。
例如:比如我們手機裡的桌布,可以呼叫onVisibilityChanged()方法。事實上,桌布可以是不可見的。但很容易忘記,持續繪製桌布會消耗很多的電量。
七、喚醒
有時候你得應用可能出於某種原因,需不時的被喚醒,去執行一些操作。
但是很少應用真正需要到提醒時間去強行喚醒裝置。當然鬧鐘這類程式會需要這種功能,但大多是等到使用者主動喚醒才工作。
多數情況下,應用需要將未來某一刻安排提醒到時,但對時間要求並不是很嚴格。為此Android定義了AlarmManager.setInexactRepeating(),它的引數和其“兄弟”setPepeating()基本相同,這種題型更節能,系統也避免了出現不必要的喚醒,Android定義了5個提醒間隔:
- INTERVAL_FIFTEEN_MINUTES
- INTERVAL_HALF_HOUR
- INTERVAL_HOUR
- INTERVAL_HALF_DAY
- INTERVAL_DAY
最好的結果就是所有應用都使用這種提醒,而不用精確的觸發提醒。為了儘可能的節電,應用還可以讓使用者配置提醒的排程,因為有人發現較長時間間隔並不會對使用者體驗有不好的影響。
八、WakeLock
有些時候,一些應用即使長時間不和裝置互動,也要阻止進入休眠狀態,來保持良好的使用者體驗,就比如在看視訊的時候,這種情況下,CPU需要做視訊解碼,同時螢幕保持開啟,讓使用者能夠觀看。此外,視訊播放時螢幕不能變暗。
Android為這種情況設計了WakeLoac類
private void runInWakeLoac(Runnable runnable,int flags){
PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLoac wl = pm.newWakeLock(flag,"My WakeLock");
wl.acquire();
runnable.run();
wl.release();
}複製程式碼
有一點需要注意:需要WAKE_LOCK許可權。
系統的行為取決於WakeLock物件建立時傳入的flags:
- PARTIAL_WAKE_LOCK(CPU開)
- SCREEN_DIM_WAKE_LOCK(CPU開、暗色顯示)
- SCREEN_BRIGHT_WAKE_LOCK(CPU開、明亮顯示)
- FULL_WAKE_LOCK(CPU開、明亮顯示、鍵盤開)
這些標記可以結合使用 - ACQUIRE_CAUSES_WAKEUP(開啟螢幕和鍵盤)
- ON_AFTER_RELEASE(WakeLock是放後繼續保持螢幕和鍵盤開啟片刻)
特別重要的一點:一定要釋放WakeLock,在退出和暫停的時候,否則可能一直顯示,電量很快耗光。
預防問題
防止出現特殊的問題,建議使用帶超時的WakeLoac.acquire()版本,它會在超時後自動釋放。
另外:可以用setKeepScreenOn()方法控制是否要保持螢幕,只要可見的View指定了要保持螢幕,螢幕就會一直保留。
九、總結
使用者不會注意到應用是否延長了電池壽命。但是如果不做任何處理,那就有可能被注意到了。因為單個應用耗電過多,使用者幾天還是可以感受出來的,使用者到時候就會解除安裝用用,所以要對電量使用做一些優化處理,並且給使用者配置選項的自由,來應對使用者產生的各種需求。