開發(fā)者大殺器 —— Battery Historian,刨根問底,揪出 Android App 耗電的元兇代碼

0x00 這是啥?

這是一篇講述應用耗電的文章,圍繞 Android 電量采集機制及第二代 Battery Historian 分析工具講述。文從數(shù)據(jù)采集、導出、環(huán)境搭建、解讀報告的角度出發(fā),從細節(jié)講解整個流程。和大談概念的文章不同,這里將進行實際操作及分析。

寫作動機來源于最近的工作需求,但分析過程中發(fā)現(xiàn)網(wǎng)上資料較為匱乏。在此執(zhí)筆寫作,以便日后回顧,亦作為分享的機會。

0x01 電量統(tǒng)計模塊概述

Android 從兩個層面統(tǒng)計電量的消耗,分別為 軟件排行榜硬件排行榜。它們各有自己的耗電榜單,軟件排行榜為機器中每個 App 的耗電榜單,硬件排行榜則為各個硬件的耗電榜單。這兩個排行榜的統(tǒng)計是互為獨立,互不干擾的。

** 此處主要講述軟件層面的統(tǒng)計。**

具體的說,耗電信息在 設置 -> 電量 中能夠非常直觀的看到。注意,Android 所有功耗統(tǒng)計都是通過代碼估算,沒有集成電路參與匯報。準確度取決于廠商 ROM 所提供的 power_profile.xml 文件。由于不同廠商 power_profile.xml 準確度及源碼有差異,因此不同手機、不同版本的數(shù)據(jù)可能有較大差異。

power_profile.xml 直接影響統(tǒng)計的準確度,并且此文件無法通過應用修改。再次強調(diào),Android 耗電估算沒有硬件的參與,全靠代碼估算。

power_profile.xml文件位于源碼下的 /framework/base/core/res/res/xml/power_profile.xml,部分內(nèi)容展示如下:

  <item name="radio.scanning">0.1</item> <!-- cellular radio scanning for signal, ~10mA -->
  <item name="gps.on">0.1</item> <!-- ~50mA -->
  <!-- Current consumed by the radio at different signal strengths, when paging -->
  <array name="radio.on"> <!-- Strength 0 to BINS-1 -->
      <value>0.2</value> <!-- ~2mA -->
      <value>0.1</value> <!-- ~1mA -->
  </array>
  </array>
  <!-- Different CPU speeds as reported in
       /sys/devices/system/cpu/cpu0/cpufreq/stats/time_in_state -->
  <array name="cpu.speeds">
      <value>400000</value> <!-- 400 MHz CPU speed -->
  </array>
  <!-- Current when CPU is idle -->
  <item name="cpu.idle">0.1</item>
  <!-- Current at each CPU speed, as per 'cpu.speeds' -->
  <array name="cpu.active">
      <value>0.1</value>  <!-- ~100mA -->
  </array>
  <array name="wifi.batchedscan"> <!-- mA -->
      <value>.0002</value> <!-- 1-8/hr -->
      <value>.002</value>  <!-- 9-64/hr -->
      <value>.02</value>   <!-- 65-512/hr -->
      <value>.2</value>    <!-- 513-4,096/hr -->
      <value>2</value>    <!-- 4097-/hr -->
  </array>

這就是在硬件層面統(tǒng)計時,直接參與運算的參數(shù)。無論是軟件耗電統(tǒng)計還是硬件耗電統(tǒng)計,都通過 BatteryStatsHelper 來進行匯總。BatteryStatsHelper位于/framework/base/core/java/com/andorid/internal/os/BatteryStatsHelper.java下。

0x02 軟件耗電統(tǒng)計

BatteryStatsHelper.java 中,有這么一個方法:


    private void processAppUsage(SparseArray<UserHandle> asUsers) {
        final boolean forAllUsers = (asUsers.get(UserHandle.USER_ALL) != null);
        mStatsPeriod = mTypeBatteryRealtime;

        BatterySipper osSipper = null;
        final SparseArray<? extends Uid> uidStats = mStats.getUidStats();
        final int NU = uidStats.size();
        for (int iu = 0; iu < NU; iu++) {
            final Uid u = uidStats.valueAt(iu);
            final BatterySipper app = new BatterySipper(BatterySipper.DrainType.APP, u, 0);

            mCpuPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType);
            mWakelockPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType);
            mMobileRadioPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType);
            mWifiPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType);
            mBluetoothPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType);
            mSensorPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType);
            mCameraPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType);
            mFlashlightPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType);

            final double totalPower = app.sumPower();
            if (DEBUG && totalPower != 0) {
                Log.d(TAG, String.format("UID %d: total power=%s", u.getUid(),
                        makemAh(totalPower)));
            }
        }
        ... // code
    }

processAppUsage()方法中,一個應用的總功耗在這里體現(xiàn)出來了:

  • cpu
  • Wakelock(保持喚醒鎖)
  • 無線電(2G/3G/4G)
  • WIFI
  • 藍牙
  • 傳感器
  • 相機
  • 閃光燈

這些數(shù)據(jù),將決定著你的應用在耗電排行榜中的位置,以及是否給予用戶警告高耗電。這些警告對于應用來說可能是致命的,用戶可能因此而卸載應用。

應用總功耗是上述八個統(tǒng)計值的和。這八個統(tǒng)計器同繼承自 PowerCalculator.java

具體來說,這八個耗電計算器的算法分別如下:

耗電統(tǒng)計概述就如上所述??偟膩碚f并不復雜,通過聚合八種不同方式的消耗,來得出總的耗電量,并給予用戶展示。

0x03 耗電數(shù)據(jù)的采集

數(shù)據(jù)的采集是機器單方面的行為,不需要依賴第三方的輔助,因為這是 Android 系統(tǒng)級的功能。但在這之前,需要做一些準備。

請事先打開開發(fā)者模式。USB 接入計算機。在終端中執(zhí)行:

adb shell dumpsys batterystats --enable full-wake-history

默認情況下,喚醒(wake)數(shù)據(jù)是不會被采集的,因此我們需要將其啟用。如果采集的不是全量 wake up 數(shù)據(jù),在分析階段則不能很好的觀測數(shù)據(jù)。

隨后終端執(zhí)行:

adb shell dumpsys batterystats --reset

此命令會清空歷史采集的信息。

最后,拔出 USB。
最后,拔出 USB。
最后,拔出 USB。

重要的事說三遍!

現(xiàn)在,耗電統(tǒng)計已經(jīng)開始了。沒錯,耗電統(tǒng)計就是一直開著的,并且無法關閉:這是一個系統(tǒng)級別的功能。

為什么要拔出 USB?因為如果你一直插著 USB ,如果電充滿了,你的數(shù)據(jù)會被清空的。Batterystats 只會記錄最后一次充滿電后的記錄,因此強烈建議先把電充滿,完成以上操作后,拔出 USB 電源。

接下來,就像日常使用手機一樣,操作你想要統(tǒng)計的應用。耗電記錄器會在后臺統(tǒng)計整臺機子所有的耗電情況。沒錯,不需要事先指定目標 App ,所有 App 都會被統(tǒng)計。這也說明,任何人都能夠統(tǒng)計任何已安裝的應用。因此,除了統(tǒng)計自家 App ,也能用于統(tǒng)計競品。

當你覺得操作得差不多了,連接到 USB,終端執(zhí)行:

adb bugreport bugreport.zip

如果是 Android 6.0 及以下執(zhí)行:

adb bugreport > bugreport.txt

bugreport.txt 就是記載著整臺手機耗電信息的源數(shù)據(jù)。

最后終端執(zhí)行:

adb shell dumpsys batterystats --disable full-wake-history

不要忘了關閉全量記錄喚醒。保持開啟會造成性能問題,除非在電量收集階段,否則建議保持關閉。

接下來,搭建分析環(huán)境:Battery Historian。

0x04 搭建 Battery Historian & 上傳 bugreport

Battery Historian ,是谷歌出品的耗電分析器。通過 Battery Historian,可將導出的 bugreport 文件可視化。在第一代的 Battery Historian 中,google 使用了 python 作為數(shù)據(jù)解析工具。拿到 bugreport 文件后,通過終端執(zhí)行 python 來生成可視化的 html 文件。這種方法使用起來較為麻煩,而且無法部署到服務器。因此在第二代 Battery Historian,google 選擇了使用 docker 容器?,F(xiàn)在完全不推薦使用第一代 Battery Historian,已經(jīng)許久沒有維護了,而且功能過于簡陋。

我在此演示 Mac 環(huán)境的搭建,其他操作系統(tǒng)大同小異,可參考 Battery Historian 官方教程。

首先下載 Docker 并安裝。

啟動 Docker。點擊上方狀態(tài)欄 Docker 圖標,如圖所示 "Docker is running" 則表示啟動成功。

docker

下一步,啟動終端,執(zhí)行:

docker run -p 9998:9999 gcr.io/android-battery-historian:2.1 --port 9999

如果您未曾運行過此 Docker 鏡像,將會自動下載此鏡像并安裝。9998 端口指的是映射到你的本地端口,這意味著,當鏡像執(zhí)行后,可通過127.0.0.1:9998訪問此鏡像。你可以自行更改此端口。等待鏡像下載完成,再次執(zhí)行上一條命令。如果成功,將輸出如下信息:

?  ~ docker run -p 9998:9999 gcr.io/android-battery-historian:2.1 --port 9999
2017/06/04 10:24:13 Listening on port:  9999

鏡像的9999端口已被監(jiān)聽,并映射到實體機器的9998端口。分析平臺部署完成了,開始上傳 bugreport 文件進行分析。

現(xiàn)在,訪問127.0.0.1:9998,成功打開 Battery Historian 分析平臺:

除了單一上傳 bugreport 文件外,還支持更多的分析文件上傳。此外,還能對比兩份bugreport文件。這對比功能簡直是神器。這里就不展開述說了,直接上傳 bugreport.txt。

上傳后效果如下:

現(xiàn)在一起來看看怎么使用 Battery Historian 分析 bugreport文件。

0x05 鳥瞰 Battery Historian

再次強調(diào),bugreport 文件包含了整臺手機運行狀況,并非單一某個 app,因此查看圖表時要特別注意,數(shù)據(jù)所展示的是當前選中的 app 數(shù)據(jù)還是全部 app 的疊加。

現(xiàn)在,我用微信作為分析目標。選中 com.tencent.mm。

現(xiàn)在,這張圖表第一個坑爹的地方出現(xiàn)了。選中目標包名前后,圖標數(shù)據(jù)會有些不一樣的地方。選中前:

選中后:

注意到加粗的 Top app , Activity Manager proc , JobScheduler 了嗎?這幾個數(shù)據(jù)在選中后,圖表數(shù)據(jù)會變?yōu)閮H有當前選中 app 的數(shù)據(jù),而其他數(shù)據(jù)仍然是整臺機子的全量數(shù)據(jù)。一不小心,還能坑你很多次。此處是初次使用 Battery Historian 需要特別注意的地方。用鼠標指向圖標,可粗略地觀察數(shù)據(jù)的變化。

數(shù)據(jù)分析分為三個 Tables,分別是 System Stats , History Stats , App Stats。System Stats 和 App Stats 是重點觀測和分析對象。

System Stats 包含了機子整體概況,包括整臺機子在這段期間消耗了多少電量,所有 app 使用 Wakelocks、JobScheduler、CPU、Wifi、傳感器等等一切的所有情況。

接下來則是 App Stats,所有的優(yōu)化都是為了此處的數(shù)據(jù)而努力。

0x06 讀懂 App Stats

App Stats 所展示的都是所選定包名所產(chǎn)生的數(shù)據(jù),不會受到外部因素的影響。

Misc Summary 部分概述了所選定 app 在收集階段的活動概況:

如上所述,

  • 電量消耗占用了總消耗的3.94%;
  • 前臺運行了 3 小時 34 分鐘;
  • 震動了 8 次,共 225 毫秒;
  • CPU 用戶態(tài)時間 22 分 33 秒;
  • Alarm 喚醒 40 次。

來看一個具體的數(shù)據(jù):查看 App Stats 下的 Wakelocks 數(shù)據(jù)區(qū)域:

注意,顯示為 WakerLock:xxxxxxx 是混淆導致。您自己的開發(fā)包不會存在此情況。

你還記不記得 App 的耗電排行榜是如何計算的?上面這些數(shù)據(jù)都會影響文章開頭所提到的耗能計算公式。

舉個例子,假如你一直持有一個 WakePowerLock,但你什么都沒干 —— 這時候其實是不會產(chǎn)生真正耗電的,對吧。但因為你持有一個 Lock,公式就是這么算的:wakeLockTime * wakeLockPower。即使你啥也沒干,Android 系統(tǒng)也認為你在耗電,這時候就很吃虧了。

再來看看 Alarm 喚醒(App Stats 下的 Wakeup alarm info):

查看你自家的 app,可能會驚訝的發(fā)現(xiàn)有如此之多不必要的喚醒。Wakeup Alarm 和 Scheduled Job 可能被某些廠商用于檢測頻繁后臺喚醒,并向用戶展示該信息。

最后,再來看看 Sensor Use 部分:

看看您自家的應用是否有過多的傳感器調(diào)用?如非必要,能復用上一次的 GPS 數(shù)據(jù)嗎?這些所有的資源消耗,都會被算入能耗當中。當您的 app 在耗電榜上屢次得冠,就離卸載不遠了。

0x07 最后

大多數(shù)情況下,優(yōu)化的效果會是驚人的。這當中可能會遇到一些阻礙,包括產(chǎn)品需求的沖突。盡管試著去協(xié)商,試著向傳達能耗所帶來的代價。

最后的最后,所帶來的滿滿的成就感,也許就是作為開發(fā)者最大的榮幸? (>_<)

內(nèi)推

現(xiàn)在,歡聚時代(YY Inc.)及虎牙(HUYA Inc.)所有崗位(包括但不限于 Android、iOS、Java、前端、大數(shù)據(jù)、機器學習、音視頻算法、其他非技術崗均可)均可進行內(nèi)部推薦。

發(fā)送簡歷到 zhujiajun#yy.com(#替換成@),并附上簡歷,即可內(nèi)推。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容