目前行業內有很多電量測試的方法:
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或網絡或兩者. 只使用一個會降低電量損耗.