Android 電量測試以及電量優化
目前行業內有很多電量測試的方法:
1.1 Batterystats & bugreport
Android 5.0及以上的裝置, 允許我們通過adb命令dump出電量使用統計資訊.
1, 因為電量統計資料是持續的, 會非常大, 統計我們的待測試App之前先reset下, 連上裝置, 命令列執行:
$ adb shell dumpsys batterystats --reset
Battery stats reset.
2, 斷開測試裝置, 操作我們的待測試App.
3, 重新連線裝置, 使用adb命令匯出相關統計資料:
// 此命令持續記錄輸出, 想要停止記錄時按Ctrl+C退出.
$ adb bugreport > bugreport.txt
匯出的統計資料儲存到bugreport.txt, 此時我們可以藉助如下工具來圖形化展示電池的消耗情況.
注意,官方SDK文件匯出檔案方式為:
adb shell dumpsys batterystats > batterystats.txt
使用python historian.py batterystats.txt > batterystats.html檢視資料
是battery-historian老版本的使用方式. 目前Battery Historian已更新2.0版本, 推薦使用bugreport方式匯出資料分析, 可以看到更多資訊.
1.2 Battery Historian
Google提供了一個開源的電池歷史資料分析工具 -Battery Historian,支援5.0(API 21)及以上系統手機的電量分析。
1.2.1 安裝
按照Battery Historian在github上的readme, 一步步安裝即可.
需要注意的是, Battery Historian是Go語言的, 安裝Go的時候需要配置其bin的環境變數.
Python環境需要是2.7的(3.x不行), 建議使用pyenv管理本地的python環境.
另外, 因為Battery Historian是一個網頁版工具, 涉及一些JS引用, 有時需要翻牆.
安裝完成後, 執行:
cd $GOPATH/src/github.com/google/battery-historian
go run cmd/battery-historian/battery-historian.go [--port <default:9999>]
程式執行在http://localhost:9999, 如下:
1.2.2 介面
匯入我們在第一步通過adb bugreport生成的bugreport.txt檔案:
如下短視訊app是Battery Historian測試結果部分截圖:
視訊列表頁
視訊詳情頁
對測試結果資料進行彙總整理:
CPU負載高,會導致耗電量高是顯而易見的
二、耗電量計算原理
根據物理學中的知識,功=電壓*電流*時間,但是一部手機中,電壓值U正常來說是不會變的,所以可以忽略,只通過電流和時間就可以表示電量。模組電量(mAh)=模組電流(mA)*模組耗時(h)。模組耗時比較容易理解,但是模組電流怎樣獲取呢,不同廠商的手機,硬體不同,是否會影響模組的電流呢。看一下系統提供的介面:./frameworks/base/core/java/com/Android/internal/os/PowerProfile.java
該類提供了public double getAveragePower(String type)介面,type可取PowerProfile中定義的常量值,包括POWER_CPU_IDLE(CPU空閒時),POWER_CPU_ACTIVE(CPU處於活動時),POWER_WIFI_ON(WiFi開啟時)等各種狀態。並且從介面可以看出來,每個模組的電流值,是從power_profile.xml檔案取的值。PowerProfile.java只是用於讀取power_profile.xml的介面而已,後者才是儲存系統耗電資訊的核心檔案。power_profile.xml檔案的存放路徑是/system/framework/framework-res.apk。
以Nexus 6P為例,在該路徑獲取到framework-res.apk檔案。使用apktool,對framework-res.apk進行反解析,獲取到手機裡面的power_profile.xml檔案,內容如下所示:
1 <?xml version="1.0" encoding="utf-8"?> 2 <device name="Android"> 3 <item name="none">0</item> 4 <item name="screen.on">169.4278765</item> 5 <item name="screen.full">79.09344216</item> 6 <item name="bluetooth.active">25.2</item> 7 <item name="bluetooth.on">1.7</item> 8 <item name="wifi.on">21.21733311</item> 9 <item name="wifi.active">98.04989804</item> 10 <item name="wifi.scan">129.8951166</item> 11 <item name="dsp.audio">26.5</item> 12 <item name="dsp.video">242.0</item> 13 <item name="gps.on">5.661105191</item> 14 <item name="radio.active">64.8918361</item> 15 <item name="radio.scanning">19.13559783</item> 16 <array name="radio.on"> 17 <value>17.52231575</value> 18 <value>5.902211798</value> 19 <value>6.454893079</value> 20 <value>6.771166916</value> 21 <value>6.725541238</value> 22 </array> 23 <array name="cpu.speeds.cluster0"> 24 <value>384000</value> 25 <value>460800</value> 26 <value>600000</value> 27 <value>672000</value> 28 <value>768000</value> 29 <value>864000</value> 30 <value>960000</value> 31 <value>1248000</value> 32 <value>1344000</value> 33 <value>1478400</value> 34 <value>1555200</value> 35 </array> 36 <array name="cpu.speeds.cluster1"> 37 <value>384000</value> 38 <value>480000</value> 39 <value>633600</value> 40 <value>768000</value> 41 <value>864000</value> 42 <value>960000</value> 43 <value>1248000</value> 44 <value>1344000</value> 45 <value>1440000</value> 46 <value>1536000</value> 47 <value>1632000</value> 48 <value>1728000</value> 49 <value>1824000</value> 50 <value>1958400</value> 51 </array> 52 <item name="cpu.idle">0.144925583</item> 53 <item name="cpu.awake">9.488210416</item> 54 <array name="cpu.active.cluster0"> 55 <value>202.17</value> 56 <value>211.34</value> 57 <value>224.22</value> 58 <value>238.72</value> 59 <value>251.89</value> 60 <value>263.07</value> 61 <value>276.33</value> 62 <value>314.40</value> 63 <value>328.12</value> 64 <value>369.63</value> 65 <value>391.05</value> 66 </array> 67 <array name="cpu.active.cluster1"> 68 <value>354.95</value> 69 <value>387.15</value> 70 <value>442.86</value> 71 <value>510.20</value> 72 <value>582.65</value> 73 <value>631.99</value> 74 <value>812.02</value> 75 <value>858.84</value> 76 <value>943.23</value> 77 <value>992.45</value> 78 <value>1086.32</value> 79 <value>1151.96</value> 80 <value>1253.80</value> 81 <value>1397.67</value> 82 </array> 83 <array name="cpu.clusters.cores"> 84 <value>4</value> 85 <value>4</value> 86 </array> 87 <item name="battery.capacity">3450</item> 88 <array name="wifi.batchedscan"> 89 <value>.0003</value> 90 <value>.003</value> 91 <value>.03</value> 92 <value>.3</value> 93 <value>3</value> 94 </array> 95 </device>
從檔案內容中可以看到,power_profile.xml檔案中,定義了消耗電量的各模組。如下圖所示:
檔案中定義了該手機各耗電模組在不同狀態下的電流值。剛剛提到,電量只跟電流值和時間相關,所以通過這個檔案,再加上模組的耗時,就可以計算出App消耗的電量,App電量=∑App模組電量。劃重點,手機系統裡面的電量排行,也是根據這個原理計算的。
瞭解原理對於平常在App耗電量的測試有很大的幫助。因為獲取到手機power_profile.xml檔案,就可以清楚的知道這個手機上,哪些模組會耗電,以及哪些模組在什麼狀態下耗電量最高。那麼測試的時候,應該重點關注呼叫了這些模組的地方。比如App在哪些地方使用WiFi、藍芽、GPS等等。
例如最近對比測試其他App發現,在一些特定的場景下,該App置於前臺20min內,掃描了WiFi 50次,這種異常會導致App耗電量大大增加。並且反過來,當有case報App耗電量異常時,也可以從這些點去考慮,幫助定位問題。
三、電量測試方法總結
如上,列出的一些常用的電量測試方法。綜合各方法的優缺點,在定製個性化電量測試工具之前,目前採用的方法是Battery Historian。目前行業內,App耗電測試有很多種方案,如果僅僅測試出一個整體的電量值,對於定位問題是遠遠不夠的。藉助Battery Historian,可以檢視自裝置上次充滿電以來各種彙總統計資訊,並且可以選擇一個App檢視詳細資訊。所以QA的測試結果反饋從“這個版本App耗電量”高,變成“這個版本CPU佔用高”“這個版本WiFi掃描異常”,可以幫助更快的定位到問題原因及解決問題。
當然,除了測試方法和測試工具,測試場景設計也非常重要。如果是在App內毫無規律的瀏覽,即使發現頁面有問題,有很難定位到是哪個模組的問題。所以要針對性的設計場景,並且進行一些場景的對比,找出差異的地方。
介紹關於App電量測試中使用的一些基本方法和思路。
電量測試採用的Battery Historian方法,雖然能初步解決問題,但是在實際的應用場景中還存在很多不足。目前美團點評雲測平臺,已經集成了電量測試方法,通過自動化操作,獲取電量測試檔案並進行解析,極大的提高了測試效率。目前每個版本釋出之前,我們都會進行專門的電量測試,保障使用者的使用體驗。在電量測試方面,美團點評測試團隊還在持續的實踐和優化中。
對於一個App, 對應因素主要有:
1 網路請求
我們可能會有發現:
- 測試用的手機充滿電放了一個十一假期還有電, 是因為測試手機沒有上SIM卡.
- 飛航模式下的手機滅屏下, 可能可以放一個月都還有電.
這是因為:
- 手機的通過內建的射頻模組和基站幾乎, 從而連結上網的, 而這個射頻模組(radio)是非常耗電的.
- 為了控制這個射頻模組的耗電, 硬體驅動及Android RIL層做了很多處理. 例如可以單獨關閉radio(飛航模式), 間歇性假休眠radio(有資料發生時才上電, 保持一個頻率的與基站互動)等等.
現如今App都是移動網際網路App, 不可避免的會有大量的網路請求, 會導致radio一直處於活躍狀態, 從而耗電量增加.
2 WakeLock
Android系統本身為了優化電量的使用, 會在沒有操作時進入休眠狀態, 來節省電量. 當然, 為了便於開發(很多應用不可避免的希望在滅屏後還能執行一些事兒, 或是要保持螢幕一直亮著--比如播放視訊), Android提供了一個PowerManager.WakeLock的東西.
我們可以用WakeLock來保持CPU執行, 或是防止螢幕變暗/關閉, 讓手機可以在使用者不操作時依然可以做一些事兒. 然而, 獲取WakeLock很容易, 釋放不好就會成為難題, 消耗電量.
例如我們獲取了一個WakeLock來保持CPU運轉, 做一個複雜運算並將資料上傳到後臺伺服器, 然後釋放該WakeLock. 然而這個過程可能並不像我們想象的那麼快, 可能因為比如伺服器掛掉, 計算出了異常等等WakeLock沒有釋放. 問題就來了, CPU會一直得不到休眠, 而大大增加耗電.
另外, WakeLock還有android:keepScreenOn屬性, 還可以讓螢幕常量, 這可是耗電大戶.
3 GPS
應用中經常會用到定位服務, Android提供了Network定位和GPS定位. 相對來說, GPS會精確得多, 對於一些諸如跑步, 導航類的應用基本會使用GPS定位. 然而, GPS定位也會消耗大量的電量.
儘可能減少App的電量消耗的建議
瞭解了上述的主要的耗電因素, 還有一些程式的耗電問題, 我們通過Battery Historian也可以分析.
針對這些耗電情況, 給出如下優化建議:
1 優化網路請求
1.1 介面設計
1.1.1 API設計
App與Server之間的API設計要考慮網路請求的頻次, 資源的狀態等. 以便App可以以較少的請求來完成業務需求和介面的展示.
1.1.2 Gzip壓縮
使用Gzip來壓縮request和response, 減少傳輸資料量, 從而減少流量消耗.
1.1.3 考慮使用Protocol Buffer代替JSON
從前我們傳輸資料使用XML, 後來使用JSON代替了XML, 很大程度上也是為了可讀性和減少資料量(當然還有對映成POJO的方便程度).
Protocol Buffer是Google推出的一種資料交換格式.
如果我們的介面每次傳輸的資料量很大的話, 可以考慮下protobuf, 會比JSON資料量小很多.
當然相比來說, JSON也有其優勢, 可讀性更高.
本文以網路流量優化的角度推薦protobuf作為一個選擇, 具體還需更具實際情況考慮.
1.1.4 圖片的Size
可以在請求圖片的url中新增諸如質量, 格式, width, height等path來獲取合適的圖片資源
1.1.5 網路快取
適當的快取, 既可以讓我們的應用看起來更快, 也能避免一些不必要的流量消耗.
1.1.6 打包網路請求
當介面設計不能滿足我們的業務需求時. 例如可能一個介面需要請求多個介面, 或是網路良好, 處於Wifi狀態下時我們想獲取更多的資料等.
這時就可以打包一些網路請求, 例如請求列表的同時, 獲取Header點選率較高的的item項的詳情資料.
2 網路代理工具
一般來說, 網路代理工具有兩個作用:
- 截獲網路請求響應包, 分析網路請求
- 設定代理網路, 移動App開發中一般用來做不同網路環境的測試, 例如Wifi/4G/3G/弱網等.
代理工具很多, 諸如Wireshark,Fiddler,Charles等
4 監聽相關狀態
通過監聽裝置的狀態:
- 休眠狀態
- 充電狀態
- 網路狀態
結合JobScheduler來根據實際情況做網路請求. 比方說Splash閃屏廣告圖片, 我們可以在連線到Wifi時下載快取到本地; 新聞類的App可以在充電, Wifi狀態下做離線快取.
3.5 弱網測試&優化
除了正常的網路優化, 我們還需考慮到弱網情況下, App的表現.
3.5.1 Android Emulator
建立和啟動Android模擬器可以設定網路速度和延遲
3.5.2 網路代理工具
設定代理網路,以Charles為例:
保持手機和PC處於同一個區域網, 在手機端wifi設定高階設定中設定代理方式為手動, 代理ip填寫PC端ip地址, 埠號預設8888.
弱網優化, 本質上是在弱網的情況下能讓使用者流暢的使用我們的App. 我們要做的就是結合上述的優化項:
- 壓縮/減少資料傳輸量
- 利用快取減少網路傳輸
- 針對弱網(行動網路), 不自動載入圖片
- 介面先反饋, 請求延遲提交
例如, 使用者點贊操作, 可以直接給出介面的點贊成功的反饋, 使用JobScheduler在網路情況較好的時候打包請求.
2 謹慎使用WakeLock
- WakeLock獲取釋放成對出現.
- 使用超時WakeLock, 以防出異常導致沒有釋放.
1 // Acquires the wake lock with a timeout. 2 acquire(long timeout);
3 監聽手機充電狀態
BatteryManager會發送一個包含充電狀態的持續廣播, 我們可以通過此廣播獲取充電狀態和電量詳情:
1 // 注意: 因為這是一個持續廣播, 我們無需寫receiver, 可以直接通過intent獲取相關資料. 2 IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); 3 Intent batteryStatus = context.registerReceiver(null, ifilter);
例如, 如果裝置正在充電:
1 // Are we charging / charged? 2 int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); 3 boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || 4 status == BatteryManager.BATTERY_STATUS_FULL; 5 6 // How are we charging? 7 int chargePlug = battery.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); 8 boolean usbCharge = chargePlug == BATTERY_PLUGGED_USB; 9 boolean acCharge = chargePlug == BATTERY_PLUGGED_AC;
另外我們也可以監聽充電狀態的變化, 只要裝置連線或斷開電源, BatteryManager就會廣播相應的操作, 我們可以註冊receiver來監聽:
1 <receiver android:name=".PowerConnectionReceiver"> 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>
監聽電池狀態, 可以讓我們將一些操作放在充電或是電量足夠的情況下進行, 以提升使用者體驗. 例如使用者資料同步, Log上傳等.
4 Doze and App Standby
Android 6.0提供了兩個用來節省電量的技術Doze和App Standby.
-
Doze
瞌睡. 如果裝置閒置了一段較長時間, Doze技術將通過延遲後臺網路活動, CPU執行等來減少電量損耗. -
App Standy
應用待機. 不是最近得到過使用者"寵幸"的App, App Standy將延緩這個應用的後臺網路活動.
5 關於定位
- 定位中使用GPS, 請記得及時關閉
1 // Remove the listener you previously added 2 locationManager.removeUpdates(locationListener);
- 減少更新頻率
- 根據實際情況選擇GPS或網路或兩者. 只使用一個會降低電量損耗.