電量優化 - 電量的統計原理與監控

最近有很多同學來催了,到底還講不講?由於活水轉崗去微信,面試七輪持續了兩個月時間,綠色通道答辯持續了差不多半個月的時間。我發現很多時候都到了身不由己,不是不想去做而是真的沒有時間了。我們想去做點事,卻發現身體和時間真的不夠用。

App “耗電綜合徵”

當我們說一個 App 耗電的時候我們在說什麼?

我們可能是指 App 喫 CPU 導致系統掉電快,也可能是在說系統告警 App 後臺掃描頻繁消耗電量,還可能是在說使用 App 時手機發燙嚴重…… 是的,相對於 Crash、ANR 等常見的 APM 指標,Android App 電量優化更像是一個綜合性的問題。

一方面,造成 App 耗電的原因是多種多樣的,比如 CPU/GPU Load、屏幕、傳感器以及其他硬件開銷等,每個分類的排查思路是大相徑庭的,再加上 AOSP 沒有 “官方” 的耗電異常檢測框架,各個 OEM 廠商自家系統對 App 耗電的監控方案又各不相同(且沒有充分的公開文檔),所以檢測方案需要結合具體 App 項目實際和用戶反饋狀況,針對具體的耗電類型做出考量和取捨。另一方面,耗電問題也經常是比較 “主觀” 的,比如用戶感覺 App 新版本掉電比較快了,或者在戶外氣溫比較高的環境使用 App 時感覺設備發燙了,又或只是單純的因爲使用時間變長了導致系統耗電排行靠前了等等,這些通常都是一些比較微妙的主觀感受,難以量化問題。

因此,如何檢測各種類型的耗電異常,以及如何提煉耗電問題的規則(劃紅線)是優化電量指標的關鍵所在。微信 Android 項目在與 App 耗電異常這項 “疑難雜症” 日常鬥智鬥勇的過程中,產出了一些比較實用的工具和優化思路。本文針對 Anroid App 的耗電問題,本文主講 “App 電量統計原理”。後面的我們會陸續對“耗電異常監控方案”以及相關的 “優化案例” 兩部分進行解析和分享。

App 電量統計原理

電量計算公式

瞭解 App 電量統計原理之前,有必要先複習一下電量計算公式:

電量 = 功率 × 時間

其中需要注意一點的是, 功率 = 電壓 × 電流。而在數碼產品中,元器件一般對電流比較敏感,而電壓基本是恆定的,所以我們直接使用電流來代替功率,這也是我們經常說 “毫安時”(mAh)而不說 “千瓦時 / 度”(kWh)的原因。

Android 硬件模塊的電量統計方式

瞭解計算公式之後,App 的電量統計思路就比較清晰了:

App 電量 = SUM (模塊功率 × 模塊時間)

其中模塊主要是指 Android 設備的各種硬件模塊,主要可以分爲以下三類。



第一類,像 Camera/FlashLight/MediaPlayer/ 一般傳感器等之類的模塊,其工作功率基本和額定功率保持一致,所以模塊電量的計算只需要統計模塊的使用時長再乘以額定功率即可。

第二類,像 Wifi/Mobile/BlueTooth 這類數據模塊,其工作功率可以分爲幾個檔位。比如,當手機的 Wifi 信號比較弱的時候,Wifi 模塊就必須工作在比較高的功率檔位以維持數據鏈路。所以這類模塊的電量計算有點類似於我們日常的電費計算,需要 “階梯計費”。

第三類,也是最複雜的模塊,CPU 模塊除了每一個 CPU Core 需要像數據模塊那樣階梯計算電量之外,CPU 的每一個集羣(Cluster,一般一個集羣包含一個或多個規格相同的 Core)也有額外的耗電,此外整個 CPU 處理器芯片也有功耗。簡單計算的話,CPU 電量 = SUM (各核心功耗) + 各集羣(Cluster)功耗 + 芯片功耗 。如果往復雜方向考慮的話,CPU 功耗還要考慮超頻以及邏輯運行的信息熵損耗等電量損耗(這方面有興趣的話可以自行拓展查證,Android 系統 CPU 的電量統計只計算到芯片功耗這一層)。屏幕模塊的電量計算就更麻煩了,很難把屏幕功耗合理地分配給各個 App, 因此 Android 系統只是簡單地計算 App 屏幕鎖(WakeLock)的持有時長,按固定係數增加 App CPU 的統計時長,粗略地把屏幕功耗算進 CPU 裏面。

最後,需要特別注意的是,以上提到的各種功率和時間在 Android 系統上的統計都是估算的,可想而知最終計算出來的電量數值可能與實際值相差巨大,Facebook 的工程師對此也有所吐槽:Mistrusting OS Level Battery Levels,這點大家心裏要有一點概念。

Android 系統電量統計服務

Android 系統的電量統計工作,是由一個叫 BatteryStatsService 的系統服務完成的。先了解一下其中四個比較關鍵的角色:

  • 功率:power_profile.xml,Android 系統使用此文件來描述設備各個硬件模塊的額定功率,包括上面提到的多檔位功率和 CPU 電量算需要到的各種參數值。
  • 時長:StopWatch & SamplingCounter,其中 StopWatch ⏱ 是用來計算 App 各種硬件模塊的使用時長,而 SamplingCounter 則是用來採樣統計 App 在不同 CPU Core 和不同 CpuFreq 下的工作時長。
  • 計算:PowerCalculators,每個硬件模塊都有一個相應命名的 PowerCalculator 實現,主要是用來完成具體的電量統計算法。
  • 存儲:batterystats.bin,電量統計服務相關數據的持久化文件。
工作流程

BatteryStatsService 的工作流程大致可以分爲兩個部分:時長統計 & 功耗計算。


BatteryStatsService 時長統計流程

BatteryStatsService 框架的核心是 ta 持有的一個叫 BatteryStats 的類,BatteryStats 又持有一個 Uid [] 數組,每一個 Uid 實例實際上對應一個 App,當我們安裝或者卸載 App 的時候,BatteryStats 就會更新相應的 Uid 元素以保持最新的映射關係。同時 BatteryStats 持有一系列的 StopWatch 和 SamplingCounter,當 App 開始使用某些硬件模塊的功能時,BatteryStats 就會調用相應 Uid 的 StopWatch 或 SamplingCounter 來統計其硬件使用時長。

這裏以 Wifi 模塊來舉例:當 App 通過 WifiManager 系統服務調用 Wifi 模塊開始掃描的時候,實際上會通過 WifiManager#startScan () --> WifiScanningServiceImp --> BatteryStatsService#noteWifiScanStartedFromSource () --> BatteryStats#noteWifiScanStartedLocked (uid) 等一連串的調用,通知 BatteryStats 開啓 App 相應 Uid 的 Wifi 模塊的 StopWatch 開始計時。當 App 通過 WifiManager 停止 Wifi 掃描的時候又會通過類似的流程調用 BatteryStats#noteWifiScanStoppedLocked (uid) 結束 StopWatch 的計時,這樣一來就通過 StopWatch 完成 App 對 Wifi 模塊使用時長的統計。

BatteryStatsService 功耗計算流程

具體電量計算方面,BatteryStats 是通過 ta 依賴的一個 BatteryStatsHelper 的輔助類來完成的。BatteryStatsHelper 通過組合使用 Uid 裏的時長數據、PoweProfile 裏的功率數據(power_profile.xml 的解析實例)以及具體各個模塊的 PowerCalculator 算法,計算出每一個 App 的綜合電量消耗,並把計算結果保存在 BatterySipper [] 數組裏(按計算值從大到小排序)。

還是以 Wifi 模塊來舉例:當需要計算 App 電量消耗的時候,BatteryStats 會通過調用 BtteryStatsHelper#refreshStats () --> #processAppUsage () 來刷新 BatterySipper [] 數組以計算最新的 App 電量消耗數據。而其中 Wifi 模塊單獨的電量統計就是在 processAppUsage 方法中通過 WifiPowerCalculator 來完成的:Wifi 模塊電量 = PowerProfile 預置的 Idle 功率 × Uid 統計的 Wifi Idle 時間 + 上行功率 × 上行時間 + 下行功率 × 下行時間。

public class WifiPowerCalculator extends PowerCalculator {

    @Override
    public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
                             long rawUptimeUs, int statsType) {
        ...
        app.wifiPowerMah =
                ((idleTime * mIdleCurrentMa) + (txTime * mTxCurrentMa) + (rxTime * mRxCurrentMa))
                / (1000*60*60);
    }
}

應用場景

作爲補充,這裏羅列幾個 BatteryStatsService 系統服務的應用場景來說明其工作方式。

Android 系統 App 耗電排行

通過以上分析,我們其實已經知道 Android 系統 App 耗電排行是通過讀取 BatteryStatsHelper 裏的 BatterySipper [] 數據來實現排行的。一般情況下,BatteryStats 的統計口徑是 STATS_SINCE_CHARGED, 也就距離上次設備充滿電到現在的狀態。不過個別 OEM 系統上這裏的統計細節有所不同,有的 Android 設備系統可以顯示最近數天甚至一週以上的 App 的電量統計數據,具體實現細節不得而知,姑且推斷是根據 BatteryStatsHelper 自行定製的服務。

adb dumpsys batterystats & adb bugreport

或許你已經知道怎麼通過 adb dumpsys batterystats 或者 adb bugreport Dump 出系統的電量統計數據,以及如何配合 Battery Historian 工具來分析這些數據,實際上這些 adb 命令都是通過 BatteryStatsService 查詢 BatteryStats 裏持有的 Uid [] 來獲得相應的電量統計數據,具體實現可以參考 com.android.server.am.BatteryStatsService#dump

CPU Load/Usage

“CPU Load xx% yy% zz%” 之類的數據相信大家都或多或少見過,ANR 的 traces.txt、以上的 batterystats 和 bugreport Dump 出來的數據,以及 adb top 命令裏都會顯示類似的 CPU 負載數據,實際上這個數據也是通過 CPU 模塊的統計時長來計算:CPU Load = SUM (App CPU Core 時長時間) / CPU 工作時間。需要注意的是 App CPU 時長是按 CPU Core 爲單位分開計算的,所以計算結果完全可能超過 100%,比如一個 8 核心的 CPU 計算結果的理論上限是 800%。

後面的我們會陸續對“耗電異常監控方案”以及相關的 “優化案例” 兩部分進行解析和分享。最後想送我們一句話,是《大學》中講的:物有本末事有終始,知所先後則近道矣

視頻鏈接:https://pan.baidu.com/s/1GJJCQfL5O68Q8Lt8NDvaoQ
視頻密碼:3bqh

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章